react icon indicating copy to clipboard operation
react copied to clipboard

FormControl.Label causing Layout Shift

Open filipedeschamps opened this issue 2 years ago • 2 comments

Describe the bug FormControl.Label is causing Layout Shift on page load (see gif below)

To Reproduce

  1. Create a page using FormControl with a FormControl.Label component. For example:
        <FormControl>
          <FormControl.Label>Username</FormControl.Label>
          <TextInput />
        </FormControl>
  1. Refresh the page.

Expected behavior <FormControl.Label> should not flash on page load.

Screenshots

Gif of the page being refreshed

layout-shift

Static image showing the exact frame where <FormControl.Label> is invisible

image

Desktop (please complete the following information):

  • OS: macOS
  • Browser tested on Chrome (latest) and Safari (latest)

Additional context

There's no logic applied to the <FormControl.Label> component. Here's the code from the gif above:

return (
    <form style={{ width: '100%' }} onSubmit={handleSubmit}>
      <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
        {globalErrorMessage && <Flash variant="danger">{globalErrorMessage}</Flash>}

        <FormControl id="username">
          <FormControl.Label>Nome de usuário</FormControl.Label>
          <TextInput
            ref={usernameRef}
            onChange={clearErrors}
            name="username"
            size="large"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
            aria-label="Seu nome de usuário"
          />
          {errorObject?.key === 'username' && (
            <FormControl.Validation variant="error">{errorObject.message}</FormControl.Validation>
          )}

          {errorObject?.type === 'string.alphanum' && (
            <FormControl.Caption>Dica: use somente letras e números, por exemplo: nomeSobrenome4 </FormControl.Caption>
          )}
        </FormControl>
        <FormControl id="email">
          <FormControl.Label>Email</FormControl.Label>
          <TextInput
            ref={emailRef}
            onChange={clearErrors}
            name="email"
            size="large"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
            aria-label="Seu email"
          />
          {errorObject?.key === 'email' && (
            <FormControl.Validation variant="error">{errorObject.message}</FormControl.Validation>
          )}
        </FormControl>
        <FormControl id="password">
          <FormControl.Label>Senha</FormControl.Label>
          <TextInput
            ref={passwordRef}
            onChange={clearErrors}
            name="password"
            type="password"
            autoCorrect="off"
            autoCapitalize="off"
            spellCheck={false}
            size="large"
            aria-label="Sua senha"
          />
          {errorObject?.key === 'password' && (
            <FormControl.Validation variant="error">{errorObject.message}</FormControl.Validation>
          )}
        </FormControl>
        <FormControl>
          <FormControl.Label visuallyHidden>Criar cadastro</FormControl.Label>
          <Button variant="primary" size="large" type="submit" disabled={isLoading} aria-label="Criar cadastro">
            Criar cadastro
          </Button>
        </FormControl>
      </Box>
    </form>
  );

Additional context (bonus)

I'm in love with Primer. It's been a long time since I've been this happy developing interfaces.

To everyone involved in this project: you are amazing 🤝

filipedeschamps avatar May 01 '22 13:05 filipedeschamps

I'm also having this issue. simple reproduction CodeSandbox demo

The issue occurs because the FormControl is using double-render strategy. https://github.com/primer/react/blob/03f3427ae18c70a143a43545f31bcbcba03ef2e4/src/utils/create-slots.tsx#L25-L30

Only input component was rendered at the first render.

1st render 2nd render
Slots image image
DOM image image

It might be hard to prevent Layout Shift but I hope at least all components can start render since the same render, instead of first render the input then render label or other stuff.

I guess these solutions might work

  1. Wrapping the input component with slot as well, contents would be rendered since the 2nd render. https://github.com/primer/react/blob/03f3427ae18c70a143a43545f31bcbcba03ef2e4/src/FormControl/FormControl.tsx#L133-L138
  2. In SlotsContext, manage slots with useState instead of useRef. React would re-render immediately if its state changed within effect. demo This way could potentially prevent layout shift.

Thanks to every contributors of this project, it is amazing!

EnixCoda avatar Jun 03 '22 14:06 EnixCoda

Hi!

First of all, sorry @filipedeschamps, we missed your initial message!

Thanks for reporting this. @EnixCoda is right, this is related to double rendered strategy for slots, we are tracking it here: https://github.com/primer/react/issues/1690

siddharthkp avatar Jun 03 '22 22:06 siddharthkp

I think this is likely solved by https://github.com/primer/react/pull/2216; now both renders should happen before the first paint so there should be no visible flash. I don't see the problem on my end anymore. Please let us know if you do still see it!

iansan5653 avatar Aug 24 '22 13:08 iansan5653