Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/components/form-elements/checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {__DEV__, invariant} from '../../utils';
import Text from '../../text/Text';
import {CheckIcon, IndeterminateIcon} from './CheckboxIcon';
import ErrorMessage from '../ErrorMessage';
import {useIsFirstRender} from '../../utils/useIsFirstRender';

type CheckboxColorType = 'dark' | 'light';
type CheckboxLabelSizeType = 'medium' | 'small';
Expand Down Expand Up @@ -184,27 +185,31 @@ const Checkbox = ({
);
const inputRef = React.useRef<HTMLInputElement>(null);
const iconRef = React.useRef<SVGSVGElement | null>(null);
const [isPristine, setIsPristine] = React.useState(true);
const isFirstRender = useIsFirstRender();
const [shouldAnimate, setShouldAnimate] = React.useState(false);

React.useEffect(() => {
if (inputRef.current) inputRef.current.indeterminate = indeterminate;
}, [inputRef, indeterminate]);
React.useEffect(() => {
if (isControlled && checked !== isChecked) {
setIsChecked(checked);
if (isPristine) setIsPristine(false);
if (!isFirstRender && !shouldAnimate) setShouldAnimate(true);
}
}, [checked, isControlled, isChecked, isPristine]);
}, [checked, isControlled, isChecked, isFirstRender, shouldAnimate]);
const onInputChange = React.useCallback(
e => {
if (!isControlled) {
setIsChecked(val => !val);
if (isPristine) setIsPristine(false);
}

if (onChange) onChange(e);

if (!shouldAnimate) {
setShouldAnimate(true);
}
},
[onChange, isControlled, isPristine]
[onChange, isControlled, shouldAnimate]
);

if (__DEV__) {
Expand All @@ -231,7 +236,7 @@ const Checkbox = ({
[`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
'sg-checkbox__icon--with-animation': shouldAnimate, // Apply animation only when checkbox is not pristine
});
const errorTextId = `${checkboxId}-errorText`;
const descriptionId = `${checkboxId}-description`;
Expand Down
33 changes: 33 additions & 0 deletions src/components/form-elements/radio/Radio.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Flex from '../../flex/Flex';
import Box from '../../box/Box';
import Headline from '../../text/Headline';
import Text from '../../text/Text';
import Button from '../../buttons/Button';

import Radio from './Radio';
import RadioGroup from './RadioGroup';
Expand Down Expand Up @@ -381,6 +382,38 @@ In the following example, className `sg-radio--custom-theme` was applied to all
</Story>
</Canvas>

<Canvas>
<Story
name="toggle without radio group"
height="120px"
args={{name: 'optionA', value: 'option-a'}}
>
{args => {
const [value, setValue] = React.useState('option-a');
return (
<div>
<div>
<Button
onClick={() =>
setValue(value === 'option-a' ? 'option-b' : 'option-a')
}
size="s"
>
click to toggle active radio
</Button>
</div>
<Radio value="option-a" checked={value === 'option-a'}>
Option A
</Radio>
<Radio value="option-b" checked={value === 'option-b'}>
Option B
</Radio>
</div>
);
}}
</Story>
</Canvas>

## Accessibility

<RadioA11y />
46 changes: 32 additions & 14 deletions src/components/form-elements/radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import classNames from 'classnames';
import Text from '../../text/Text';
import generateRandomString from '../../../js/generateRandomString';
import useRadioContext from './useRadioContext';
import {useIsFirstRender} from '../../utils/useIsFirstRender';

export type RadioColorType = 'light' | 'dark';
type RadioLabelSizeType = 'medium' | 'small';
Expand Down Expand Up @@ -167,20 +168,35 @@ const Radio = ({
const isWithinRadioGroup = Boolean(
radioGroupContext && Object.keys(radioGroupContext).length
);
const [isPristine, setIsPristine] = React.useState(true);
const shouldAnimate =
(isWithinRadioGroup && !radioGroupContext.isPristine) || !isPristine;
const isFirstRender = useIsFirstRender();
const [shouldAnimate, setShouldAnimate] = React.useState(false);
const isControlled = checked !== undefined || isWithinRadioGroup;
let isChecked: boolean | undefined = undefined;
const [isChecked, setIsChecked] = React.useState<boolean>();

if (isControlled) {
// Radio can either be directly set as checked, or be controlled by a RadioGroup
isChecked =
checked !== undefined
? checked
: Boolean(radioGroupContext.selectedValue) &&
radioGroupContext.selectedValue === value;
}
React.useEffect(() => {
if (isControlled) {
// Radio can either be directly set as checked, or be controlled by a RadioGroup
const newIsChecked =
checked !== undefined
? checked
: Boolean(radioGroupContext.selectedValue) &&
radioGroupContext.selectedValue === value;

setIsChecked(newIsChecked);

if (!isFirstRender && !shouldAnimate && newIsChecked !== isChecked) {
setShouldAnimate(true);
}
}
}, [
isChecked,
checked,
isControlled,
value,
radioGroupContext.selectedValue,
isFirstRender,
shouldAnimate,
]);

const colorName = radioGroupContext.color || color;
const isDisabled =
Expand Down Expand Up @@ -213,13 +229,15 @@ const Radio = ({
if (isWithinRadioGroup) {
radioGroupContext.setLastFocusedValue(value);
radioGroupContext.setSelectedValue(e, value);
} else {
setIsPristine(false);
}

if (onChange) {
onChange(e);
}

if (!shouldAnimate) {
setShouldAnimate(true);
}
};

return (
Expand Down
23 changes: 13 additions & 10 deletions src/components/utils/useIsFirstRender.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import {useRef} from 'react';
import React from 'react';

const useIsFirstRender = () => {
const isFirstRender = useRef(true);
export const useIsFirstRender = () => {
const [isFirstRender, setIsFirstRender] = React.useState(true);

if (isFirstRender.current === true) {
isFirstRender.current = false;
return true;
}
React.useLayoutEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @clxandstuff , we need isomorphic version here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const raf = window.requestAnimationFrame(() => {
setIsFirstRender(false);
});

return isFirstRender.current;
};
return () => {
window.cancelAnimationFrame(raf);
};
}, []);

export default useIsFirstRender;
return isFirstRender;
};