Yleistä
Suodatus vaatii aina pohjaksi tehdyn haun. Tavoitteena on rajata näytettäviä hakutuloksia siten, että tulokset vastaavat mahdollisimman hyvin käyttäjän tarpeita ja odotuksia.
Käyttäjä voi soveltaa hakutuloksiin erilaisia suodatusehtoja ja -yhdistelmiä osuvimpien tulosten löytämiseksi.
- Tarjoa käyttäjälle vain olennaisimmat suodattimet
- Otsikoi kaikki suodatusvaihtoehdot
- Näytä selkeästi, mitkä suodattimet ovat valittuina
- Indikoi aina selkeästi suodatuksen tila (esim. tulosten määrä ja aktiiviset suodattimet)
Saavutettavuus
- Saavutettavuuden kannalta on tärkeintä, että suodatusmekanismi on selkeä
- Ilmoita selkeästi, että tulosten määrä on muuttunut
- Älä siirrä käyttäjää eri paikkaan, kun suodatusehto muuttuu
- Ilmoita käyttäkälle myös se, jos suodatus ei tuota tuloksia
Rakenne
Haun ja suodatuksen rakenne ja asettelu seuraa käyttäjän käyttöpolkua:
-
Haku
- Suodatus
- Järjestely
- Lopulliset hakutulokset
<>
<div className="kds-w-3/4">
<Heading as="h3" mt={0}>
Asiakirjahaku
</Heading>
<Text>Hae asiakirjoja henkilötunnuksella</Text>
<TextInput
labelText="Henkilötunnus"
required
placeholder="010203-123AB"
addonAfter={<Button variant="primary">Hae</Button>}
className="kds-w-3/4 kds-pr-8"
/>
<Heading as="h4">Rajaa tuloksia</Heading>
<Row className="kds-w-3/4">
<InputGroup>
<InputLabel htmlFor="asiakirjan-tyyppi-1">Asiakirjan tyyppi</InputLabel>
<Select id="asiakirjan-tyyppi-1">
<option>(valitse)</option>
<option>PDF</option>
<option>Text</option>
<option>Excel</option>
</Select>
</InputGroup>
<InputGroup>
<InputLabel htmlFor="päätöksen-lopputulos-1">Päätöksen lopputulos</InputLabel>
<Select id="päätöksen-lopputulos-1">
<option>(valitse)</option>
<option>Käsittelyssä</option>
<option>Hyväksytty</option>
<option>Hylätty</option>
</Select>
</InputGroup>
</Row>
<Heading as="h4">Hakutulokset (3)</Heading>
<InputGroup className="kds-w-3/4 kds-pr-8">
<InputLabel htmlFor="järjestä-1">Järjestä</InputLabel>
<Select id="järjestä-1" defaultValue={1}>
<option>(valitse)</option>
<option>Uusin ensin</option>
<option>Vanhin ensin</option>
<option>Suosituin ensin</option>
</Select>
</InputGroup>
</div>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 1
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">1.4.2018</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 2
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">12.6.2020</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 3
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">11.12.2024</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
</>
Asettelu
Halutulosten suodatukseen, järjestelyyn ja lajitteluun käytettävät toiminnot tulee sijoittaa käyttöliittymässä hakutulosten läheisyyteen.
Yleensä toiminnot esitetään hakutulosten yläpuolella, jolloin suodatukseen käytettävät elementit pysyvät samassa paikassa, vaikka tulosten määrä muuttuisi.
Joissain tapauksissa (esim. asiantuntijakäyttöliittymät) suodatustoiminnot voidaan sijoittaa tulosten viereen. Rinnakkainen asettelu ei kuitenkaan sovellu kapeimmille näyttöleveyksille.
Jos suodatusvaihtoja on paljon, mobiilinäkymässä suodatus kannattaa esittää erikseen sisällön päälle avautuvassa näkymässä (overlay).
const overlayStyles = {
position: "absolute",
top: 5,
right: 5,
height: "100%",
width: "60%",
backgroundColor: "white",
boxShadow: "5px 0 5px rgba(0,0,0,0.5)",
padding: "20px",
zIndex: 1000,
display: "flex",
flexDirection: "column",
};
const containerStyles = {
position: "relative",
overflow: "hidden",
padding: "32px",
minHeight: "35rem",
};
const backgroundOverlayStyles = {
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 999,
};
const SearchResults = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleOverlay = () => {
setIsOpen(!isOpen);
};
const [data] = useState([
{
name: "Hakutulos 1",
category: "Päätös",
date: "1.12.2019",
info: "Hakemuksesi on hylätty.",
status: "denied",
},
{
name: "Hakutulos 2",
category: "Hakemus",
date: "13.4.2024",
info: "Hakemuksesi on käsittelyssä.",
status: "active",
},
]);
const [filters, setFilters] = useState({
active: false,
denied: false,
approved: false,
resolution: false,
application: false,
});
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setFilters((prevFilters) => ({
...prevFilters,
[name]: checked,
}));
};
const closeButtonRef = useRef();
const contentRef = useRef();
const filterResultsButtonRef = useRef();
const [results, setResults] = useState(data);
const filterResults = () => {
return data.filter((item) => {
const noCategoryIsSelected = !filters.resolution && !filters.application;
const noFilterIsSelected = !filters.active && !filters.denied && !filters.approved;
if (noCategoryIsSelected && noFilterIsSelected) return true;
const categoryMatch =
(filters.resolution && item.category === "Päätös") || (filters.application && item.category === "Hakemus");
if (categoryMatch && (filters.resolution || filters.application) && noFilterIsSelected) return true;
const statusMatch =
(filters.active && item.status === "active") ||
(filters.denied && item.status === "denied") ||
(filters.approved && item.status === "approved");
if (noCategoryIsSelected) return statusMatch;
return categoryMatch && statusMatch;
});
};
useEffect(() => {
if (isOpen) closeButtonRef.current.focus();
}, [isOpen]);
useOnClickOutside(contentRef, (e) => {
if (filterResultsButtonRef.current === e.target) {
return;
}
setIsOpen(false);
});
return (
<div style={containerStyles}>
<Heading as="h4" className="kds-mt-0">
Hakutulokset ({results.length})
</Heading>
<Button
variant="primary"
appearance="outline"
className="kds-mb-6"
ref={filterResultsButtonRef}
onClick={() => {
toggleOverlay();
}}
>
Rajaa tuloksia
</Button>
<div>
{results.map((result, index) => (
<Box p={0} key={index}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
{result.name}
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">{result.date}</DescriptionListItem>
<DescriptionListItem label="Lisätieto">{result.info}</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
))}
</div>
{isOpen && <div style={backgroundOverlayStyles}></div>}
{isOpen && (
<div style={overlayStyles} ref={contentRef}>
<div>
<Button
className="kds-p-2 kds-mr-12"
ref={closeButtonRef}
appearance="outline"
aria-label="Sulje"
onClick={() => {
toggleOverlay();
filterResultsButtonRef.current.focus();
}}
>
<IconClose />
</Button>
<Heading as="h2">Rajaa tuloksia</Heading>
<div className="kds-h-3/4">
<SelectionGroup labelText="Asiakirjatyyppi">
<Checkbox
labelText="Päätös (1)"
checked={filters.resolution}
name="resolution"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hakemus (1)"
checked={filters.application}
name="application"
onChange={handleCheckboxChange}
/>
</SelectionGroup>
<SelectionGroup labelText="Asiakirjan tila" className="kds-mt-4">
<Checkbox
labelText="Käsittelyssä (1)"
checked={filters.active}
name="active"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hyväksytty (0)"
checked={filters.approved}
name="approved"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hylätty (1)"
checked={filters.denied}
name="denied"
onChange={handleCheckboxChange}
/>
</SelectionGroup>
</div>
<ButtonGroup horizontal>
<Button
variant="primary"
className="kds-mt-6"
onClick={() => {
toggleOverlay();
setResults(filterResults());
filterResultsButtonRef.current.focus();
}}
>
Suodata ({results.length})
</Button>
<Button
variant="primary"
appearance="outline"
className="kds-mt-6 kds-mr-16"
onClick={() =>
setFilters({
active: false,
denied: false,
approved: false,
Päätös: false,
Hakemus: false,
})
}
>
Tyhjennä
</Button>
</ButtonGroup>
</div>
</div>
)}
</div>
);
};
render(<SearchResults />);
Otsikointi
Kelassa ja Kanta-palveluissa suodatuksen otsikointiin käytetään yleensä “rajaa”-termiä (esim. “Rajaa hakua”).
<SelectionGroup labelText="Rajaa hakua">
<Checkbox labelText="Sivut" />
<Checkbox labelText="Tiedostot" />
<Checkbox labelText="Tiedotteet ja ajankohtaiset" defaultChecked />
<Checkbox labelText="Lomakkeet" />
</SelectionGroup>
Huomioi, että kenttien otsikot näkyvät käyttäjälle kaikissa tilanteissa.
Select-komponentti vaatii aina erillisen otsikon (label).
const items = [
{ value: "Rokotukset", label: "Rokotukset" },
{ value: "Yleislääketiede", label: "Yleislääketiede" },
{ value: "Lorem", label: "Lorem ipsum" },
{ value: "Ipsum", label: "Lorem ipsum" },
];
const MultiselectExample = () => {
const [selectedItems, setSelectedItems] = useState([items[0], items[1]]);
const removeItem = (value) => {
setSelectedItems((prevSelectedItems) => prevSelectedItems.filter((item) => item.value !== value));
};
return (
<>
<Multiselect
items={items}
labelText="Rajaa aiheen mukaan"
className="kds-mb-2"
selectedItems={selectedItems}
onSelectedItemsChange={(items) => {
setSelectedItems(items);
}}
/>
{selectedItems.map((item, index) => (
<Chip
key={`${item.value}-${index}`}
label={item.label}
onDelete={() => removeItem(item.value)}
className="kds-mb-2 kds-mr-2"
deleteButtonAriaLabel={`Poista ${item.label}`}
/>
))}
</>
);
};
render(<MultiselectExample />);
Multiselect-komponentin filter-variantissa otsikko näkyy kentän sisällä valituista suodattimista riippumatta.
const items = [
{ value: "Migreeni", label: "Migreeni" },
{ value: "Vatsataudit", label: "Vatsataudit" },
{ value: "Lihaskipu", label: "Lihaskipu" },
{ value: "Suolistotaudit", label: "Suolistotaudit" },
];
const MultiselectExample = () => {
const [selectedItems, setSelectedItems] = useState([items[0], items[1]]);
const removeItem = (value) => {
setSelectedItems((prevSelectedItems) => prevSelectedItems.filter((item) => item.value !== value));
};
return (
<>
<Multiselect
items={items}
labelText="Rajaa aiheen mukaan mukaan"
variant="filter"
selectedItems={selectedItems}
className="kds-mt-6 kds-mb-2"
onSelectedItemsChange={(items) => {
setSelectedItems(items);
}}
/>
{selectedItems.map((item, index) => (
<Chip
key={`${item.value}-${index}`}
label={item.label}
onDelete={() => removeItem(item.value)}
className="kds-mb-2 kds-mr-2"
deleteButtonAriaLabel={`Poista ${item.label}`}
/>
))}
</>
);
};
render(<MultiselectExample />);
Suodatusehdot
Suodatusehtojen määrittely riippuu aina suodatettavan datan ominaisuuksista ja käyttäjän tarpeista. Selvitä mitkä suodattimet ovat käyttäjän kannalta olennaisia. Jos suodattimia on liikaa, se voi hämmentää käyttäjää.
Rajoita suodattimien määrä vain käyttäjän kannalta olennaisimpiin.
<>
<Heading mt={0} as="h3">
Rajaa hakutuloksia
</Heading>
<Row>
<InputGroup>
<InputLabel htmlFor="asiakirjan-tyyppi-2">Asiakirjan tyyppi</InputLabel>
<Select id="asiakirjan-tyyppi-2">
<option>(valitse)</option>
<option>PDF</option>
<option>Text</option>
<option>Excel</option>
</Select>
</InputGroup>
<InputGroup>
<InputLabel htmlFor="päätöksen-lopputulos-2">Päätöksen lopputulos</InputLabel>
<Select id="päätöksen-lopputulos-2">
<option>(valitse)</option>
<option>Käsittelyssä</option>
<option>Hyväksytty</option>
<option>Hylätty</option>
</Select>
</InputGroup>
</Row>
</>
Tulosten määrän ilmoittaminen
Tulosten määrä on hyvä työkalu suodatuksen tilan esittämiseksi. Tulosten määrä on hyvä ilmoittaa mahdollisimman lähellä haun ja suodatuksen elementtejä.
Käyttäjälle on olennaista, kuinka suodatusehto vaikuttaa tulosten määrään. Jos suodatukseen on käytössä monia eri kategorioita, käyttäjälle tulee näyttää yksittäisten suodatusvaihtoehdon vaikutus tulosten määrään.
Usein käytetty tapa tulosten kokonaismäärän esittämiseen on ilmaista määrä hakutulosten otsikossa. Tällöin käyttäjälle tulee myös indikoida erikseen, miten käytetty suodatus vaikuttaa hakutulosten määrään.
<div className="kds-w-4/4">
<Heading as="h4" mt={0}>
Hakutulokset (34)
</Heading>
<SelectionGroup labelText="Asiakirjan tila">
<div className="kds-flex kds-mb-4">
<Radio name="asiakirjan-tila-1" labelText="Kaikki (34)" className="kds-mt-2 kds-mr-4" />
<Radio name="asiakirjan-tila-1" labelText="Käsittelyssä (12)" defaultChecked className="kds-mr-4" />
<Radio name="asiakirjan-tila-1" labelText="Hylätty (22)" />
</div>
</SelectionGroup>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 1
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">1.4.2018</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 2
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">12.6.2020</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 3
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">11.12.2024</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
</div>
Määrän ilmoittaminen taulukoissa
Taulukoissa suodatustulosten määrä näytetään tyypillisesti myös sivutuksen (Pagination) yhteydessä, jos tuloksia on runsaasti.
Jos tulosten määrä näytetään sivutuksessa ja sivutus on asetettu tuloslistauksen alareunaan, varmista, että suodatettujen tulosten määrä näkyy selkeästi myös suodattimien lähellä sivun yläreunassa. Näin suodatuksen myötä muuttuneen tulosmäärän havaitseminen ei edellytä siirtymistä taulukon tai sivun alareunaan.
<>
<Heading as="h4" mt={0}>
Hakutulokset (34)
</Heading>
<SelectionGroup labelText="Asiakirjan tila">
<div className="kds-flex kds-mb-4">
<Radio name="asiakirjan-tila-2" labelText="Kaikki (34)" className="kds-mt-2 kds-mr-4" />
<Radio name="asiakirjan-tila-2" labelText="Käsittelyssä (12)" className="kds-mr-4" defaultChecked />
<Radio name="asiakirjan-tila-2" labelText="Hylätty (22)" />
</div>
</SelectionGroup>
<Table>
<TableHead>
<TableHeadRow>
<TableHeader>Lorem</TableHeader>
<TableHeader>Ipsum</TableHeader>
</TableHeadRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Dolor</TableCell>
<TableCell>Sit amet</TableCell>
</TableRow>
<TableRow>
<TableCell>Consectetur</TableCell>
<TableCell>Adipiscing elit</TableCell>
</TableRow>
<TableRow>
<TableCell>Consectetur</TableCell>
<TableCell>Adipiscing elit</TableCell>
</TableRow>
<TableRow>
<TableCell>Consectetur</TableCell>
<TableCell>Adipiscing elit</TableCell>
</TableRow>
<TableRow>
<TableCell>Consectetur</TableCell>
<TableCell>Adipiscing elit</TableCell>
</TableRow>
</TableBody>
</Table>
<Pagination aria-label="Sivutus">
<PaginationGroup>
<PaginationButton previous disabled aria-label="Edellinen sivu" />
<PaginationButton active>1</PaginationButton>
<PaginationButton>2</PaginationButton>
<PaginationButton next aria-label="Seuraava sivu" />
</PaginationGroup>
<PaginationGroup>
<PaginationText>Rivit 1-12, 12 tulosta</PaginationText>
</PaginationGroup>
<PaginationGroup>
<Dropdown isOpen={false}>
<DropdownToggle as={PaginationButton} caret>
5/sivu
</DropdownToggle>
<DropdownMenu>
<DropdownMenuItem active as="button">
50/sivu
</DropdownMenuItem>
<DropdownMenuItem as="button">100/sivu</DropdownMenuItem>
<DropdownMenuItem as="button">200/sivu</DropdownMenuItem>
<DropdownMenuItem divider />
<DropdownMenuItem as="button">Näytä kaikki</DropdownMenuItem>
</DropdownMenu>
</Dropdown>
</PaginationGroup>
</Pagination>
</>
Suodatusvaihtoehtojen esittäminen
Vähän suodatusvaihtoehtoja
Jos hakutulosten suodatukseen on tarjolla ainoastaan kaksi tai kolme vaihtoehtoa, käytä ensisijaisesti komponentteja, joiden avulla käyttäjä voi havaita nopeasti kaikki suodatusvaihtoehdot (Checkbox ja Radio).
<>
<SelectionGroup labelText="Asiakirjan tila">
<div className="kds-flex kds-mb-4">
<Radio name="asiakirjan-tila-3" labelText="Kaikki (34)" className="kds-mt-2 kds-mr-4" />
<Radio name="asiakirjan-tila-3" labelText="Käsittelyssä (12)" className="kds-mr-4" defaultChecked />
<Radio name="asiakirjan-tila-3" labelText="Hylätty (22)" />
</div>
</SelectionGroup>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 1
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">1.4.2018</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 2
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">12.6.2020</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 3
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">11.12.2024</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
</>
Paljon suodatusvaihtoehtoja
Pääsääntöisesti useamman kuin kolmen suodatusehdon näyttämiseen kannattaa soveltaa vaihtoehtojen esittämistä allekkain valintalistana (Select tai Multiselect).
const Example = () => {
const items = [
{ value: "vammaistuki", label: "16 vuotta täyttäneen vammaistuki" },
{ value: "adoptiotuki", label: "Adoptiotuki" },
{ value: "elatustuki", label: "Elatustuki" },
{ value: "elake_hoitotuki", label: "Eläkettä saavan hoitotuki" },
{ value: "erityishoito", label: "Erityishoitoraha" },
{ value: "lisakuukausi", label: "Lisäkuukauden siirtämisestä ilmoittaminen" },
{ value: "kuntoutus", label: "Kuntoutusraha" },
{ value: "lapsi", label: "Lapsilisä" },
{ value: "lastenhoito", label: "Lastenhoidon tuet" },
{ value: "matka", label: "Matkakorvaus" },
{ value: "opinto", label: "Opintotuki" },
{ value: "opinto_tulovalvonta", label: "Opintotuen tulovalvonnan uudelleenkäsittelypyyntö" },
{ value: "osasairaus", label: "Osasairauspäiväraha" },
{ value: "peruspaiva", label: "Peruspäiväraha" },
{ value: "sairauspaiva", label: "Sairauspäiväraha" },
{ value: "sotilas", label: "Sotilasavustus" },
{ value: "tyomarkkina", label: "Työmarkkinatuki" },
{ value: "tyottomyysaika", label: "Työttömyysajan ilmoitus" },
{ value: "asumis", label: "Yleinen asumistuki" },
{ value: "vanhempain", label: "Vanhempainpäivärahat" },
{ value: "aitiys", label: "Äitiysavustus" },
];
return (
<div className="kds-w-2/4">
<Multiselect
className="kds-mb-2"
labelText="Etuudet"
initialSelectedItems={[
items[0],
items[1],
items[2],
items[3],
items[4],
items[5],
items[6],
items[7],
items[8],
items[9],
items[10],
items[11],
items[12],
]}
items={items}
/>
</div>
);
};
render(<Example />);
Ehtojen ryhmittely
Haku- ja suodatustoimintojen tulee erottua riittävän selkeästi toisistaan.
Suodatus kohdistuu aina hakutoiminnon tuottamiin tuloksiin. Pyri ryhmittelemään toiminnot siten, että suodatus esitetään käyttäjälle vasta haun jälkeen.
<>
<Heading as="h3" mt={0}>
Asiakirjahaku
</Heading>
<Text>Hae asiakirjoja henkilötunnuksella</Text>
<TextInput
labelText="Henkilötunnus"
required
placeholder="010203-123AB"
className="kds-w-2/4"
addonAfter={<Button variant="primary">Hae</Button>}
/>
<Box className="kds-mt-12">
<Heading as="h4" className="kds-mt-0">
Rajaa tuloksia
</Heading>
<SelectionGroup labelText="Asiakirjan tila">
<Radio name="asiakirjan-tila-4" labelText="Kaikki (34)" />
<Radio name="asiakirjan-tila-4" labelText="Käsittelyssä (12)" defaultChecked />
<Radio name="asiakirjan-tila-4" labelText="Hylätty (22)" />
</SelectionGroup>
</Box>
<Heading as="h4">Hakutulokset (12)</Heading>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 1
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">1.4.2018</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 2
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">12.6.2020</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
<Box p={0}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
Hakutulos 3
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">11.12.2024</DescriptionListItem>
<DescriptionListItem label="Lisätieto">
"Lorem ipsum dolor sit amet, consectetur adipiscing elit"
</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
</>
Kun kyseessä on useamman suodattimen muodostama kokonaisuus, suodatusvaihtoehdot tulee ryhmitellä käyttöliittymässä yhtenäiseksi suodatusehtojen kokonaisuudeksi esimerkiksi harmaan taustasävyn tai muun rajaavan elementin avulla.
<>
<Heading as="h3" mt={0}>
Rajaa hakutuloksia
</Heading>
<InputLabel htmlFor="päätöksen-tila-1">Päätöksen tila</InputLabel>
<div className="kds-w-2/4">
<Select id="päätöksen-tila-1" className="kds-mb-6">
<option>Hyväksytty</option>
<option>Hylätty</option>
<option>Keskeneräinen</option>
</Select>
</div>
<DateRangePickerV2 labelText="Päätöksen päivämäärä" defaultValue={{ from: new Date(2023, 9, 2), to: "2023-10-27" }} />
<div className="kds-w-2/4">
<Multiselect
className="kds-mb-2"
labelText="Asiakirjan tyyppi"
initialSelectedItems={[
{ value: "Lomake", label: "Lomake" },
{ value: "Ilmoitus", label: "Ilmoitus" },
{ value: "Todustus", label: "Todistus" },
]}
items={[
{ value: "Luonnos", label: "Luonnos" },
{ value: "Päätös", label: "Päätös" },
{ value: "Lomake", label: "Lomake" },
{ value: "Ilmoitus", label: "Ilmoitus" },
{ value: "Todustus", label: "Todistus" },
]}
/>
</div>
<div className="kds-flex">
<Chip
label="Lomake"
deleteButtonAriaLabel="Poista valinta: Lomake"
onDelete={() => console.log("delete")}
className="kds-mb-2 kds-mr-2"
/>
<Chip
label="Ilmoitus"
deleteButtonAriaLabel="Poista valinta: Ilmoitus"
onDelete={() => console.log("delete")}
className="kds-mb-2 kds-mr-2"
/>
<Chip
label="Todistus"
deleteButtonAriaLabel="Poista valinta: Todistus"
onDelete={() => console.log("delete")}
className="kds-mb-2 kds-mr-2"
/>
</div>
</>
Ehtojen piilottaminen
Jos vain pieni osa käyttäjistä tarvitsee palvelussa tiettyjä suodatuskriteerejä, vähemmän käytetyt vaihtoehdot voi piilottaa pudotusvalikon alle (Accordion tai Collapse).
const Example = () => {
const fromRef = useRef(null);
const toRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const renderInputs = ({ FromInput, ToInput, fromInputProps, toInputProps, calendarButton }) => {
return (
<>
<div className="kds-flex kds-self-end" style={{ gap: "0.5rem" }}>
<span>
<InputLabel htmlFor={fromInputProps.id}>Alkamispäivä</InputLabel>
<FromInput {...fromInputProps} ref={fromRef} />
</span>
<span className="kds-flex kds-self-end kds-mb-2"> - </span>
<span>
<InputLabel htmlFor={toInputProps.id}>Päättymispäivä</InputLabel>
<ToInput {...toInputProps} ref={toRef} />
</span>
<span className="kds-flex kds-self-end">{calendarButton}</span>
</div>
</>
);
};
return (
<>
<Heading as="h3" className="kds-mt-0">
Rajaa lääkemääräyksiä
</Heading>
<DateRangePickerV2
renderInputs={renderInputs}
defaultValue={{ from: new Date(2023, 9, 2), to: "2023-10-27" }}
labelText="Lääkemääräyksen päivämäärä"
/>
<Accordion isOpen={isOpen} id="lääkemääräykset-1" appearance="link" className="kds-mb-2" reversed>
<AccordionToggle onClick={() => setIsOpen(!isOpen)}>Lisää rajausehtoja</AccordionToggle>
<AccordionBody>
<SelectionGroup labelText="Saatavuus">
<Radio name="saatavuus" labelText="Saatavilla" />
<Radio name="saatavuus" labelText="Ei saatavilla" />
</SelectionGroup>
</AccordionBody>
</Accordion>
</>
);
};
render(<Example />);
Aktiiviset suodatusehdot
Näytä selkeästi kulloinkin aktiivisena olevat suodatusvaihtoehdot ja varmista, että käyttäjä voi halutessaan helposti myös muuttaa valittuja suodatusehtoja.
const items = [
{ value: "Työmarkkinatuki", label: "Työmarkkinatuki" },
{
value: "Opintotuen tulovalvonnan uudelleenkäsittelypyyntö",
label: "Opintotuen tulovalvonnan uudelleenkäsittelypyyntö",
},
{ value: "Sairauspäiväraha", label: "Sairauspäiväraha" },
{ value: "Matkakorvaus", label: "Peruspäiväraha" },
{ value: "erityishoito", label: "Erityishoitoraha" },
];
const MultiselectExample = () => {
const [selectedItems, setSelectedItems] = useState([items[0], items[2], items[3], items[4]]);
const removeItem = (value) => {
setSelectedItems((prevSelectedItems) => prevSelectedItems.filter((item) => item.value !== value));
};
return (
<SelectionGroup labelText="Rajaa hakua">
<Checkbox labelText="Sivut" />
<Checkbox labelText="Lomakkeet" defaultChecked />
<Checkbox labelText="Tiedotteet ja ajankohtaiset" defaultChecked />
<Checkbox labelText="Tiedostot" />
</SelectionGroup>
);
};
render(<MultiselectExample />);
Jos eri suodatinvaihtoehtoja on runsaasti, käyttäjälle voi olla perusteltua tarjota myös “Tyhjennä valinnat”-toiminnallisuus. Jos suodatuskategorioita on monta, tyhjennystoiminnon voi esittää esimerkiksi toisen tai kolmannen tason painikkeena.
Myös aktiivisten suodatusehtojen esittämisessä kannattaa soveltaa allekkaista asettelua. Esimerkiksi Chip-komponentin avulla esitettyjen aktiivisten ehtojen silmäiltävyys kärsii, kun elementit rivittyvät epäsäännöllisesti.
const items = [
{ value: "vammaistuki", label: "16 vuotta täyttäneen vammaistuki" },
{ value: "adoptiotuki", label: "Adoptiotuki" },
{ value: "elatustuki", label: "Elatustuki" },
{ value: "elake_hoitotuki", label: "Eläkettä saavan hoitotuki" },
{ value: "erityishoito", label: "Erityishoitoraha" },
{ value: "lisakuukausi", label: "Lisäkuukauden siirtämisestä ilmoittaminen" },
{ value: "kuntoutus", label: "Kuntoutusraha" },
{ value: "lapsi", label: "Lapsilisä" },
{ value: "lastenhoito", label: "Lastenhoidon tuet" },
{ value: "matka", label: "Matkakorvaus" },
{ value: "opinto", label: "Opintotuki" },
{ value: "opinto_tulovalvonta", label: "Opintotuen tulovalvonnan uudelleenkäsittelypyyntö" },
{ value: "osasairaus", label: "Osasairauspäiväraha" },
{ value: "peruspaiva", label: "Peruspäiväraha" },
{ value: "sairauspaiva", label: "Sairauspäiväraha" },
{ value: "sotilas", label: "Sotilasavustus" },
{ value: "tyomarkkina", label: "Työmarkkinatuki" },
{ value: "tyottomyysaika", label: "Työttömyysajan ilmoitus" },
{ value: "asumis", label: "Yleinen asumistuki" },
{ value: "vanhempain", label: "Vanhempainpäivärahat" },
{ value: "aitiys", label: "Äitiysavustus" },
];
const MultiselectExample = () => {
const [selectedItems, setSelectedItems] = useState([items[0], items[1], items[2], items[3]]);
const removeItem = (value) => {
setSelectedItems((prevSelectedItems) => prevSelectedItems.filter((item) => item.value !== value));
};
return (
<div className="kds-w-2/4">
<Multiselect
items={items}
labelText="Etuudet"
variant="filter"
selectedItems={selectedItems}
onSelectedItemsChange={(items) => {
console.log("selected items", items);
setSelectedItems(items);
}}
/>
<div>
{selectedItems.map((item, index) => (
<Chip
key={`${item.value}-${index}`}
label={item.label}
onDelete={() => removeItem(item.value)}
className="kds-mb-2 kds-mr-2"
deleteButtonAriaLabel={`Poista ${item.label}`}
/>
))}
</div>
</div>
);
};
render(<MultiselectExample />);
Dynaaminen suodatus
Suodatukseen käytettävät toiminnot ovat tyypillisesti dynaamisia: käyttäjän ei tarvitse painaa erillistä painiketta suodatettujen hakutulosten päivittämiseksi. Tulosnäkymä päivittyy reaaliaikaisesti käyttäjän valitsemien suodatusehtojen mukaan.
Jos suodatus avautuu erilliseen overlay-näkymään pienellä näyttökoolla, suodatustoimintojen yhteyteen voi liittää ”Rajaa”-painikkeen. Painikkeen tarkoitus on viedä käyttäjä overlay-näkymästä takaisin tulosnäkymään.
const overlayStyles = {
position: "absolute",
top: 5,
right: 5,
height: "100%",
width: "60%",
backgroundColor: "white",
boxShadow: "5px 0 5px rgba(0,0,0,0.5)",
padding: SPACING_6,
zIndex: 1000,
display: "flex",
flexDirection: "column",
};
const containerStyles = {
position: "relative",
overflow: "hidden",
padding: SPACING_8,
minHeight: "35rem",
};
const backgroundOverlayStyles = {
position: "absolute",
top: 0,
left: 0,
height: "100%",
width: "100%",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 999,
};
const SearchResults = () => {
const [isOpen, setIsOpen] = useState(false);
const toggleOverlay = () => {
setIsOpen(!isOpen);
};
const [data] = useState([
{
name: "Hakutulos 1",
category: "Päätös",
date: "1.12.2019",
info: "Hakemuksesi on hylätty.",
status: "denied",
},
{
name: "Hakutulos 2",
category: "Hakemus",
date: "13.4.2024",
info: "Hakemuksesi on käsittelyssä.",
status: "active",
},
]);
const [filters, setFilters] = useState({
active: false,
denied: false,
approved: false,
resolution: false,
application: false,
});
const handleCheckboxChange = (e) => {
const { name, checked } = e.target;
setFilters((prevFilters) => ({
...prevFilters,
[name]: checked,
}));
};
const closeButtonRef = useRef();
const contentRef = useRef();
const filterResultsButtonRef = useRef();
const [results, setResults] = useState(data);
useEffect(() => {
setResults(filterResults());
}, [filters]);
const filterResults = () => {
return data.filter((item) => {
const noCategoryIsSelected = !filters.resolution && !filters.application;
const noFilterIsSelected = !filters.active && !filters.denied && !filters.approved;
if (noCategoryIsSelected && noFilterIsSelected) return true;
const categoryMatch =
(filters.resolution && item.category === "Päätös") || (filters.application && item.category === "Hakemus");
if (categoryMatch && (filters.resolution || filters.application) && noFilterIsSelected) return true;
const statusMatch =
(filters.active && item.status === "active") ||
(filters.denied && item.status === "denied") ||
(filters.approved && item.status === "approved");
if (noCategoryIsSelected) return statusMatch;
return categoryMatch && statusMatch;
});
};
useEffect(() => {
if (isOpen) closeButtonRef.current.focus();
}, [isOpen]);
useOnClickOutside(contentRef, (e) => {
if (filterResultsButtonRef.current === e.target) {
return;
}
setIsOpen(false);
});
return (
<div style={containerStyles}>
<Heading as="h4" mt={0}>
Hakutulokset ({results.length})
</Heading>
<Button
variant="primary"
appearance="outline"
className="kds-mb-6"
ref={filterResultsButtonRef}
onClick={() => {
toggleOverlay();
}}
>
Rajaa tuloksia
</Button>
<div>
{results.map((result, index) => (
<Box p={0} key={index}>
<Box variant="primary" appearance="solid" m={0}>
<Heading as="h5" size={5} mt={0} mb={2}>
{result.name}
</Heading>
</Box>
<Spacing base={4} sm={6}>
<DescriptionList stack>
<DescriptionListItem label="Päivämäärä">{result.date}</DescriptionListItem>
<DescriptionListItem label="Lisätieto">{result.info}</DescriptionListItem>
</DescriptionList>
</Spacing>
</Box>
))}
</div>
{isOpen && <div style={backgroundOverlayStyles}></div>}
{isOpen && (
<div style={overlayStyles} ref={contentRef}>
<div>
<Button
className="kds-p-2 kds-mr-12"
ref={closeButtonRef}
appearance="outline"
aria-label="Sulje"
onClick={() => {
toggleOverlay();
filterResultsButtonRef.current.focus();
}}
>
<IconClose />
</Button>
<Heading as="h2">Rajaa tuloksia</Heading>
<div className="kds-h-3/4">
<SelectionGroup labelText="Asiakirjatyyppi">
<Checkbox
labelText="Päätös (1)"
checked={filters.resolution}
name="resolution"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hakemus (1)"
checked={filters.application}
name="application"
onChange={handleCheckboxChange}
/>
</SelectionGroup>
<SelectionGroup labelText="Asiakirjan tila" className="kds-mt-4">
<Checkbox
labelText="Käsittelyssä (1)"
checked={filters.active}
name="active"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hyväksytty (0)"
checked={filters.approved}
name="approved"
onChange={handleCheckboxChange}
/>
<Checkbox
labelText="Hylätty (1)"
checked={filters.denied}
name="denied"
onChange={handleCheckboxChange}
/>
</SelectionGroup>
</div>
<ButtonGroup horizontal>
<Button
variant="primary"
className="kds-mt-6"
onClick={() => {
toggleOverlay();
setResults(filterResults());
filterResultsButtonRef.current.focus();
}}
>
Suodata ({results.length})
</Button>
<Button
variant="primary"
appearance="outline"
className="kds-mt-6 kds-mr-16"
onClick={() =>
setFilters({
active: false,
denied: false,
approved: false,
Päätös: false,
Hakemus: false,
})
}
>
Tyhjennä
</Button>
</ButtonGroup>
</div>
</div>
)}
</div>
);
};
render(<SearchResults />);
Tulosten ja ehtojen päivittyminen
Varmista, että dynaamisesti päivittyvä suodatusnäkymä säilyy käyttäjän kannalta hallittavana.
Jos tulokset päivittyvät automaattisesti käyttäjän vaihtaessa suodatusvaihtoehtoja, varmista että käyttöliittymän kokonaisnäkymän tila pysyy samanlaisena (esim. haitarielementit pysyvät avattuina, kenttiin täytetyt tiedot ennallaan).
Älä siirrä käyttäjää tuloslistaukseen automaattisesti (ei auto-scrollausta), kun yksittäinen suodatusvaihtoehto muuttuu. Jos käyttöliittymän näkymä liikkuu automaattisesti, käyttäjän on vaikeaa arvioida, miten tulokset ovat rajauksen myötä muuttuneet.