From cea849011d9a4422a1aae8612f9a083e33437caa Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Thu, 7 May 2026 16:17:30 +0200 Subject: [PATCH 1/5] fix(useCheckboxGroup): allow passing props shared by all checkboxes to useCheckboxGroup --- .../components/checkbox/checkbox.stories.tsx | 180 +++--------------- .../use-checkbox-group/use-checkbox-group.ts | 92 +++++---- 2 files changed, 78 insertions(+), 194 deletions(-) diff --git a/apps/www/app/content/components/checkbox/checkbox.stories.tsx b/apps/www/app/content/components/checkbox/checkbox.stories.tsx index 0e40b28c16..2475f7a7a0 100644 --- a/apps/www/app/content/components/checkbox/checkbox.stories.tsx +++ b/apps/www/app/content/components/checkbox/checkbox.stories.tsx @@ -25,7 +25,9 @@ export const OneOption = () => ( ); export const Group = () => { - const [value, setValue] = useState(['epost']); + const { getCheckboxProps } = useCheckboxGroup({ + value: ['epost'], + }); return (
@@ -35,48 +37,17 @@ export const Group = () => { Velg alle alternativene som er relevante for deg. - { - if (e.target.checked) { - setValue([...value, 'epost']); - } else { - setValue(value.filter((v) => v !== 'epost')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'telefon']); - } else { - setValue(value.filter((v) => v !== 'telefon')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'sms']); - } else { - setValue(value.filter((v) => v !== 'sms')); - } - }} - /> + + +
); }; export const GroupEn = () => { - const [value, setValue] = useState(['epost']); + const { getCheckboxProps } = useCheckboxGroup({ + value: ['email'], + }); return (
@@ -84,42 +55,9 @@ export const GroupEn = () => { Select all the options that are relevant to you. - { - if (e.target.checked) { - setValue([...value, 'email']); - } else { - setValue(value.filter((v) => v !== 'email')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'phone']); - } else { - setValue(value.filter((v) => v !== 'phone')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'text']); - } else { - setValue(value.filter((v) => v !== 'text')); - } - }} - /> + + +
); }; @@ -351,7 +289,10 @@ export const InTableEn = () => { }; export const Outline = () => { - const [value, setValue] = useState(['epost']); + const { getCheckboxProps } = useCheckboxGroup({ + value: ['epost'], + variant: 'outline', + }); return (
@@ -361,51 +302,18 @@ export const Outline = () => { Velg alle alternativene som er relevante for deg. - { - if (e.target.checked) { - setValue([...value, 'epost']); - } else { - setValue(value.filter((v) => v !== 'epost')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'telefon']); - } else { - setValue(value.filter((v) => v !== 'telefon')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'sms']); - } else { - setValue(value.filter((v) => v !== 'sms')); - } - }} - /> + + +
); }; export const OutlineEn = () => { - const [value, setValue] = useState(['epost']); + const { getCheckboxProps } = useCheckboxGroup({ + value: ['email'], + variant: 'outline', + }); return (
@@ -413,45 +321,9 @@ export const OutlineEn = () => { Select all the options that are relevant to you. - { - if (e.target.checked) { - setValue([...value, 'email']); - } else { - setValue(value.filter((v) => v !== 'email')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'phone']); - } else { - setValue(value.filter((v) => v !== 'phone')); - } - }} - /> - { - if (e.target.checked) { - setValue([...value, 'text']); - } else { - setValue(value.filter((v) => v !== 'text')); - } - }} - /> + + +
); }; diff --git a/packages/react/src/utilities/hooks/use-checkbox-group/use-checkbox-group.ts b/packages/react/src/utilities/hooks/use-checkbox-group/use-checkbox-group.ts index d8e813598c..b55d5d4a44 100644 --- a/packages/react/src/utilities/hooks/use-checkbox-group/use-checkbox-group.ts +++ b/packages/react/src/utilities/hooks/use-checkbox-group/use-checkbox-group.ts @@ -7,45 +7,49 @@ import type { } from 'react'; import { useEffect, useId, useRef, useState } from 'react'; import type { CheckboxProps } from '../../../components'; +import type { MergeRight } from '../../types'; -export type UseCheckboxGroupProps = { - /** - * Disables all checkboxes in the group. - */ - disabled?: boolean; - /** - * Error message for the group. - * If set, all checkboxes will have `aria-invalid` set to `true`. - */ - error?: ReactNode; - /** - * Name of the group. - * If not set, a random id will be generated. - */ - name?: string; - /** - * Makes all checkboxes in the group read-only. - * If set, all checkboxes will have `aria-readonly` set to `true`. - */ - readOnly?: boolean; - /** - * Initial value of the group - * @default [] - */ - value?: string[]; - /** - * Makes all checkboxes in the group required. - * If set, all checkboxes will have `required` set to `true`. - */ - required?: boolean; - /** - * Callback that is called when the value of the group changes. - * @param nextValue string[] - * @param currentValue string[] - * @returns void - */ - onChange?: (nextValue: string[], currentValue: string[]) => void; -}; +export type UseCheckboxGroupProps = MergeRight< + CheckboxProps, + { + /** + * Disables all checkboxes in the group. + */ + disabled?: boolean; + /** + * Error message for the group. + * If set, all checkboxes will have `aria-invalid` set to `true`. + */ + error?: ReactNode; + /** + * Name of the group. + * If not set, a random id will be generated. + */ + name?: string; + /** + * Makes all checkboxes in the group read-only. + * If set, all checkboxes will have `aria-readonly` set to `true`. + */ + readOnly?: boolean; + /** + * Initial value of the group + * @default [] + */ + value?: string[]; + /** + * Makes all checkboxes in the group required. + * If set, all checkboxes will have `required` set to `true`. + */ + required?: boolean; + /** + * Callback that is called when the value of the group changes. + * @param nextValue string[] + * @param currentValue string[] + * @returns void + */ + onChange?: (nextValue: string[], currentValue: string[]) => void; + } +>; /** * Get anything that is set on a checkbox, but @@ -151,7 +155,14 @@ export function useCheckboxGroup( * */ getCheckboxProps: (propsOrValue?: string | GetCheckboxProps) => { - const props = + let groupProps: + | Omit + | undefined; + if (props) { + const { onChange, error, ...rest } = props; + groupProps = rest; + } + const checkboxProps = typeof propsOrValue === 'string' ? { value: propsOrValue } : propsOrValue || {}; @@ -161,7 +172,7 @@ export function useCheckboxGroup( ref: forwardedRef = undefined, value = '', ...rest - } = props; + } = checkboxProps; const handleRef = (element: HTMLInputElement | null) => { if (element) { @@ -209,6 +220,7 @@ export function useCheckboxGroup( }; return { + ...groupProps, ...rest, 'aria-describedby': `${error ? errorId : ''} ${rest['aria-describedby'] || ''}`.trim() || From edd689d6605ee4a840846029db204335ff8d66f4 Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Fri, 8 May 2026 12:03:04 +0200 Subject: [PATCH 2/5] fix(useRadioGroup): allow passing props shared by all radios to useRadioGroup --- .../components/radio/radio.stories.tsx | 65 ++++++++++--------- .../hooks/use-radio-group/use-radio-group.ts | 56 +++++++++------- 2 files changed, 69 insertions(+), 52 deletions(-) diff --git a/apps/www/app/content/components/radio/radio.stories.tsx b/apps/www/app/content/components/radio/radio.stories.tsx index 8c1289a847..119851a958 100644 --- a/apps/www/app/content/components/radio/radio.stories.tsx +++ b/apps/www/app/content/components/radio/radio.stories.tsx @@ -1,6 +1,7 @@ import { Fieldset, Radio, + useRadioGroup, ValidationMessage, } from '@digdir/designsystemet-react'; @@ -9,6 +10,11 @@ export const Preview = () => { }; export const Group = () => { + const { getRadioProps } = useRadioGroup({ + name: 'kontakt', + value: 'epost', + }); + return (
Hvordan ønsker du at vi kontakter deg? @@ -19,26 +25,28 @@ export const Group = () => {
); }; export const GroupEn = () => { + const { getRadioProps } = useRadioGroup({ + name: 'contact', + value: 'email', + }); + return (
How would you like us to contact you? @@ -49,20 +57,17 @@ export const GroupEn = () => {
); @@ -189,6 +194,12 @@ export const InlineEn = () => { }; export const Outline = () => { + const { getRadioProps } = useRadioGroup({ + name: 'kontakt', + value: 'epost', + variant: 'outline', + }); + return (
Hvordan ønsker du at vi kontakter deg? @@ -198,30 +209,30 @@ export const Outline = () => {
); }; export const OutlineEn = () => { + const { getRadioProps } = useRadioGroup({ + name: 'contact', + value: 'email', + variant: 'outline', + }); + return (
How would you like us to contact you? @@ -232,23 +243,17 @@ export const OutlineEn = () => {
); diff --git a/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts b/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts index 972c5ec8f1..6f9ec93ae5 100644 --- a/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts +++ b/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts @@ -7,27 +7,31 @@ import type { } from 'react'; import { useId, useState } from 'react'; import type { RadioProps } from '../../../components'; +import type { MergeRight } from '../../types'; -export type UseRadioGroupProps = { - /** Set disabled state of all radios */ - disabled?: boolean; - /** Shared error message for all radios */ - error?: ReactNode; - /** Name of all radios. - * @default string of auto-generated name - */ - name?: string; - /** Set read only state of all radios */ - readOnly?: boolean; - /** Set required state of all radios */ - required?: boolean; - /** - * Initial value of the group - */ - value?: string; - /** Callback when selected radios changes */ - onChange?: (nextValue: string, prevValue: string) => void; -}; +export type UseRadioGroupProps = MergeRight< + RadioProps, + { + /** Set disabled state of all radios */ + disabled?: boolean; + /** Shared error message for all radios */ + error?: ReactNode; + /** Name of all radios. + * @default string of auto-generated name + */ + name?: string; + /** Set read only state of all radios */ + readOnly?: boolean; + /** Set required state of all radios */ + required?: boolean; + /** + * Initial value of the group + */ + value?: string; + /** Callback when selected radios changes */ + onChange?: (nextValue: string, prevValue: string) => void; + } +>; /** * Get anything that is set on a radio, but @@ -106,11 +110,18 @@ export function useRadioGroup({ * */ getRadioProps: (propsOrValue: string | GetRadioProps) => { - const props = + let groupProps: + | Omit + | undefined; + if (props) { + const { onChange, error, ...rest } = props; + groupProps = rest; + } + const radioProps = typeof propsOrValue === 'string' ? { value: propsOrValue } : propsOrValue; - const { ref: forwardedRef = undefined, value = '', ...rest } = props; + const { ref: forwardedRef = undefined, value = '', ...rest } = radioProps; const handleRef = (element: HTMLInputElement | null) => { if (element) { @@ -138,6 +149,7 @@ export function useRadioGroup({ }; return { + ...groupProps, ...rest, name: radioGroupName, 'aria-describedby': From af9d762dd5371be5049912d41dc77576388305f1 Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Fri, 8 May 2026 12:09:34 +0200 Subject: [PATCH 3/5] Add changeset --- .changeset/fuzzy-zoos-rhyme.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-zoos-rhyme.md diff --git a/.changeset/fuzzy-zoos-rhyme.md b/.changeset/fuzzy-zoos-rhyme.md new file mode 100644 index 0000000000..bd74ad74b7 --- /dev/null +++ b/.changeset/fuzzy-zoos-rhyme.md @@ -0,0 +1,5 @@ +--- +"@digdir/designsystemet-react": patch +--- + +**useCheckboxGroup** and **useRadioGroup**: these hooks now accept all `CheckboxProps` / `RadioProps` that don't conflict with the hooks' own props. This can be used to easily set common props like `variant` for all the checkboxes or radios in the group. From 0bdf68440d3ce4443767c2e60ec912930621e9a0 Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Fri, 8 May 2026 12:40:18 +0200 Subject: [PATCH 4/5] fix checkbox Disabled story extending the wrong base story, which now caused all three checkboxes to have the same label/description --- packages/react/src/components/checkbox/checkbox.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/components/checkbox/checkbox.stories.tsx b/packages/react/src/components/checkbox/checkbox.stories.tsx index f7ce01e410..196d7c9705 100644 --- a/packages/react/src/components/checkbox/checkbox.stories.tsx +++ b/packages/react/src/components/checkbox/checkbox.stories.tsx @@ -177,7 +177,7 @@ export const ReadOnly = { export const Disabled = { args: { - ...Preview.args, + ...Group.args, name: 'my-disabled', disabled: true, }, From 8450f9445c28394ddc16ffb3841b95183b8c1df8 Mon Sep 17 00:00:00 2001 From: Une Sofie Kinn Ekroll Date: Fri, 8 May 2026 12:53:31 +0200 Subject: [PATCH 5/5] Align function signature for useRadioGroup with useCheckboxGroup --- .../hooks/use-radio-group/use-radio-group.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts b/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts index 6f9ec93ae5..36116c5d31 100644 --- a/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts +++ b/packages/react/src/utilities/hooks/use-radio-group/use-radio-group.ts @@ -78,16 +78,17 @@ type useRadioGroupReturn = { * value: '', * }); */ -export function useRadioGroup({ - error, - readOnly, - required, - disabled, - name, - onChange, - value: initalValue = '', -}: UseRadioGroupProps = {}): useRadioGroupReturn { - const [groupValue, setGroupValue] = useState(initalValue); +export function useRadioGroup(props?: UseRadioGroupProps): useRadioGroupReturn { + const { + error, + readOnly, + required, + disabled, + name, + onChange, + value: initialValue = '', + } = props || {}; + const [groupValue, setGroupValue] = useState(initialValue); const errorId = useId(); const namedId = useId(); const radioGroupName = name || namedId;