diff --git a/package.json b/package.json index 52d0041e5d..f16dab6dce 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "meow": "^5.0.0", "patch-package": "^6.5.1", "path": "^0.12.7", - "postinstall-postinstall": "^2.1.0" + "postinstall-postinstall": "^2.1.0", + "use-isomorphic-layout-effect": "^1.1.2" }, "devDependencies": { "@babel/cli": "^7.8.3", diff --git a/src/components/form-elements/checkbox/Checkbox.spec.tsx b/src/components/form-elements/checkbox/Checkbox.spec.tsx index 9040b6f35c..24209f2bc3 100644 --- a/src/components/form-elements/checkbox/Checkbox.spec.tsx +++ b/src/components/form-elements/checkbox/Checkbox.spec.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Checkbox from './Checkbox'; -import {render} from '@testing-library/react'; +import {render, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {testA11y} from '../../../axe'; @@ -138,7 +138,7 @@ describe('', () => { expect(checkboxInput.checked).toBe(false); }); - it('it does not apply animation unless initial state has changed', () => { + it('it does not apply animation unless initial state has changed after first render of DOM', async () => { const checkbox = renderCheckbox({ defaultChecked: false, children: 'my label', @@ -150,10 +150,16 @@ describe('', () => { expect(checkboxInput.checked).toBe(false); expect(iconWithAnimation.length).toBe(0); - userEvent.click(checkbox.getByLabelText('my label')); - expect(checkboxInput).toEqual(document.activeElement); + setTimeout(() => { + userEvent.click(checkbox.getByLabelText('my label')); + }); + await waitFor(() => { + expect(checkboxInput).toEqual(document.activeElement); + }); expect(checkboxInput.checked).toBe(true); - expect(iconWithAnimation.length).toBe(1); + await waitFor(() => { + expect(iconWithAnimation.length).toBe(1); + }); }); describe('a11y', () => { diff --git a/src/components/form-elements/checkbox/Checkbox.tsx b/src/components/form-elements/checkbox/Checkbox.tsx index 51a122eaf2..e328030e63 100644 --- a/src/components/form-elements/checkbox/Checkbox.tsx +++ b/src/components/form-elements/checkbox/Checkbox.tsx @@ -5,6 +5,7 @@ import {__DEV__, invariant} from '../../utils'; import Text from '../../text/Text'; import {CheckIcon, IndeterminateIcon} from './CheckboxIcon'; import ErrorMessage from '../ErrorMessage'; +import {useFirstPaint} from '../../utils/useFirstPaint'; type CheckboxColorType = 'dark' | 'light'; type CheckboxLabelSizeType = 'medium' | 'small'; @@ -175,6 +176,7 @@ const Checkbox = ({ 'aria-labelledby': ariaLabelledBy, ...props }: CheckboxPropsType) => { + const checkboxIconRef = React.useRef(); const {current: checkboxId} = React.useRef( id === undefined || id === '' ? generateRandomString() : id ); @@ -184,7 +186,11 @@ const Checkbox = ({ ); const inputRef = React.useRef(null); const iconRef = React.useRef(null); - const [isPristine, setIsPristine] = React.useState(true); + const shouldAnimate = React.useRef(false); + + useFirstPaint(() => { + shouldAnimate.current = true; + }); React.useEffect(() => { if (inputRef.current) inputRef.current.indeterminate = indeterminate; @@ -192,19 +198,28 @@ const Checkbox = ({ React.useEffect(() => { if (isControlled && checked !== isChecked) { setIsChecked(checked); - if (isPristine) setIsPristine(false); + if (shouldAnimate.current && checkboxIconRef.current) { + checkboxIconRef.current.classList.add( + 'sg-checkbox__icon--with-animation' + ); + } } - }, [checked, isControlled, isChecked, isPristine]); + }, [checked, isControlled, isChecked]); const onInputChange = React.useCallback( e => { if (!isControlled) { setIsChecked(val => !val); - if (isPristine) setIsPristine(false); } if (onChange) onChange(e); + + if (shouldAnimate.current && checkboxIconRef.current) { + checkboxIconRef.current.classList.add( + 'sg-checkbox__icon--with-animation' + ); + } }, - [onChange, isControlled, isPristine] + [onChange, isControlled, checkboxIconRef] ); if (__DEV__) { @@ -230,9 +245,7 @@ const Checkbox = ({ 'sg-checkbox__label--with-padding-bottom': description || errorMessage, [`sg-checkbox__label--${String(labelSize)}`]: labelSize, }); - const iconClass = classNames('sg-checkbox__icon', { - 'sg-checkbox__icon--with-animation': !isPristine, // Apply animation only when checkbox is not pristine - }); + const iconClass = classNames('sg-checkbox__icon'); const errorTextId = `${checkboxId}-errorText`; const descriptionId = `${checkboxId}-description`; const describedbyIds = React.useMemo(() => { @@ -287,6 +300,7 @@ const Checkbox = ({