diff --git a/.changeset/hip-buses-peel.md b/.changeset/hip-buses-peel.md new file mode 100644 index 00000000000..8f801600bcd --- /dev/null +++ b/.changeset/hip-buses-peel.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Adds CheckboxGroup and RadioGroup components to replace the ChoiceFieldset component diff --git a/docs/content/CheckboxGroup.mdx b/docs/content/CheckboxGroup.mdx new file mode 100644 index 00000000000..61198cd488c --- /dev/null +++ b/docs/content/CheckboxGroup.mdx @@ -0,0 +1,317 @@ +--- +title: CheckboxGroup +description: A `CheckboxGroup` is used to render a set of checkboxes to let users select one or more options +status: Alpha +source: https://github.com/primer/react/blob/main/src/CheckboxGroup/CheckboxGroup.tsx +storybook: '/react/storybook/?path=/story/forms-checkboxgroup-examples--basic' +--- + +import {CheckboxGroup, Checkbox, Box} from '@primer/components' +import {CheckIcon, XIcon, AlertIcon} from '@primer/octicons-react' +import {ComponentChecklist} from '../src/component-checklist' + +## Examples + +### Basic + +```jsx live + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + +``` + +### Using onChange handlers + +```javascript live noinline +const WithOnChangeHandlers = () => { + const [selectedCheckboxValues, setSelectedCheckboxValues] = React.useState(['one', 'two']) + const [lastSelectedCheckboxValue, setLastSelectedCheckboxValue] = React.useState() + + const handleCheckboxGroupChange = (selectedValues, e) => { + setSelectedCheckboxValues(selectedValues) + setLastSelectedCheckboxValue(e.currentTarget.value) + } + + const handleChoiceOneChange = e => { + alert('Choice one has its own handler') + } + + return ( + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + + {Boolean(selectedCheckboxValues.length) && ( +
The selected checkbox values are {selectedCheckboxValues.join(', ')}
+ )} + {Boolean(lastSelectedCheckboxValue) &&
The last affected checkbox value is {lastSelectedCheckboxValue}
} +
+ ) +} + +render() +``` + +### Disabled + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### Required + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### With validation + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + Your choices are wrong + +``` + +### With caption + +```jsx live + + Choices + You can pick any or all of these choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### A visually hidden label + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### With an external label + +```jsx live +<> + + Choices + + + + + Choice one + + + + Choice two + + + + Choice three + + + +``` + +## Props + +### CheckboxGroup + + + + + + + The unique identifier for this input group. Used to associate the label, validation text, and caption text.{' '} +
You may want a custom ID to make it easier to select elements in integration tests. + + } + /> + + + +
+ +### CheckboxGroup.Label + +A title for the set of choices. If a `CheckboxGroup.Label` is not passed as a child, you must pass the external title's ID to the `aria-describedby` prop on `CheckboxGroup` + + + + + + +### CheckboxGroup.Description + + + + + + +### CheckboxGroup.Validation + +If the user's selection has been flagged during validation, `CheckboxGroup.Validation` may be used to render contextual validation information to help the user complete their task + + + + + + + +## Status + + diff --git a/docs/content/ChoiceFieldset.mdx b/docs/content/ChoiceFieldset.mdx index fa2e9755183..549f8f77795 100644 --- a/docs/content/ChoiceFieldset.mdx +++ b/docs/content/ChoiceFieldset.mdx @@ -1,6 +1,6 @@ --- title: ChoiceFieldset -status: Alpha +status: Deprecated source: https://github.com/primer/react/blob/main/src/ChoiceFieldset/ChoiceFieldset.tsx storybook: '/react/storybook/?path=/story/forms-choicefieldset--radio-group' --- @@ -11,6 +11,10 @@ import {ComponentChecklist} from '../src/component-checklist' A `ChoiceFieldset` is a controlled component that is used to render a related set of checkbox or radio inputs. +## Deprecation + +Use [CheckboxGroup](/CheckboxGroup) or [RadioGroup](/RadioGroup) instead. + ## Examples ### Basic diff --git a/docs/content/FormControl.mdx b/docs/content/FormControl.mdx index 28660dde034..7be8334832a 100644 --- a/docs/content/FormControl.mdx +++ b/docs/content/FormControl.mdx @@ -7,7 +7,18 @@ source: https://github.com/primer/react/blob/main/src/FormControl/FormControl.ts storybook: '/react/storybook?path=/story/forms-inputfield--text-input-field' --- -import {FormControl, TextInputWithTokens, Autocomplete, Select, Textarea, Checkbox, Radio, Text} from '@primer/react' +import { + FormControl, + TextInputWithTokens, + Autocomplete, + Select, + Textarea, + Checkbox, + CheckboxGroup, + Radio, + RadioGroup, + Text +} from '@primer/react' import {MarkGithubIcon} from '@primer/octicons-react' ## Examples @@ -83,27 +94,38 @@ render(DifferentInputs) ### With checkbox and radio inputs ```jsx live - -
+ + + Checkboxes + + + Checkbox one + - Checkbox option one - + + Checkbox two - Checkbox option two - + + Checkbox three -
-
+ + + + Radios - Radio option one - + + Radio one - Radio option two - + + Radio two -
+ + + Radio three + +
``` @@ -280,8 +302,8 @@ A `FormControl.Label` must be passed for the field to be accessible to assistive diff --git a/docs/content/RadioGroup.mdx b/docs/content/RadioGroup.mdx new file mode 100644 index 00000000000..e81af8f35df --- /dev/null +++ b/docs/content/RadioGroup.mdx @@ -0,0 +1,316 @@ +--- +title: RadioGroup +description: A `RadioGroup` is used to render a set of radio inputs to let users select a single option +status: Alpha +source: https://github.com/primer/react/blob/main/src/RadioGroup/RadioGroup.tsx +storybook: '/react/storybook/?path=/story/forms-radiogroup-examples--basic' +--- + +import {RadioGroup, Radio, Box} from '@primer/components' +import {CheckIcon, XIcon, AlertIcon} from '@primer/octicons-react' +import {ComponentChecklist} from '../src/component-checklist' + +## Examples + +### Basic + +```jsx live + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + +``` + +### Using onChange handlers + +```javascript live noinline +const WithOnChangeHandlers = () => { + const [selectedRadioValue, setSelectedCheckboxValues] = React.useState('two') + const [selectedRadioId, setSelectedRadioId] = React.useState() + + const handleRadioGroupChange = (selectedValue, e) => { + setSelectedCheckboxValues(selectedValue) + setSelectedRadioId(e.currentTarget.id) + } + + const handleChoiceOneChange = e => { + alert('Choice one has its own handler') + } + + return ( + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + + {selectedRadioValue &&
The selected radio value is {selectedRadioValue}
} + {selectedRadioId &&
The last selected radio ID is {selectedRadioId}
} +
+ ) +} + +render() +``` + +### Disabled + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### Required + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### With validation + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + Your choices are wrong + +``` + +### With caption + +```jsx live + + Choices + You can pick any of these choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### A visually hidden label + +```jsx live + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + +``` + +### With an external label + +```jsx live +<> + + Choices + + + + + Choice one + + + + Choice two + + + + Choice three + + + +``` + +## Props + +### RadioGroup + + + + + + + The unique identifier for this input group. Used to associate the label, validation text, and caption text.{' '} +
You may want a custom ID to make it easier to select elements in integration tests. + + } + /> + + + + +
+ +### RadioGroup.Label + +A title for the set of choices. If a `RadioGroup.Label` is not passed as a child, you must pass the external title's ID to the `aria-describedby` prop on `RadioGroup` + + + + + + +### RadioGroup.Description + + + + + + +### RadioGroup.Validation + +If the user's selection has been flagged during validation, `RadioGroup.Validation` may be used to render contextual validation information to help the user complete their task + + + + + + + +## Status + + diff --git a/docs/src/@primer/gatsby-theme-doctocat/nav.yml b/docs/src/@primer/gatsby-theme-doctocat/nav.yml index 9584bf38cf0..c73fc657523 100644 --- a/docs/src/@primer/gatsby-theme-doctocat/nav.yml +++ b/docs/src/@primer/gatsby-theme-doctocat/nav.yml @@ -53,10 +53,8 @@ url: /Buttons - title: Checkbox url: /Checkbox - # Temporarily hiding ChoiceFieldset because the API - # is being refactored with breaking changes - # - title: ChoiceFieldset - # url: /ChoiceFieldset + - title: CheckboxGroup + url: /CheckboxGroup - title: CircleBadge url: /CircleBadge - title: CircleOcticon @@ -99,6 +97,8 @@ url: /ProgressBar - title: Radio url: /Radio + - title: RadioGroup + url: /RadioGroup - title: Select url: /Select - title: SelectPanel diff --git a/src/Checkbox.tsx b/src/Checkbox.tsx index 22b219b4fb7..d7aeb16090a 100644 --- a/src/Checkbox.tsx +++ b/src/Checkbox.tsx @@ -1,8 +1,9 @@ import styled from 'styled-components' import {useProvidedRefOrCreate} from './hooks' -import React, {InputHTMLAttributes, ReactElement, useLayoutEffect} from 'react' +import React, {ChangeEventHandler, InputHTMLAttributes, ReactElement, useContext, useLayoutEffect} from 'react' import sx, {SxProp} from './sx' import {FormValidationStatus} from './utils/types/FormValidationStatus' +import {CheckboxGroupContext} from './CheckboxGroup' export type CheckboxProps = { /** @@ -21,12 +22,16 @@ export type CheckboxProps = { * Indicates whether the checkbox must be checked */ required?: boolean - /** * Indicates whether the checkbox validation state */ validationStatus?: FormValidationStatus -} & InputHTMLAttributes & + /** + * A unique value that is never shown to the user. + * Used during form submission and to identify which checkbox inputs are selected + */ + value: string +} & Exclude, 'value'> & SxProp const StyledCheckbox = styled.input` @@ -42,10 +47,15 @@ const StyledCheckbox = styled.input` */ const Checkbox = React.forwardRef( ( - {checked, indeterminate, disabled, sx: sxProp, required, validationStatus, ...rest}: CheckboxProps, + {checked, indeterminate, disabled, onChange, sx: sxProp, required, validationStatus, value, ...rest}: CheckboxProps, ref ): ReactElement => { const checkboxRef = useProvidedRefOrCreate(ref as React.RefObject) + const checkboxGroupContext = useContext(CheckboxGroupContext) + const handleOnChange: ChangeEventHandler = e => { + checkboxGroupContext.onChange && checkboxGroupContext.onChange(e) + onChange && onChange(e) + } useLayoutEffect(() => { if (checkboxRef.current) { @@ -65,6 +75,9 @@ const Checkbox = React.forwardRef( required={required} aria-required={required ? 'true' : 'false'} aria-invalid={validationStatus === 'error' ? 'true' : 'false'} + onChange={handleOnChange} + value={value} + name={value} {...rest} /> ) diff --git a/src/CheckboxGroup.tsx b/src/CheckboxGroup.tsx new file mode 100644 index 00000000000..5e36ded7262 --- /dev/null +++ b/src/CheckboxGroup.tsx @@ -0,0 +1,76 @@ +import React, {ChangeEvent, ChangeEventHandler, createContext, FC} from 'react' +import CheckboxOrRadioGroup, {CheckboxOrRadioGroupProps} from './_CheckboxOrRadioGroup' +import CheckboxOrRadioGroupCaption from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption' +import CheckboxOrRadioGroupLabel from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel' +import CheckboxOrRadioGroupValidation from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation' +import {useRenderForcingRef} from './hooks' +import {SxProp} from './sx' +import {Checkbox, FormControl} from '.' + +type CheckboxGroupProps = { + /** + * An onChange handler that gets called when any of the checkboxes change + */ + onChange?: (selected: string[], e?: ChangeEvent) => void +} & CheckboxOrRadioGroupProps & + SxProp + +export const CheckboxGroupContext = createContext<{ + disabled?: boolean + onChange?: ChangeEventHandler +}>({}) + +const CheckboxGroup: FC = ({children, disabled, onChange, ...rest}) => { + const formControlComponentChildren = React.Children.toArray(children) + .filter(child => React.isValidElement(child) && child.type === FormControl) + .map(formControlComponent => + React.isValidElement(formControlComponent) ? formControlComponent.props.children : [] + ) + .flat() + + const checkedCheckboxes = React.Children.toArray(formControlComponentChildren) + .filter(child => React.isValidElement(child) && child.type === Checkbox) + .map( + checkbox => + React.isValidElement(checkbox) && + (checkbox.props.checked || checkbox.props.defaultChecked) && + checkbox.props.value + ) + .filter(Boolean) + const [selectedCheckboxValues, setSelectedCheckboxValues] = useRenderForcingRef(checkedCheckboxes) + + const updateSelectedCheckboxes: ChangeEventHandler = e => { + const {value, checked} = e.currentTarget + + if (checked) { + setSelectedCheckboxValues([...(selectedCheckboxValues.current || []), value]) + return + } + + setSelectedCheckboxValues((selectedCheckboxValues.current || []).filter(selectedValue => selectedValue !== value)) + } + + return ( + { + if (onChange) { + updateSelectedCheckboxes(e) + onChange(selectedCheckboxValues.current || [], e) + } + } + }} + > + + {children} + + + ) +} + +export default Object.assign(CheckboxGroup, { + Caption: CheckboxOrRadioGroupCaption, + Label: CheckboxOrRadioGroupLabel, + Validation: CheckboxOrRadioGroupValidation +}) diff --git a/src/ChoiceFieldset/ChoiceFieldset.tsx b/src/ChoiceFieldset/ChoiceFieldset.tsx index 19597e9daca..cd97ded0d37 100644 --- a/src/ChoiceFieldset/ChoiceFieldset.tsx +++ b/src/ChoiceFieldset/ChoiceFieldset.tsx @@ -1,4 +1,4 @@ -import React, {ComponentProps} from 'react' +import React from 'react' import {Box, useSSRSafeId} from '..' import createSlots from '../utils/create-slots' import {FormValidationStatus} from '../utils/types/FormValidationStatus' @@ -56,6 +56,9 @@ export interface ChoiceFieldsetContext extends ChoiceFieldsetProps { const {Slots, Slot} = createSlots(['Description', 'ChoiceList', 'Legend', 'Validation']) export {Slot} +/** + * @deprecated Use `CheckboxGroup` or `RadioGroup` instead. + */ const ChoiceFieldset = >({ children, disabled, @@ -124,7 +127,6 @@ const ChoiceFieldset = >({ ) } -export type InputFieldComponentProps = ComponentProps export type {ChoiceFieldsetListProps} from './ChoiceFieldsetList' export type {ChoiceFieldsetLegendProps} from './ChoiceFieldsetLegend' export type {ChoiceFieldProps} from './ChoiceFieldsetListItem' diff --git a/src/FormControl/FormControl.tsx b/src/FormControl/FormControl.tsx index 0091fc65a16..c1ac5ea74f8 100644 --- a/src/FormControl/FormControl.tsx +++ b/src/FormControl/FormControl.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, {useContext} from 'react' import {Autocomplete, Box, Checkbox, Radio, Select, Textarea, TextInput, TextInputWithTokens, useSSRSafeId} from '..' import FormControlCaption from './_FormControlCaption' import FormControlLabel from './_FormControlLabel' @@ -8,6 +8,7 @@ import ValidationAnimationContainer from '../_ValidationAnimationContainer' import {get} from '../constants' import FormControlLeadingVisual from './_FormControlLeadingVisual' import {SxProp} from '../sx' +import CheckboxOrRadioGroupContext from '../_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext' export type FormControlProps = { children?: React.ReactNode @@ -30,25 +31,27 @@ export interface FormControlContext extends Pick { +const FormControl = ({children, disabled: disabledProp, id: idProp, required, sx}: FormControlProps) => { const expectedInputComponents = [Autocomplete, Checkbox, Radio, Select, TextInput, TextInputWithTokens, Textarea] + const choiceGroupContext = useContext(CheckboxOrRadioGroupContext) + const disabled = choiceGroupContext?.disabled || disabledProp const id = useSSRSafeId(idProp) const validationChild = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === FormControlValidation ? child : null ) - const captionChildren: React.ReactElement[] | undefined | null = React.Children.map(children, child => + const captionChild = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === FormControlCaption ? child : null - )?.filter(Boolean) - const labelChild: React.ReactNode | undefined | null = React.Children.toArray(children).find( + ) + const labelChild = React.Children.toArray(children).find( child => React.isValidElement(child) && child.type === FormControlLabel ) - const validationMessageId = validationChild ? `${id}-validationMsg` : '' - const validationStatus = React.isValidElement(validationChild) ? validationChild.props.variant : undefined - const captionId = captionChildren?.length ? `${id}-caption` : undefined + const validationMessageId = validationChild && `${id}-validationMessage` + const captionId = captionChild && `${id}-caption` + const validationStatus = React.isValidElement(validationChild) && validationChild.props.variant const InputComponent = React.Children.toArray(children).find(child => expectedInputComponents.some(inputComponent => React.isValidElement(child) && child.type === inputComponent) ) - const inputProps = React.isValidElement(InputComponent) ? InputComponent.props : undefined + const inputProps = React.isValidElement(InputComponent) && InputComponent.props const isChoiceInput = React.isValidElement(InputComponent) && (InputComponent.type === Checkbox || InputComponent.type === Radio) diff --git a/src/Radio.tsx b/src/Radio.tsx index f9c36c42b91..64616927adc 100644 --- a/src/Radio.tsx +++ b/src/Radio.tsx @@ -1,7 +1,8 @@ import styled from 'styled-components' -import React, {InputHTMLAttributes, ReactElement} from 'react' +import React, {ChangeEventHandler, InputHTMLAttributes, ReactElement, useContext} from 'react' import sx, {SxProp} from './sx' import {FormValidationStatus} from './utils/types/FormValidationStatus' +import {RadioGroupContext} from './RadioGroup' export type RadioProps = { /** @@ -12,7 +13,7 @@ export type RadioProps = { /** * Name attribute of the input element. Required for grouping radio inputs */ - name: string + name?: string /** * Apply inactive visual appearance to the radio button */ @@ -49,9 +50,23 @@ const StyledRadio = styled.input` */ const Radio = React.forwardRef( ( - {checked, disabled, sx: sxProp, required, validationStatus, value, name, ...rest}: RadioProps, + {checked, disabled, name: nameProp, onChange, sx: sxProp, required, validationStatus, value, ...rest}: RadioProps, ref ): ReactElement => { + const radioGroupContext = useContext(RadioGroupContext) + const handleOnChange: ChangeEventHandler = e => { + radioGroupContext?.onChange && radioGroupContext.onChange(e) + onChange && onChange(e) + } + const name = nameProp || radioGroupContext?.name + + if (!name) { + // eslint-disable-next-line no-console + console.warn( + 'A radio input must have a `name` attribute. Pass `name` as a prop directly to each Radio, or nest them in a `RadioGroup` component with a `name` prop' + ) + } + return ( ( aria-required={required ? 'true' : 'false'} aria-invalid={validationStatus === 'error' ? 'true' : 'false'} sx={sxProp} + onChange={handleOnChange} {...rest} /> ) diff --git a/src/RadioGroup.tsx b/src/RadioGroup.tsx new file mode 100644 index 00000000000..de86c0b1835 --- /dev/null +++ b/src/RadioGroup.tsx @@ -0,0 +1,63 @@ +import React, {ChangeEvent, ChangeEventHandler, createContext, FC} from 'react' +import CheckboxOrRadioGroup, {CheckboxOrRadioGroupProps} from './_CheckboxOrRadioGroup' +import CheckboxOrRadioGroupCaption from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption' +import CheckboxOrRadioGroupLabel from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel' +import CheckboxOrRadioGroupValidation from './_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation' +import {useRenderForcingRef} from './hooks' +import {SxProp} from './sx' + +type RadioGroupProps = { + /** + * An onChange handler that gets called when the selection changes + */ + onChange?: (selected: string | null, e?: ChangeEvent) => void + /** + * The name used to identify this group of radios + */ + name: string +} & CheckboxOrRadioGroupProps & + SxProp + +export const RadioGroupContext = createContext<{ + disabled?: boolean + onChange?: ChangeEventHandler + name: string +} | null>(null) + +const RadioGroup: FC = ({children, disabled, onChange, name, ...rest}) => { + const [selectedRadioValue, setSelectedRadioValue] = useRenderForcingRef(null) + + const updateSelectedCheckboxes: ChangeEventHandler = e => { + const {value, checked} = e.currentTarget + + if (checked) { + setSelectedRadioValue(value) + return + } + } + + return ( + { + if (onChange) { + updateSelectedCheckboxes(e) + onChange(selectedRadioValue.current, e) + } + } + }} + > + + {children} + + + ) +} + +export default Object.assign(RadioGroup, { + Caption: CheckboxOrRadioGroupCaption, + Label: CheckboxOrRadioGroupLabel, + Validation: CheckboxOrRadioGroupValidation +}) diff --git a/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx b/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx new file mode 100644 index 00000000000..fb1c963c236 --- /dev/null +++ b/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx @@ -0,0 +1,185 @@ +import React from 'react' +import {Box, Checkbox, FormControl, Radio, useSSRSafeId} from '..' +import ValidationAnimationContainer from '../_ValidationAnimationContainer' +import CheckboxOrRadioGroupCaption from './_CheckboxOrRadioGroupCaption' +import CheckboxOrRadioGroupLabel from './_CheckboxOrRadioGroupLabel' +import CheckboxOrRadioGroupValidation from './_CheckboxOrRadioGroupValidation' +import {Slots} from './slots' +import styled from 'styled-components' +import {get} from '../constants' +import CheckboxOrRadioGroupContext from './_CheckboxOrRadioGroupContext' +import VisuallyHidden from '../_VisuallyHidden' +import {SxProp} from '../sx' + +export type CheckboxOrRadioGroupProps = { + /** + * Used when associating the input group with a label other than `CheckboxOrRadioGroup.Label` + */ + ['aria-labelledby']?: string + /** + * Whether the input group allows user input + */ + disabled?: boolean + /** + * The unique identifier for this input group. Used to associate the label, validation text, and caption text. + * You may want a custom ID to make it easier to select elements in integration tests. + */ + id?: string + /** + * If true, the user must make a selection before the owning form can be submitted + */ + required?: boolean +} & SxProp + +export type CheckboxOrRadioGroupContext = { + validationMessageId?: string + captionId?: string +} & CheckboxOrRadioGroupProps + +const Body = styled.div` + display: flex; + flex-direction: column; + list-style: none; + margin: 0; + padding: 0; + + > * + * { + margin-top: ${get('space.2')}; + } +` + +const CheckboxOrRadioGroup: React.FC = ({ + 'aria-labelledby': ariaLabelledby, + children, + disabled, + id: idProp, + required, + sx +}) => { + const expectedInputComponents = [Checkbox, Radio] + const labelChild = React.Children.toArray(children).find( + child => React.isValidElement(child) && child.type === CheckboxOrRadioGroupLabel + ) + const validationChild = React.Children.toArray(children).find(child => + React.isValidElement(child) && child.type === CheckboxOrRadioGroupValidation ? child : null + ) + const captionChild = React.Children.toArray(children).find(child => + React.isValidElement(child) && child.type === CheckboxOrRadioGroupCaption ? child : null + ) + const id = useSSRSafeId(idProp) + const validationMessageId = validationChild && `${id}-validationMessage` + const captionId = captionChild && `${id}-caption` + const checkIfOnlyContainsChoiceInputs = () => { + const formControlComponentChildren = React.Children.toArray(children) + .filter(child => React.isValidElement(child) && child.type === FormControl) + .map(formControlComponent => + React.isValidElement(formControlComponent) ? formControlComponent.props.children : [] + ) + .flat() + + return Boolean( + React.Children.toArray(formControlComponentChildren).find(child => + expectedInputComponents.some(inputComponent => React.isValidElement(child) && child.type === inputComponent) + ) + ) + } + + if (!checkIfOnlyContainsChoiceInputs()) { + // eslint-disable-next-line no-console + console.warn('Only `Checkbox` and `Radio` form controls should be used in a `CheckboxOrRadioGroup`.') + } + + if (!labelChild && !ariaLabelledby) { + // eslint-disable-next-line no-console + console.warn( + 'A choice group must be labelled using a `CheckboxOrRadioGroup.Label` child, or by passing `aria-labelledby` to the CheckboxOrRadioGroup component.' + ) + } + + return ( + + {slots => { + const isLegendVisible = React.isValidElement(labelChild) && !labelChild.props.visuallyHidden + + return ( + +
+ + {labelChild ? ( + /* + Placing the caption text and validation text in the provides a better user + experience for more screenreaders. + + Reference: https://blog.tenon.io/accessible-validation-of-checkbox-and-radiobutton-groups/ + */ + + {slots.Label} + {slots.Caption} + {React.isValidElement(slots.Validation) && slots.Validation.props.children && ( + {slots.Validation.props.children} + )} + + ) : ( + /* + If CheckboxOrRadioGroup.Label wasn't passed as a child, we don't render a + but we still want to render a caption + */ + slots.Caption + )} + + + {React.Children.toArray(children).filter(child => React.isValidElement(child))} + + + {validationChild && ( + + aria-hidden={Boolean(labelChild)} + show + > + {slots.Validation} + + )} +
+
+ ) + }} +
+ ) +} + +CheckboxOrRadioGroup.defaultProps = { + disabled: false, + required: false +} + +export type {CheckboxOrRadioGroupLabelProps} from './_CheckboxOrRadioGroupLabel' +export default Object.assign(CheckboxOrRadioGroup, { + Caption: CheckboxOrRadioGroupCaption, + Label: CheckboxOrRadioGroupLabel, + Validation: CheckboxOrRadioGroupValidation +}) diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx new file mode 100644 index 00000000000..3a569d0ccce --- /dev/null +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import {Text} from '..' +import {SxProp} from '../sx' +import {CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' +import {Slot} from './slots' + +const CheckboxOrRadioGroupCaption: React.FC = ({children, sx}) => ( + + {({disabled, captionId}: CheckboxOrRadioGroupContext) => ( + + {children} + + )} + +) + +export default CheckboxOrRadioGroupCaption diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx new file mode 100644 index 00000000000..415efeb281e --- /dev/null +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx @@ -0,0 +1,7 @@ +import {createContext} from 'react' + +const CheckboxOrRadioGroupContext = createContext<{ + disabled?: boolean +} | null>(null) + +export default CheckboxOrRadioGroupContext diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel.tsx new file mode 100644 index 00000000000..7fbc34eca25 --- /dev/null +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import {Box} from '..' +import {SxProp} from '../sx' +import VisuallyHidden from '../_VisuallyHidden' +import {CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' +import {Slot} from './slots' + +export type CheckboxOrRadioGroupLabelProps = { + /** + * Whether to visually hide the fieldset legend + */ + visuallyHidden?: boolean +} & SxProp + +const CheckboxOrRadioGroupLabel: React.FC = ({children, visuallyHidden, sx}) => ( + + {({required, disabled}: CheckboxOrRadioGroupContext) => ( + + {required ? ( + + {children} + * + + ) : ( + children + )} + + )} + +) + +CheckboxOrRadioGroupLabel.defaultProps = { + visuallyHidden: false +} + +export default CheckboxOrRadioGroupLabel diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx new file mode 100644 index 00000000000..67538b52627 --- /dev/null +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import InputValidation from '../_InputValidation' +import {SxProp} from '../sx' +import {FormValidationStatus} from '../utils/types/FormValidationStatus' +import {CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' +import {Slot} from './slots' + +export type CheckboxOrRadioGroupValidationProps = { + /** Changes the visual style to match the validation status */ + variant: FormValidationStatus +} & SxProp + +const CheckboxOrRadioGroupValidation: React.FC = ({children, variant, sx}) => ( + + {({validationMessageId = ''}: CheckboxOrRadioGroupContext) => ( + + {children} + + )} + +) + +export default CheckboxOrRadioGroupValidation diff --git a/src/_CheckboxOrRadioGroup/index.ts b/src/_CheckboxOrRadioGroup/index.ts new file mode 100644 index 00000000000..ceebd3ba524 --- /dev/null +++ b/src/_CheckboxOrRadioGroup/index.ts @@ -0,0 +1,2 @@ +export {default} from './CheckboxOrRadioGroup' +export type {CheckboxOrRadioGroupProps} from './CheckboxOrRadioGroup' diff --git a/src/_CheckboxOrRadioGroup/slots.ts b/src/_CheckboxOrRadioGroup/slots.ts new file mode 100644 index 00000000000..75ba8a8171b --- /dev/null +++ b/src/_CheckboxOrRadioGroup/slots.ts @@ -0,0 +1,3 @@ +import createSlots from '../utils/create-slots' + +export const {Slots, Slot} = createSlots(['Caption', 'Label', 'Validation']) diff --git a/src/_InputLabel.tsx b/src/_InputLabel.tsx index 0d664464429..6118c7c9633 100644 --- a/src/_InputLabel.tsx +++ b/src/_InputLabel.tsx @@ -20,7 +20,7 @@ const InputLabel: React.FC = ({children, disabled, required, vis fontSize: 1, display: 'block', color: disabled ? 'fg.muted' : 'fg.default', - cursor: 'pointer', + cursor: disabled ? 'default' : 'pointer', ...sx }} > diff --git a/src/_InputValidation.tsx b/src/_InputValidation.tsx index 20aa0fd30f6..9b29e04798f 100644 --- a/src/_InputValidation.tsx +++ b/src/_InputValidation.tsx @@ -40,7 +40,7 @@ const InputValidation: React.FC = ({children, id, validationStatus, sx}) }} > {IconComponent && ( - + )} diff --git a/src/_ValidationAnimationContainer.tsx b/src/_ValidationAnimationContainer.tsx index c4206d3658a..591a8c9c745 100644 --- a/src/_ValidationAnimationContainer.tsx +++ b/src/_ValidationAnimationContainer.tsx @@ -1,8 +1,8 @@ -import React, {useEffect, useState} from 'react' +import React, {HTMLProps, useEffect, useState} from 'react' import styled, {keyframes, css} from 'styled-components' import {Box} from '.' -interface Props { +interface Props extends HTMLProps { show?: boolean } diff --git a/src/_VisuallyHidden.tsx b/src/_VisuallyHidden.tsx index 9237f91a086..abb75c85abf 100644 --- a/src/_VisuallyHidden.tsx +++ b/src/_VisuallyHidden.tsx @@ -26,7 +26,7 @@ const VisuallyHidden = styled.span` ` VisuallyHidden.defaultProps = { - isVisible: true + isVisible: false } export default VisuallyHidden diff --git a/src/__tests__/CheckboxGroup.test.tsx b/src/__tests__/CheckboxGroup.test.tsx new file mode 100644 index 00000000000..a7c99427bef --- /dev/null +++ b/src/__tests__/CheckboxGroup.test.tsx @@ -0,0 +1,167 @@ +import React from 'react' +import '@testing-library/jest-dom/extend-expect' +import {render} from '@testing-library/react' +import {Checkbox, CheckboxGroup, FormControl, SSRProvider} from '..' +import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing' +import userEvent from '@testing-library/user-event' +import {CheckboxGroupContext} from '../CheckboxGroup' + +describe('CheckboxGroup', () => { + const mockWarningFn = jest.fn() + + beforeAll(() => { + jest.spyOn(global.console, 'warn').mockImplementation(mockWarningFn) + }) + afterAll(() => { + jest.clearAllMocks() + }) + behavesAsComponent({ + Component: CheckboxGroup, + options: {skipAs: true, skipSx: true}, // skipping sx check because we have to render this in a to keep snapshots consistent + toRender: () => ( + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + + ) + }) + checkExports('CheckboxGroup', { + default: CheckboxGroup, + CheckboxGroupContext + }) + it('renders a disabled group of inputs', () => { + const {getAllByRole, getByRole} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const checkboxInputs = getAllByRole('checkbox') as HTMLInputElement[] + const fieldset = getByRole('group') as HTMLFieldSetElement + + for (const checkboxInput of checkboxInputs) { + expect(checkboxInput.disabled).toBe(true) + } + + expect(fieldset.disabled).toBe(true) + }) + it('renders a required group of inputs', () => { + const {getByTitle} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const requiredIndicator = getByTitle('required field') + + expect(requiredIndicator).toBeInTheDocument() + }) + it('calls onChange handlers passed to CheckboxGroup and Checkbox', () => { + const handleParentChange = jest.fn() + const handleCheckboxChange = jest.fn() + const {getByLabelText} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const checkbox = getByLabelText('Choice one') as HTMLInputElement + + expect(handleParentChange).not.toHaveBeenCalled() + expect(handleCheckboxChange).not.toHaveBeenCalled() + userEvent.click(checkbox) + expect(handleParentChange).toHaveBeenCalled() + expect(handleCheckboxChange).toHaveBeenCalled() + }) + it('calls onChange handler on CheckboxGroup with selected values', () => { + const handleParentChange = jest.fn() + const {getByLabelText} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + + const checkbox = getByLabelText('Choice one') as HTMLInputElement + + expect(handleParentChange).not.toHaveBeenCalled() + userEvent.click(checkbox) + expect(handleParentChange).toHaveBeenCalledWith( + ['two', 'one'], + expect.objectContaining({ + target: expect.objectContaining({ + value: 'one' + }) + }) + ) + userEvent.click(checkbox) + expect(handleParentChange).toHaveBeenCalledWith( + ['two'], + expect.objectContaining({ + target: expect.objectContaining({ + value: 'one' + }) + }) + ) + }) +}) + +checkStoriesForAxeViolations('CheckboxGroup/fixtures') +checkStoriesForAxeViolations('CheckboxGroup/examples') diff --git a/src/__tests__/CheckboxOrRadioGroup.test.tsx b/src/__tests__/CheckboxOrRadioGroup.test.tsx new file mode 100644 index 00000000000..59e91a901dd --- /dev/null +++ b/src/__tests__/CheckboxOrRadioGroup.test.tsx @@ -0,0 +1,207 @@ +import React from 'react' +import '@testing-library/jest-dom/extend-expect' +import {render, within} from '@testing-library/react' +import {Checkbox, FormControl, Radio, SSRProvider, TextInput} from '..' +import {behavesAsComponent, checkExports} from '../utils/testing' +import CheckboxOrRadioGroup from '../_CheckboxOrRadioGroup' + +const INPUT_GROUP_LABEL = 'Choices' + +describe('CheckboxOrRadioGroup', () => { + const mockWarningFn = jest.fn() + + beforeAll(() => { + jest.spyOn(global.console, 'warn').mockImplementation(mockWarningFn) + }) + afterAll(() => { + jest.clearAllMocks() + }) + behavesAsComponent({ + Component: CheckboxOrRadioGroup, + options: {skipAs: true, skipSx: true}, // skipping sx check because we have to render this in a to keep snapshots consistent + toRender: () => ( + + + {INPUT_GROUP_LABEL} + + + Choice one + + + + Choice two + + + + Choice three + + + + ) + }) + checkExports('_CheckboxOrRadioGroup', { + default: CheckboxOrRadioGroup + }) + it('renders a group of inputs with a caption in the ', () => { + render( + + {INPUT_GROUP_LABEL} + Caption text + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const legend = document.getElementsByTagName('legend')[0] + const caption = within(legend).getByText('Caption text') + + expect(caption).toBeInTheDocument() + }) + it('renders a group of inputs with a validation message in the ', () => { + render( + + {INPUT_GROUP_LABEL} + Caption text + + + Choice one + + + + Choice two + + + + Choice three + + Validation text + + ) + const legend = document.getElementsByTagName('legend')[0] + const validationMsg = within(legend).getByText('Validation text') + + expect(validationMsg).toBeInTheDocument() + }) + it('renders with a hidden label', () => { + const {getByText} = render( + + {INPUT_GROUP_LABEL} + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const legend = getByText(INPUT_GROUP_LABEL) + + expect(legend).toBeInTheDocument() + }) + it('uses a legend to label the input group', () => { + const {getByRole} = render( + + {INPUT_GROUP_LABEL} + + + Choice one + + + + Choice two + + + + Choice three + + + ) + + expect(getByRole('group', {name: INPUT_GROUP_LABEL})).toBeTruthy() + }) + it('associates a label with the input group when the label is not a child of CheckboxOrRadioGroup', () => { + const INPUT_GROUP_LABEL_ID = 'the-label' + const {getByLabelText} = render( + <> +

{INPUT_GROUP_LABEL}

+ + + + Choice one + + + + Choice two + + + + Choice three + + + + ) + const fieldset = getByLabelText(INPUT_GROUP_LABEL) + + expect(fieldset).toBeInTheDocument() + }) + it('logs a warning when trying to render a group without a label', () => { + const consoleSpy = jest.spyOn(global.console, 'warn') + + render( + + + + Choice one + + + + Choice two + + + + Choice three + + + ) + + expect(consoleSpy).toHaveBeenCalled() + }) + it('logs a warning when trying to render an input component other than Radio or Checkbox', () => { + const consoleSpy = jest.spyOn(global.console, 'warn') + + render( + + {INPUT_GROUP_LABEL} + + Choice one + + + + + Choice two + + + + Choice three + + + ) + + expect(consoleSpy).toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/FormControl.test.tsx b/src/__tests__/FormControl.test.tsx index 34786c56fa0..34724e7c730 100644 --- a/src/__tests__/FormControl.test.tsx +++ b/src/__tests__/FormControl.test.tsx @@ -251,8 +251,8 @@ describe('FormControl', () => { const inputNode = getByLabelText(LABEL_TEXT) const validationNode = getByText(ERROR_TEXT) - expect(validationNode.getAttribute('id')).toBe(`${fieldId}-validationMsg`) - expect(inputNode.getAttribute('aria-describedby')).toBe(`${fieldId}-validationMsg`) + expect(validationNode.getAttribute('id')).toBe(`${fieldId}-validationMessage`) + expect(inputNode.getAttribute('aria-describedby')).toBe(`${fieldId}-validationMessage`) }) }) diff --git a/src/__tests__/RadioGroup.test.tsx b/src/__tests__/RadioGroup.test.tsx new file mode 100644 index 00000000000..037a0d0369e --- /dev/null +++ b/src/__tests__/RadioGroup.test.tsx @@ -0,0 +1,158 @@ +import React from 'react' +import '@testing-library/jest-dom/extend-expect' +import {render} from '@testing-library/react' +import {RadioGroup, FormControl, Radio, SSRProvider} from '..' +import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing' +import userEvent from '@testing-library/user-event' +import {RadioGroupContext} from '../RadioGroup' + +describe('RadioGroup', () => { + const mockWarningFn = jest.fn() + + beforeAll(() => { + jest.spyOn(global.console, 'warn').mockImplementation(mockWarningFn) + }) + afterAll(() => { + jest.clearAllMocks() + }) + behavesAsComponent({ + Component: RadioGroup, + options: {skipAs: true, skipSx: true}, // skipping sx check because we have to render this in a to keep snapshots consistent + toRender: () => ( + + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + + ) + }) + checkExports('RadioGroup', { + default: RadioGroup, + RadioGroupContext + }) + it('renders a disabled group of inputs', () => { + const {getAllByRole, getByRole} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const radioInputs = getAllByRole('radio') as HTMLInputElement[] + const fieldset = getByRole('group') as HTMLFieldSetElement + + for (const radioInput of radioInputs) { + expect(radioInput.disabled).toBe(true) + } + + expect(fieldset.disabled).toBe(true) + }) + it('renders a required group of inputs', () => { + const {getByTitle} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const requiredIndicator = getByTitle('required field') + + expect(requiredIndicator).toBeInTheDocument() + }) + it('calls onChange handlers passed to RadioGroup and Radio', () => { + const handleParentChange = jest.fn() + const handleRadioChange = jest.fn() + const {getByLabelText} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + const checkbox = getByLabelText('Choice one') as HTMLInputElement + + expect(handleParentChange).not.toHaveBeenCalled() + expect(handleRadioChange).not.toHaveBeenCalled() + userEvent.click(checkbox) + expect(handleParentChange).toHaveBeenCalled() + expect(handleRadioChange).toHaveBeenCalled() + }) + it('calls onChange handler on RadioGroup with selected value', () => { + const handleParentChange = jest.fn() + const {getByLabelText} = render( + + Choices + + + Choice one + + + + Choice two + + + + Choice three + + + ) + + const checkbox = getByLabelText('Choice one') as HTMLInputElement + + expect(handleParentChange).not.toHaveBeenCalled() + userEvent.click(checkbox) + expect(handleParentChange).toHaveBeenCalledWith( + 'one', + expect.objectContaining({ + target: expect.objectContaining({ + value: 'one' + }) + }) + ) + }) +}) + +checkStoriesForAxeViolations('RadioGroup/fixtures') +checkStoriesForAxeViolations('RadioGroup/examples') diff --git a/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap b/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap index af9bb835cd8..ed74f706514 100644 --- a/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap +++ b/src/__tests__/__snapshots__/Autocomplete.test.tsx.snap @@ -442,7 +442,20 @@ Array [ type="text" /> , - .c2 { + .c0 { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + -webkit-clip: rect(0,0,0,0); + clip: rect(0,0,0,0); + white-space: nowrap; + border-width: 0; +} + +.c2 { margin-top: 8px; margin-bottom: 8px; } @@ -618,19 +631,6 @@ Array [ --item-hover-divider-border-color-override: hsla(210,18%,87%,1); } -.c0 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - -webkit-clip: rect(0,0,0,0); - clip: rect(0,0,0,0); - white-space: nowrap; - border-width: 0; -} - @media (hover:hover) and (pointer:fine) { .c4:hover { background: var( --item-hover-bg-override,rgba(208,215,222,0.32) ); @@ -1386,7 +1386,20 @@ Array [ type="text" /> , - .c2 { + .c0 { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + -webkit-clip: rect(0,0,0,0); + clip: rect(0,0,0,0); + white-space: nowrap; + border-width: 0; +} + +.c2 { margin-top: 8px; margin-bottom: 8px; } @@ -1525,19 +1538,6 @@ Array [ --item-hover-divider-border-color-override: hsla(210,18%,87%,1); } -.c0 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - -webkit-clip: rect(0,0,0,0); - clip: rect(0,0,0,0); - white-space: nowrap; - border-width: 0; -} - @media (hover:hover) and (pointer:fine) { .c4:hover { background: var( --item-hover-bg-override,rgba(208,215,222,0.32) ); @@ -2240,7 +2240,20 @@ Array [ type="text" /> , - .c2 { + .c0 { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + -webkit-clip: rect(0,0,0,0); + clip: rect(0,0,0,0); + white-space: nowrap; + border-width: 0; +} + +.c2 { margin-top: 8px; margin-bottom: 8px; } @@ -2390,19 +2403,6 @@ Array [ --item-hover-divider-border-color-override: hsla(210,18%,87%,1); } -.c0 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - -webkit-clip: rect(0,0,0,0); - clip: rect(0,0,0,0); - white-space: nowrap; - border-width: 0; -} - @media (hover:hover) and (pointer:fine) { .c4:hover { background: var( --item-hover-bg-override,rgba(208,215,222,0.32) ); @@ -3105,7 +3105,20 @@ Array [ type="text" /> , - .c2 { + .c0 { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + -webkit-clip: rect(0,0,0,0); + clip: rect(0,0,0,0); + white-space: nowrap; + border-width: 0; +} + +.c2 { margin-top: 8px; margin-bottom: 8px; } @@ -3212,19 +3225,6 @@ Array [ --item-hover-divider-border-color-override: hsla(210,18%,87%,1); } -.c0 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - -webkit-clip: rect(0,0,0,0); - clip: rect(0,0,0,0); - white-space: nowrap; - border-width: 0; -} - @media (hover:hover) and (pointer:fine) { .c4:hover { background: var( --item-hover-bg-override,rgba(208,215,222,0.32) ); @@ -3548,7 +3548,20 @@ Array [ id="customInput" type="text" />, - .c2 { + .c0 { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + -webkit-clip: rect(0,0,0,0); + clip: rect(0,0,0,0); + white-space: nowrap; + border-width: 0; +} + +.c2 { margin-top: 8px; margin-bottom: 8px; } @@ -3655,19 +3668,6 @@ Array [ --item-hover-divider-border-color-override: hsla(210,18%,87%,1); } -.c0 { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - -webkit-clip: rect(0,0,0,0); - clip: rect(0,0,0,0); - white-space: nowrap; - border-width: 0; -} - @media (hover:hover) and (pointer:fine) { .c4:hover { background: var( --item-hover-bg-override,rgba(208,215,222,0.32) ); diff --git a/src/__tests__/__snapshots__/Checkbox.test.tsx.snap b/src/__tests__/__snapshots__/Checkbox.test.tsx.snap index 91b973a8e8e..1f1f73e7285 100644 --- a/src/__tests__/__snapshots__/Checkbox.test.tsx.snap +++ b/src/__tests__/__snapshots__/Checkbox.test.tsx.snap @@ -11,6 +11,7 @@ exports[`Checkbox renders consistently 1`] = ` aria-invalid="false" aria-required="false" className="c0" + onChange={[Function]} type="checkbox" /> `; diff --git a/src/__tests__/__snapshots__/CheckboxGroup.test.tsx.snap b/src/__tests__/__snapshots__/CheckboxGroup.test.tsx.snap new file mode 100644 index 00000000000..b75d144d4f4 --- /dev/null +++ b/src/__tests__/__snapshots__/CheckboxGroup.test.tsx.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheckboxGroup renders consistently 1`] = ` +.c0 { + margin: 0; + padding: 0; + border: none; +} + +.c1 { + margin-bottom: 8px; + padding: 0; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c4 > input { + margin-left: 0; + margin-right: 0; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + list-style: none; + margin: 0; + padding: 0; +} + +.c2 > * + * { + margin-top: 8px; +} + +.c5 { + cursor: pointer; +} + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; diff --git a/src/__tests__/__snapshots__/CheckboxOrRadioGroup.test.tsx.snap b/src/__tests__/__snapshots__/CheckboxOrRadioGroup.test.tsx.snap new file mode 100644 index 00000000000..9d3641efc54 --- /dev/null +++ b/src/__tests__/__snapshots__/CheckboxOrRadioGroup.test.tsx.snap @@ -0,0 +1,125 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CheckboxOrRadioGroup renders consistently 1`] = ` +.c0 { + margin: 0; + padding: 0; + border: none; +} + +.c1 { + margin-bottom: 8px; + padding: 0; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c4 > input { + margin-left: 0; + margin-right: 0; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + list-style: none; + margin: 0; + padding: 0; +} + +.c2 > * + * { + margin-top: 8px; +} + +.c5 { + cursor: pointer; +} + +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; diff --git a/src/__tests__/__snapshots__/ChoiceFieldset.test.tsx.snap b/src/__tests__/__snapshots__/ChoiceFieldset.test.tsx.snap index 73ef8296197..293456083ea 100644 --- a/src/__tests__/__snapshots__/ChoiceFieldset.test.tsx.snap +++ b/src/__tests__/__snapshots__/ChoiceFieldset.test.tsx.snap @@ -34,11 +34,6 @@ exports[`ChoiceFieldset renders a disabled list 1`] = ` flex-direction: column; } -.c6 { - cursor: pointer; - cursor: not-allowed; -} - .c2 { color: #57606a; font-size: 16px; @@ -50,7 +45,12 @@ exports[`ChoiceFieldset renders a disabled list 1`] = ` font-size: 14px; display: block; color: #57606a; + cursor: default; +} + +.c6 { cursor: pointer; + cursor: not-allowed; } .c3 { @@ -198,10 +198,6 @@ exports[`ChoiceFieldset renders a fieldset with a description 1`] = ` flex-direction: column; } -.c7 { - cursor: pointer; -} - .c2 { font-size: 16px; padding: 0; @@ -215,6 +211,10 @@ exports[`ChoiceFieldset renders a fieldset with a description 1`] = ` cursor: pointer; } +.c7 { + cursor: pointer; +} + .c4 { display: -webkit-box; display: -webkit-flex; @@ -385,10 +385,6 @@ exports[`ChoiceFieldset renders a list of items with leading visuals and caption fill: currentColor; } -.c6 { - cursor: pointer; -} - .c2 { font-size: 16px; padding: 0; @@ -402,6 +398,10 @@ exports[`ChoiceFieldset renders a list of items with leading visuals and caption cursor: pointer; } +.c6 { + cursor: pointer; +} + .c3 { display: -webkit-box; display: -webkit-flex; @@ -693,10 +693,6 @@ exports[`ChoiceFieldset renders with a hidden legend 1`] = ` flex-direction: column; } -.c5 { - cursor: pointer; -} - .c7 { font-weight: 600; font-size: 14px; @@ -718,6 +714,10 @@ exports[`ChoiceFieldset renders with a hidden legend 1`] = ` border-width: 0; } +.c5 { + cursor: pointer; +} + .c2 { display: -webkit-box; display: -webkit-flex; @@ -878,8 +878,9 @@ exports[`ChoiceFieldset renders with a success validation message 1`] = ` display: flex; } -.c6 { - cursor: pointer; +.c11 { + -webkit-animation: 170ms eGcHP cubic-bezier(0.44,0.74,0.36,1); + animation: 170ms eGcHP cubic-bezier(0.44,0.74,0.36,1); } .c2 { @@ -895,9 +896,8 @@ exports[`ChoiceFieldset renders with a success validation message 1`] = ` cursor: pointer; } -.c11 { - -webkit-animation: 170ms eGcHP cubic-bezier(0.44,0.74,0.36,1); - animation: 170ms eGcHP cubic-bezier(0.44,0.74,0.36,1); +.c6 { + cursor: pointer; } .c3 { @@ -1038,6 +1038,7 @@ exports[`ChoiceFieldset renders with a success validation message 1`] = ` font-size="0" >