Skip to main content

Designvalg for RadioGroup

Denne siden forklarer hvorfor RadioGroup er bygget som den er. Den er ment for de som er nysgjerrige på avveiningene bak komponenten. Du trenger den ikke for å bruke den. For bruk og API, se RadioGroup.

WC eier ARIA og DOM-synk; React-laget er bevisst tynt

Hele ARIA-koblingsapparatet ligger i <ix-radio-group>: id-generering, htmlFor, name-synkronisering, aria-labelledby/describedby/invalid/required, disabled-propagering og readonly-tastaturblokk. React-wrapperen eksponerer kun props-API, kontrollert state og presentasjons-attributter (data-state, data-orientation, className).

Hvorfor: HTML- og React-bruk får identisk a11y-oppførsel uten duplisering. Vurdert: eie ARIA i React og late web component-en være ren styling — ville gjort HTML-bruken systematisk dårligere.

<div role="radiogroup">, ikke <fieldset>

Komponenten bruker en host-<div> (gjennom custom element <ix-radio-group>) med role="radiogroup" istedenfor native <fieldset> + <legend>.

Hvorfor: Safari har kjente bugs med <fieldset> kombinert med flex/grid-layout, og DigDir anbefaler ARIA-varianten for radioknapp-grupper. Vurdert: native <fieldset> — semantisk reneste, men praktisk ubrukelig på grunn av layout-buggene. Detaljene står i _strategier/accessibility-cross-cutting-concerns.md §9.6.

Compound-komponent: RadioGroup + RadioButton

API-et i React er <RadioGroup> med <RadioButton>-barn, ikke en prop-array (options={...}).

Hvorfor: gir lesbar JSX og lar hver knapp bære eget innhold (description, custom labels, ekstra HTML-attributter). Vurdert: options-array — låser strukturen og gjør alt som ikke er "label + value" tungvint.

Context bærer de native felt-koblingene, ikke disabled/readOnly/required

RadioGroupContext propagerer det React må rute ned på hver native <input>: gruppens name, valgt verdi (kontrollert modus), den event-baserte onChange, onBlur, og i register-modus også RHF sin ref (samme objekt på alle inputs, som RHF akkumulerer) pluss et uncontrolled-flagg. disabled, readOnly og required ligger ikke i Context. De settes som HTML-attributter på <ix-radio-group>, og web component-en propagerer dem ned til hver <input>.

Hvorfor: radioene er ekte native input-elementer, så register() kobles slik det er bygget for radios: ref/onChange/onBlur rutes rett ned på inputene, og RHF eier checked via de native refene. Context er kanalen for den rutingen. disabled m.fl. holdes utenfor for å unngå dobbelt sannhet: WC må uansett vite om disabled for per-knapp bevaring (se under), så å duplisere det i Context ville bare gitt to kilder å holde i synk.

Ingen egen web component for RadioButton

indeks-web har bare <ix-radio-group>. En enkelt radioknapp er CSS rundt en native <input type="radio"> og har ingen logikk som krever JavaScript.

Vurdert: <ix-radio-button> for symmetri med gruppen, men det ville bare wrappet et input uten å gi noe.

childList-observer wirer dynamisk lagt til inputs

En MutationObserver lytter etter at nye <input type="radio"> legges til etter mount (typisk via React conditional rendering) og kjører ID-/name-/htmlFor-/disabled-wiring på dem (indeks-web/lib/components/radio-group/IxRadioGroup.ts).

Hvorfor: a11y må fungere ved dynamiske gruppestørrelser uten at forfatteren manuelt må kalle noe.

aria-labelledby til legend, aria-describedby til description og error

Gruppen får sitt tilgjengelige navn fra legend-elementet via aria-labelledby. Hjelpetekst og evt. feilmelding kobles via aria-describedby (description først, error etter).

Hvorfor: gir skjermlesere konsistent rekkefølge: gruppe-navn → hjelpetekst → feilmelding.

aria-invalid settes kun på host, ikke på hver input

Error-elementet ligger alltid i DOM. En MutationObserver setter aria-invalid="true"<ix-radio-group> så snart error-noden får innhold, og fjerner attributten igjen når den tømmes.

Hvorfor: gruppens validitet er det som teller for skjermlesere. Å duplisere aria-invalid på hver input gir ingen merverdi. Vurdert: per-input aria-invalid — bare støy, samme informasjon én gang.

aria-live="polite" på error, ikke role="alert"

Error-elementet får aria-live="polite". Komponenten setter dette selv. Forfatteren skal ikke skrive det.

Hvorfor: polite venter til skjermleseren er ferdig med pågående annonsering, mens role="alert" (implisitt assertive) avbryter. Feilmeldinger i skjema skal ikke avbryte brukeren midt i en setning.

Native tastaturoppførsel beholdes

Tab inn/ut av gruppen, piltaster (alle fire retninger) for å bytte valg, Space for å velge. Vi har ingen egne tastatur-handlere: native <input type="radio"> med felles name gir alt gratis.

Eneste unntak: readonly blokkeres i en keydown-listener fordi readOnly er en no-op på radio-input per HTML-spec. pointer-events: none i CSS dekker mus og touch.

required på første input, disabled bevart per knapp via WeakMap

For required: web component-en setter requiredkun første input og aria-required="true" på host. HTML5-form-validering trigges av én required siden alle inputs deler name.

Hvorfor: Safari viser separate validation-bobler per input hvis flere er required. Vurdert: required på alle inputs — gir duplikate bobler.

For disabled: web component-en snapshotter hver inputs egen disabled i en WeakMap første gang gruppe-disabled toggles på, og restorer verdien når gruppe-disabled toggles av igjen.

Hvorfor: per-knapp disabled (satt av forfatteren) må overleve at gruppen blir deaktivert og reaktivert. Vurdert: enkel overskriving av input.disabled — ville mistet per-knapp-info ved første gruppe-toggle.