web: more a11y for search

This commit is contained in:
2026-05-25 23:25:51 +02:00
parent b5c9188488
commit 2c8f8a218c
2 changed files with 54 additions and 26 deletions
+12 -6
View File
@@ -1,10 +1,10 @@
import { ImageFragmentDefinition, stripHtml } from "@/lib/common";
import { formatDate, formatOccurrenceMonths } from "@/lib/date";
import { unmaskFragment } from "@/gql"; import { unmaskFragment } from "@/gql";
import type { ImageFragment, SearchQuery } from "@/gql/graphql"; import type { ImageFragment, SearchQuery } from "@/gql/graphql";
import styles from "./searchContainer.module.scss"; import { ImageFragmentDefinition, stripHtml } from "@/lib/common";
import { Image } from "../general/Image"; import { formatDate, formatOccurrenceMonths } from "@/lib/date";
import Link from "next/link"; import Link from "next/link";
import { Image } from "../general/Image";
import styles from "./searchContainer.module.scss";
export type SearchResult = SearchQuery["results"][number]; export type SearchResult = SearchQuery["results"][number];
@@ -78,12 +78,18 @@ function getResultSnippet(result: SupportedResult): string | null {
export function SearchResults({ results }: { results: SearchResult[] }) { export function SearchResults({ results }: { results: SearchResult[] }) {
if (!results.length) { if (!results.length) {
return <div className={styles.noResults}>Ingen resultater</div>; return (
<div className={styles.noResults} aria-live="polite">
Ingen resultater
</div>
);
} }
const supportedResults = results.filter(isSupported); const supportedResults = results.filter(isSupported);
return ( return (
<div> <div>
<p className={styles.resultsCounter}>{results.length} resultater</p> <p className={styles.resultsCounter} aria-live="polite">
{results.length} resultater
</p>
{supportedResults.map((result) => ( {supportedResults.map((result) => (
<ResultRow key={result.id} result={result} /> <ResultRow key={result.id} result={result} />
))} ))}
+42 -20
View File
@@ -1,11 +1,17 @@
"use client"; "use client";
import { useEffect, useRef, useState, useTransition, type ReactNode } from "react";
import { useDebouncedCallback } from "use-debounce";
import { useRouter } from "next/navigation";
import { PageHeader } from "../general/PageHeader";
import { getSearchPath } from "@/lib/common"; import { getSearchPath } from "@/lib/common";
import styles from "./searchContainer.module.scss"; import { useRouter } from "next/navigation";
import {
type ReactNode,
useEffect,
useRef,
useState,
useTransition,
} from "react";
import { useDebouncedCallback } from "use-debounce";
import { Icon } from "../general/Icon"; import { Icon } from "../general/Icon";
import { PageHeader } from "../general/PageHeader";
import styles from "./searchContainer.module.scss";
export function SearchShell({ export function SearchShell({
initialQuery, initialQuery,
@@ -35,22 +41,38 @@ export function SearchShell({
return ( return (
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<PageHeader heading="Søk" /> <PageHeader heading={initialQuery ? `Søk: «${initialQuery}»` : "Søk"} />
<div className={styles.searchField}> <form
<input action="/sok"
name="query" method="get"
type="text" onSubmit={(e) => {
autoFocus e.preventDefault();
value={inputValue} pushQuery.cancel();
onChange={(e) => { lastPushedRef.current = inputValue;
setInputValue(e.target.value); startTransition(() => {
pushQuery(e.target.value); replace(getSearchPath(inputValue));
}} });
/> }}
<div className={styles.searchIcon}> >
<Icon type="search" /> <div className={styles.searchField}>
<label htmlFor="search-query" className="sr-only">
Søk
</label>
<input
id="search-query"
name="q"
type="text"
value={inputValue}
onChange={(e) => {
setInputValue(e.target.value);
pushQuery(e.target.value);
}}
/>
<div className={styles.searchIcon} aria-hidden="true">
<Icon type="search" />
</div>
</div> </div>
</div> </form>
{children} {children}
</div> </div>
); );