Make a Block Waterloggable

To make blocks waterloggable, implement Waterloggable interface and override some methods.

public class VerticalSlabBlock extends HorizontalFacingBlock implements Waterloggable {
    [...]
}

In this case, we store the Properties.WATERLOGGED as a static field in this class, and you can use WATERLOGGED to access the field. (Unnecessary as it looks, most vanilla Minecraft block classes do that, so we do as well. Of course you can refuse to do that, and use Properties.WATERLOGGED every time to access it, or just directly import the static field.) We have to manually make the block recognize the property, as it is not done for you by Waterloggable interface.

    // Note the ''Properties'' is net.minecraft.state.property.Properties. Don't import the wrong one.
    public static final BooleanProperty WATERLOGGED = Properties.WATERLOGGED;
 
    // Let default value of the WATERLOGGED property become ``false``
    public VerticalSlabBlock(Settings settings) {
        super(settings);
        setDefaultState(getDefaultState()
            .with(Properties.HORIZONTAL_FACING, Direction.NORTH)
            .with(WATERLOGGED, false));
    }
 
    // Make the block recognize the property, otherwise setting the property will through exceptions.
    @Override
    protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
        builder.add(Properties.HORIZONTAL_FACING, WATERLOGGED);
    }

We also have to override getPlacementState, so that the block placed in water is initially waterlogged.

    @Override
    public BlockState getPlacementState(ItemPlacementContext ctx) {
        return this.getDefaultState()
            .with(Properties.HORIZONTAL_FACING, ctx.getHorizontalPlayerFacing().getOpposite())
            .with(WATERLOGGED, ctx.getWorld().getFluidState(ctx.getBlockPos()).isOf(Fluids.WATER));
    }

Override getFluidState so that when it is waterlogged the block displays water.

    @Override
    public FluidState getFluidState(BlockState state) {
        return state.get(WATERLOGGED) ? Fluids.WATER.getStill(false) : super.getFluidState(state);
    }

Override getStateForNeighborUpdate so that it correctly handles the flowing of water.

For versions 1.21.1 and below, write like this:

    @Override
    public BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) {
        if (state.get(WATERLOGGED)) {
            // For 1.17 and below: 
            // world.getFluidTickScheduler().schedule(pos, Fluids.WATER, Fluids.WATER.getTickRate(world));
            // For versions since 1.18 below 1.21.2:
            world.scheduleFluidTick(pos, Fluids.WATER, Fluids.WATER.getTickRate(world));
        }
 
        return super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos);
    }

For versions 1.21.2 and above, write like this:

    @Override
    public BlockState getStateForNeighborUpdate(BlockState state, WorldView world, ScheduledTickView tickView, BlockPos pos, Direction direction, BlockPos neighborPos, BlockState neighborState, Random random) {
        if (state.get(WATERLOGGED)) {
            // For versions since 1.21.2:
            tickView.scheduleFluidTick(pos, Fluids.WATER, Fluids.WATER.getTickRate(world));
        }
 
        return super.getStateForNeighborUpdate(state, world, tickView, pos, direction, neighborPos, neighborState, random);
    }

Now the block becomes waterloggable, and works correctly with water.