diff --git a/.changeset/young-queens-notice.md b/.changeset/young-queens-notice.md new file mode 100644 index 00000000000..f9e3c7f417f --- /dev/null +++ b/.changeset/young-queens-notice.md @@ -0,0 +1,7 @@ +--- +"@primer/react": patch +--- + +`CheckboxGroup` and `RadioGroup` are now SSR-compatible. + +Warning: In this new implementation, `CheckboxGroup.Caption`, `CheckboxGroup.Label,` and `CheckboxGroup.Validation` must be direct children of `CheckboxGroup`. The same applies to `RadioGroup`. diff --git a/src/FormControl/FormControl.tsx b/src/FormControl/FormControl.tsx index 41c2056ecf9..f8e7cca7d8f 100644 --- a/src/FormControl/FormControl.tsx +++ b/src/FormControl/FormControl.tsx @@ -16,7 +16,7 @@ import ValidationAnimationContainer from '../_ValidationAnimationContainer' import {get} from '../constants' import FormControlLeadingVisual from './_FormControlLeadingVisual' import {SxProp} from '../sx' -import CheckboxOrRadioGroupContext from '../_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext' +import {CheckboxOrRadioGroupContext} from '../_CheckboxOrRadioGroup' import InlineAutocomplete from '../drafts/InlineAutocomplete' export type FormControlProps = { @@ -58,7 +58,7 @@ const FormControl = React.forwardRef( InlineAutocomplete, ] const choiceGroupContext = useContext(CheckboxOrRadioGroupContext) - const disabled = choiceGroupContext?.disabled || disabledProp + 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, diff --git a/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx b/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx index f08a88c93dd..b63812e920a 100644 --- a/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx +++ b/src/_CheckboxOrRadioGroup/CheckboxOrRadioGroup.tsx @@ -1,15 +1,14 @@ import React from 'react' +import styled from 'styled-components' import Box from '../Box' -import {useSSRSafeId} from '../utils/ssr' import ValidationAnimationContainer from '../_ValidationAnimationContainer' +import {get} from '../constants' +import {useSSRSafeId} from '../utils/ssr' 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 {useSlots} from '../hooks/useSlots' import {SxProp} from '../sx' export type CheckboxOrRadioGroupProps = { @@ -37,6 +36,8 @@ export type CheckboxOrRadioGroupContext = { captionId?: string } & CheckboxOrRadioGroupProps +export const CheckboxOrRadioGroupContext = React.createContext({}) + const Body = styled.div` display: flex; flex-direction: column; @@ -57,6 +58,11 @@ const CheckboxOrRadioGroup: React.FC { + const [slots, rest] = useSlots(children, { + caption: CheckboxOrRadioGroupCaption, + label: CheckboxOrRadioGroupLabel, + validation: CheckboxOrRadioGroupValidation, + }) const labelChild = React.Children.toArray(children).find( child => React.isValidElement(child) && child.type === CheckboxOrRadioGroupLabel, ) @@ -67,8 +73,8 @@ const CheckboxOrRadioGroup: React.FC - {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. +
+ + {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} - + 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(rest).filter(child => React.isValidElement(child))} + + + {validationChild && ( + + aria-hidden={Boolean(labelChild)} + show + > + {slots.validation} + + )} +
+
) } diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx index cbae8c9c3ed..2cb783f7c2d 100644 --- a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupCaption.tsx @@ -2,16 +2,14 @@ import React from 'react' import Text from '../Text' import {SxProp} from '../sx' import {CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' -import {Slot} from './slots' -const CheckboxOrRadioGroupCaption: React.FC> = ({children, sx}) => ( - - {({disabled, captionId}: CheckboxOrRadioGroupContext) => ( - - {children} - - )} - -) +const CheckboxOrRadioGroupCaption: React.FC> = ({children, sx}) => { + const {disabled, captionId} = React.useContext(CheckboxOrRadioGroupContext) + return ( + + {children} + + ) +} export default CheckboxOrRadioGroupCaption diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx deleted file mode 100644 index 415efeb281e..00000000000 --- a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupContext.tsx +++ /dev/null @@ -1,7 +0,0 @@ -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 index a95b8cac167..40e54cec14e 100644 --- a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel.tsx +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupLabel.tsx @@ -1,9 +1,8 @@ import React from 'react' import Box from '../Box' -import {SxProp} from '../sx' import VisuallyHidden from '../_VisuallyHidden' +import {SxProp} from '../sx' import {CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' -import {Slot} from './slots' export type CheckboxOrRadioGroupLabelProps = { /** @@ -16,30 +15,29 @@ const CheckboxOrRadioGroupLabel: React.FC ( - - {({required, disabled}: CheckboxOrRadioGroupContext) => ( - - {required ? ( - - {children} - * - - ) : ( - children - )} - - )} - -) +}) => { + const {required, disabled} = React.useContext(CheckboxOrRadioGroupContext) + return ( + + {required ? ( + + {children} + * + + ) : ( + children + )} + + ) +} export default CheckboxOrRadioGroupLabel diff --git a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx index b4ac8604b94..f213e5c3c94 100644 --- a/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx +++ b/src/_CheckboxOrRadioGroup/_CheckboxOrRadioGroupValidation.tsx @@ -3,7 +3,6 @@ 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 */ @@ -14,14 +13,13 @@ const CheckboxOrRadioGroupValidation: React.FC ( - - {({validationMessageId = ''}: CheckboxOrRadioGroupContext) => ( - - {children} - - )} - -) +}) => { + const {validationMessageId = ''} = React.useContext(CheckboxOrRadioGroupContext) + return ( + + {children} + + ) +} export default CheckboxOrRadioGroupValidation diff --git a/src/_CheckboxOrRadioGroup/index.ts b/src/_CheckboxOrRadioGroup/index.ts index ceebd3ba524..0a399acd807 100644 --- a/src/_CheckboxOrRadioGroup/index.ts +++ b/src/_CheckboxOrRadioGroup/index.ts @@ -1,2 +1,2 @@ -export {default} from './CheckboxOrRadioGroup' +export {default, CheckboxOrRadioGroupContext} from './CheckboxOrRadioGroup' export type {CheckboxOrRadioGroupProps} from './CheckboxOrRadioGroup' diff --git a/src/__tests__/CheckboxOrRadioGroup.test.tsx b/src/__tests__/CheckboxOrRadioGroup.test.tsx index bcaace9f81c..80793e40300 100644 --- a/src/__tests__/CheckboxOrRadioGroup.test.tsx +++ b/src/__tests__/CheckboxOrRadioGroup.test.tsx @@ -3,7 +3,7 @@ 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' +import CheckboxOrRadioGroup, {CheckboxOrRadioGroupContext} from '../_CheckboxOrRadioGroup' const INPUT_GROUP_LABEL = 'Choices' @@ -41,6 +41,7 @@ describe('CheckboxOrRadioGroup', () => { }) checkExports('_CheckboxOrRadioGroup', { default: CheckboxOrRadioGroup, + CheckboxOrRadioGroupContext, }) it('renders a group of inputs with a caption in the ', () => { render( diff --git a/src/hooks/useSlots.ts b/src/hooks/useSlots.ts index e9df43bc449..42fe382e629 100644 --- a/src/hooks/useSlots.ts +++ b/src/hooks/useSlots.ts @@ -5,7 +5,7 @@ import {warning} from '../utils/warning' export type SlotConfig = Record> type SlotElements = { - [Property in keyof Type]: React.ReactElement + [Property in keyof Type]: React.ReactElement, Type[Property]> } /** @@ -52,7 +52,7 @@ export function useSlots( } // If the child is a slot, add it to the `slots` object - slots[slotKey] = child + slots[slotKey] = child as React.ReactElement, T[keyof T]> }) return [slots, rest]