Esikatselu
<SelectionGroup labelText="Lorem ipsum" helpText="Dolor sit amet" hasLegacyLegend={false}>
<Checkbox name="lorem" labelText="Lorem" />
<Checkbox name="lorem" labelText="Ipsum" />
<Checkbox name="lorem" labelText="Dolor" />
</SelectionGroup>
Käyttötarkoitus
Komponentin avulla voi ryhmitellä valintanapit ja valintaruudut saavutettaviksi ryhmiksi. Valintanapit ja valintaruudut ryhmitellään pääasiassa allekkain.
Saavutettavuus
Komponentti toteuttaa oletuksena semanttisen fieldset-ryhmän, joka otsikoidaan legend-elementillä.
Ryhmän otsikko, täyttöohje ja virheilmoitus luetaan automaattisesti, kun kohdistus siirtyy ryhmässä olevaan valintapainikkeeseen tai valintaruutuun.
Vaihtoehtoisesti ryhmittely voidaan toteuttaa myös ARIA-rooleilla role="group" tai role="radiogroup".
Elementtiä voi vaihtaa as-propilla, jonka vaihtaminen lisää automaatisesti oikean roolin ja otsikkoviittauksen aria-labelledby-attribuuttiin.
Huom! VoiceOver- (iOS) ja TalkBack-ruudunlukijat tukevat paremmin natiivia fieldset-ryhmittelyä. ARIA-rooleja käytettäessä ryhmän nimeäminen ja virheilmoitukset saattavat jäädä lukematta.
Katso myös: Valintojen ryhmittely ilman näkyvää nimilappua
Tunnetut ongelmat
Valintaruutuja käytettäessä NVDA ei mene automaattisesti lomaketilaan, toisin kuin muiden komponenttien kanssa.
Tästä syystä virheilmoituksen liittäminen fieldset-elementtiin aria-describedby-attribuutilla ei toimi ja virheviesti
jää kokonaan lukematta. Valintanappien kanssa aria-describedby-attribuutin käyttö virheviestin automaattiseen lukemiseen toimii.
Lue lisää: Why do radio buttons trigger focus mode, but checkboxes don't? (Avautuu uuteen välilehteen)
Komponentissa on nyt toteutus, jossa täyttöohje ja virheilmoitus on piilotettu ruudunlukijoille osaksi legend-elementtiä.
Lukemisjärjestys ei ole aivan optimaalinen, mutta valintanapit ja valintaruudut toimivat samalla logiikalla. Lisäksi osa sisällöstä joudutaan toistamaan piilotettuna ja ruudunlukijalle näkyy näkymättömiä tekstielementtejä.
Esimerkit
Radio
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
>
<Radio name="prescription-radio" labelText="Kaikki reseptit" />
<Radio name="prescription-radio" labelText="Lääkettä jäljellä" />
<Radio name="prescription-radio" labelText="Uusimispyyntö kesken" />
</SelectionGroup>
Checkbox
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
>
<Checkbox name="prescription" labelText="Kaikki reseptit" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" />
</SelectionGroup>
Oletusvalinta
<SelectionGroup labelText="Rajaa näytettäviä reseptejä" hasLegacyLegend={false}>
<Radio name="prescription-radio-default" labelText="Kaikki reseptit" defaultChecked />
<Radio name="prescription-radio-default" labelText="Lääkettä jäljellä" />
<Radio name="prescription-radio-default" labelText="Uusimispyyntö kesken" />
</SelectionGroup>
<SelectionGroup labelText="Rajaa näytettäviä reseptejä" hasLegacyLegend={false}>
<Checkbox name="prescription" labelText="Kaikki reseptit" defaultChecked />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" />
</SelectionGroup>
Virheilmoitus
Näytä virheellisessä kenttäryhmässä aina selkeä virheilmoitus. Ryhmälle näytetään aina yksi yhteinen virheilmoitus
valintojen alapuolella. Kenttäryhmän voi merkitä virheelliseksi käyttämällä invalid ja errorText -propeja.
<SelectionGroup
errorText="Valitse vastaus."
hasLegacyLegend={false}
invalid
labelText="Onko sinulla alle 18-vuotiaita lapsia?"
required
requiredLabel="Pakollinen tieto"
>
<Radio name="prescription-radio-invalid" labelText="Kyllä" value="Kyllä" />
<Radio name="prescription-radio-invalid" labelText="Ei" value="Ei" />
</SelectionGroup>
<SelectionGroup
errorText="Valitse vastaus."
hasLegacyLegend={false}
invalid
labelText="Hyväksyn palvelun ehdot"
required
requiredLabel="Pakollinen tieto"
>
<Checkbox name="terms" labelText="Hyväksyn palvelun käyttöehdot" value="yes" />
<Checkbox name="privacy" labelText="Hyväksyn palvelun tietosuojaehdot" value="yes" />
</SelectionGroup>
Estetty ryhmä
<SelectionGroup labelText="Rajaa näytettäviä reseptejä" hasLegacyLegend={false} disabled>
<Radio name="prescription-radio-disabled" labelText="Kaikki reseptit" value="kaikki" />
<Radio name="prescription-radio-disabled" labelText="Lääkettä jäljellä" value="jäljellä" />
<Radio name="prescription-radio-disabled" labelText="Uusimispyyntö kesken" value="kesken" />
</SelectionGroup>
<SelectionGroup labelText="Rajaa näytettäviä reseptejä" hasLegacyLegend={false} disabled>
<Checkbox name="prescription" labelText="Kaikki reseptit" value="kaikki" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" value="jäljellä" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" value="kesken" />
</SelectionGroup>
Kaksipalstainen asettelu
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
hasLegacyLegend={false}
>
<Radio name="prescription-radio-2columns" labelText="Kaikki reseptit" value="kaikki" defaultChecked />
<Radio name="prescription-radio-2columns" labelText="Lääkettä jäljellä" value="jäljellä" />
<Radio name="prescription-radio-2columns" labelText="Uusimispyyntö kesken" value="kesken" />
</SelectionGroup>
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
hasLegacyLegend={false}
>
<Checkbox name="prescription" labelText="Kaikki reseptit" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" />
</SelectionGroup>
Sivuttainen asettelu
Yksipalstainen asettelu.
<SelectionGroup labelText="Rajaa näytettäviä reseptejä" hasLegacyLegend={false} horizontal>
<Radio name="prescription-radio-horizontal" labelText="Kaikki reseptit" value="kaikki" defaultChecked />
<Radio name="prescription-radio-horizontal" labelText="Lääkettä jäljellä" value="jäljellä" />
<Radio name="prescription-radio-horizontal" labelText="Uusimispyyntö kesken" value="kesken" />
</SelectionGroup>
Kaksipalstainen asettelu.
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
hasLegacyLegend={false}
horizontal
>
<Radio name="prescription-radio-horizontal-2columns" labelText="Kaikki reseptit" value="kaikki" defaultChecked />
<Radio name="prescription-radio-horizontal-2columns" labelText="Lääkettä jäljellä" value="jäljellä" />
<Radio name="prescription-radio-horizontal-2columns" labelText="Uusimispyyntö kesken" value="kesken" />
</SelectionGroup>
Pakollisuuden merkitseminen ryhmälle
Yksi valinnoista pakollinen
Esimerkki merkitsee kentät pakollisiksi siihen asti, että yksi niistä on valittu. Valinnan jälkeen yksittäisiä valintaruutuja ei enää ilmoiteta pakollisiksi.
function SelectionGroupExample() {
const [options, setOptions] = useState([
{ value: "all", label: "Kaikki reseptit", isChecked: false },
{ value: "available", label: "Lääkettä jäljellä", isChecked: false },
{ value: "renewal", label: "Lääkettä jäljellä", isChecked: false },
]);
const hasSelectedOption = options.some(({ isChecked }) => isChecked);
const handleOnChange = (event) => {
const { value, checked } = event.target;
setOptions((currentOptions) =>
currentOptions.map((option) => (option.value === value ? { ...option, isChecked: checked } : option))
);
};
return (
<SelectionGroup
labelText="Rajaa näytettäviä reseptejä"
hasLegacyLegend={false}
requiredLabel="Pakollinen tieto"
requiredGroup
>
{options.map(({ label, value, isChecked }) => (
<Checkbox
checked={isChecked}
key={value}
labelText={label}
name={value}
onChange={handleOnChange}
requiredGroup={!hasSelectedOption}
value={value}
/>
))}
</SelectionGroup>
);
}
Kaikki valinnat pakollisia
Esimerkki merkitsee kaikki ryhmän valintaruudut pakollisiksi ja jokainen niistä ilmoitetaan ruudunlukijalle pakollisena. Käytetään silloin kun käyttäjän on valittava kaikki valintaruudut.
<SelectionGroup labelText="Hyväksyn palvelun ehdot" hasLegacyLegend={false} requiredLabel="Pakollinen tieto" required>
<Checkbox name="terms" labelText="Hyväksyn palvelun käyttöehdot" value="yes" />
<Checkbox name="privacy" labelText="Hyväksyn palvelun tietosuojaehdot" value="yes" />
</SelectionGroup>
Kokovaihtoehdot
Pienin koko (xs) on tarkoitettu tiivistä asettelua vaativiin asiantuntijakäyttöliittymiin.
<>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="xs"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="xs" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="xs" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="xs" />
</SelectionGroup>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="sm"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="sm" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="sm" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="sm" />
</SelectionGroup>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="md"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="md" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="md" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="md" />
</SelectionGroup>
</>
<>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="xs"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="xs" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="xs" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="xs" />
</SelectionGroup>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="sm"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="sm" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="sm" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="sm" />
</SelectionGroup>
<SelectionGroup
className="kds-mb-6"
labelText="Rajaa näytettäviä reseptejä"
leftCol={(children) => <Column md={4}>{children}</Column>}
helpText="Valitse mitkä reseptit näytetään listauksessa."
hasLegacyLegend={false}
size="md"
>
<Checkbox name="prescription" labelText="Kaikki reseptit" size="md" />
<Checkbox name="prescription" labelText="Lääkettä jäljellä" size="md" />
<Checkbox name="prescription" labelText="Uusimispyyntö kesken" size="md" />
</SelectionGroup>
</>
Valintojen ryhmittely ilman näkyvää nimilappua
Piilotettu nimilappu
SelectionGroup-komponentille voidaan antaa piilotettu nimilappu (hiddenLabelText), joka toimii ryhmän nimenä tilanteessa, jossa ryhmällä ei ole näkyvää nimilappua.
Huonona puolena vaihtoehdossa on näkymätön tekstielementti, johon kuitenkin pääsee ruudunlukijalla navigoimaan.
function SelectionGroupExample() {
const baseId = useId();
const radioRef = useRef<HTMLInputElement[]>([]);
const [value, setValue] = useState<string>("");
const [invalid, setInvalid] = useState(false);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
if (invalid && event.target.value) {
setInvalid(false);
}
};
const handleSubmit = () => {
const isInvalid = value ? false : true;
if (isInvalid) {
setInvalid(true);
radioRef.current[0]?.focus();
}
};
return (
<Chat>
<ChatMessage position="left">
<Text>Saatko virtuaalivaluutan louhinnasta tuloja?</Text>
</ChatMessage>
<ChatMessage position="right">
<SelectionGroup
className="kds-mb-4 sm:kds-mb-6"
errorText="Valitse vastaus."
hiddenLabelText="Saatko virtuaalivaluutan louhinnasta tuloja?"
id={baseId}
invalid={invalid}
required
>
<Radio
checked={value === "yes"}
labelText="Kyllä"
name="virtual-income"
onChange={handleChange}
ref={(el) => (radioRef.current[0] = el)}
value="yes"
/>
<Radio
checked={value === "no"}
labelText="En saa"
name="virtual-income"
onChange={handleChange}
ref={(el) => (radioRef.current[1] = el)}
value="no"
/>
</SelectionGroup>
<ButtonGroup horizontal>
<Button onClick={handleSubmit}>Jatka</Button>
<Button appearance="outline">Palaa</Button>
</ButtonGroup>
</ChatMessage>
</Chat>
);
}
Nimilappuun viittaminen aria-labelledby-attribuutilla
Vaihtoehtoisesti voidaan käyttää aria-labelledby-attribuuttia yhdistämään nimilappu, täyttöohje ja virheilmoitus ryhmään.
Vaihtoehto ei tuota näkymättömiä tekstielementtejä, mutta se ei toimi yhtä hyvin erityisesti VoiceOver- (iOS) ja TalkBack-ruudunlukijoiden kanssa ryhmään liitettyjen tekstien ilmoittamiseen.
function SelectionGroupExample() {
const baseId = useId();
const checkboxRef = useRef<HTMLInputElement[]>([]);
const [terms, setTerms] = useState(false);
const [privacy, setPrivacy] = useState(false);
const [invalidTerms, setInvalidTerms] = useState(false);
const [invalidPrivacy, setInvalidPrivacy] = useState(false);
const isGroupInvalid = invalidTerms || invalidPrivacy;
const handleTermsChange = (event: ChangeEvent<HTMLInputElement>) => {
setTerms(event.target.checked);
setInvalidTerms(!event.target.checked);
};
const handlePrivacyChange = (event: ChangeEvent<HTMLInputElement>) => {
setPrivacy(event.target.checked);
setInvalidPrivacy(!event.target.checked);
};
const handleSubmit = () => {
const isInvalid = !(terms && privacy);
if (isInvalid) {
setInvalidTerms(!terms);
setInvalidPrivacy(!privacy);
if (!terms) {
checkboxRef.current[0]?.focus();
} else {
checkboxRef.current[1]?.focus();
}
}
};
return (
<Chat>
<ChatMessage position="left">
<Text id={`${baseId}-label`}>
Hyväksytkö palvelun ehdot?<span aria-hidden="true"> *</span>
<span className="kds-sr-only"> Pakollinen tieto</span>
</Text>
</ChatMessage>
<ChatMessage position="right">
<SelectionGroup
ariaLabelledBy={({ ids }) => clsx([`${baseId}-label`, ids.help, ids.error].filter(Boolean).join(" "))}
as="div"
className="kds-mb-4 sm:kds-mb-6"
errorText="Valitse vastaus."
id={baseId}
invalidGroup={isGroupInvalid}
required
>
<Checkbox
checked={terms}
invalid={invalidTerms}
labelText="Hyväksyn palvelun käyttöehdot"
name="terms"
onChange={handleTermsChange}
ref={(el) => (checkboxRef.current[0] = el)}
value="yes"
/>
<Checkbox
checked={privacy}
invalid={invalidPrivacy}
labelText="Hyväksyn palvelun tietosuojaehdot"
name="privacy"
onChange={handlePrivacyChange}
ref={(el) => (checkboxRef.current[1] = el)}
value="yes"
/>
</SelectionGroup>
<ButtonGroup horizontal>
<Button onClick={handleSubmit}>Jatka</Button>
<Button appearance="outline">Palaa</Button>
</ButtonGroup>
</ChatMessage>
</Chat>
);
}