Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/accordion-component.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@sb1/indeks-css': minor
'@sb1/indeks-react': minor
---

Ny komponent: Accordion

Accordion viser og skjuler innhold i seksjoner. Den bygger på native `<details>`/`<summary>`, så tastatur (Tab, Enter/Space) og skjermleser-semantikk (disclosure med åpen/lukket-tilstand) fungerer uten ekstra ARIA — og uten JavaScript.

- Ren CSS: ingen web-komponent. Seksjonene er uavhengige (flere kan stå åpne samtidig). Den myke åpne/lukke-animasjonen er en progressiv CSS-enhancement (`interpolate-size` + `::details-content`); nettlesere uten støtte hopper bare åpent/lukket, fortsatt fullt funksjonelt.
- React-laget er et tynt, compound-API: `Accordion` (en `<div class="ix-accordion">`) med `Accordion.Item` (→ `<details>`), `Accordion.Header` (→ `<summary>`) og `Accordion.Content`. `defaultOpen` på `Accordion.Item` speiler native `<details open>`.
- Fullt brukbar uten React via klassene `.ix-accordion`, `.ix-accordion__item`, `.ix-accordion__header` og `.ix-accordion__content` på native `<details>`/`<summary>`.
- Visuelle states (default, hover, active, focus, expanded) er definert; en chevron roterer for å vise tilstand, og animasjonen respekterer `prefers-reduced-motion`. Fokusring kun på header.
- Bevisst avvik fra akseptansekriteriene: header er `<summary>` framfor `<button aria-expanded aria-controls>`. Native disclosure oppfyller samme intensjon (fokuserbar knapp, Enter/Space, annonsert tilstand) uten eksplisitt aria-kobling, siden innholdet ligger inne i samme `<details>`. Dokumentert i tilgjengelighets-tabellen.
136 changes: 136 additions & 0 deletions docs/internal/dev-warnings-react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Plan: Flytt dev-advarsler fra web components til React-laget

## Kontekst

Flere komponenter har `if (import.meta.env.DEV) { console.warn(...) }`-blokker med
a11y-/bruksadvarsler (manglende label, `type="number"`, manglende `tooltip-label`,
manglende legend osv.). Spørsmålet var om de blir strippet fra publiserte pakker.

**Empirisk funn:** Ja — de strippes allerede i dag. `indeks-web/dist/npm/index.js`
har 0 `console.warn` og 0 `import.meta.env`, selv om npm-bygget *ikke* minifiseres
(`indeks-web/vite.config.ts:29`, `minify: isCdn` → `false`). Vite evaluerer
`import.meta.env.DEV` til `false` ved Indeks' egen build og tree-shaker bort `if (false)`.

**Men det avslører det egentlige problemet:** fordi `import.meta.env.DEV` bakes ved
*Indeks'* build, blir advarslene borte for **alle** konsumenter — også de som selv
kjører i dev. Advarslene lever i praksis kun når man kjører kildekoden internt
(Storybook / eksempel-app). Ønsket er at advarslene skal nå konsumenter i *deres*
dev-modus og forsvinne i *deres* prod-build.

**Hvorfor ikke bare bytte til `process.env.NODE_ENV` i web components:** Web
components er dokumentert lastet som rå ESM rett fra CDN (`indeks-docs/.../utvikler.mdx`),
uten byggesteg hos konsument. Da finnes ikke `process` i nettleseren
(`ReferenceError: process is not defined`), og det finnes ikke noe konsument-byggesteg
som kan utsette dev/prod-valget. CDN er nettopp den primære måten web components brukes
på. Derfor er konsument-styrte advarsler **fysisk umulig** for CDN-kanalen.

**Valgt strategi:** Flytt advarslene til `@sb1/indeks-react`. React-pakken
distribueres **kun via npm** (`indeks-react/package.json` — ingen CDN-build), så den
bundles alltid av konsumenten. Da kan vi bruke rammeverk-økosystemets standard-
mønster `process.env.NODE_ENV !== 'production'`, som konsumentens bundler (Vite/webpack/
Next) bytter ut → advarsler følger med i konsumentens dev og strippes i deres prod.
`@types/node` ligger allerede i `indeks-react` (`package.json:65`), så det opprinnelige
TypeScript-bruddet (commit `278544a`) er ikke lenger relevant.

## Mål

1. React-konsumenter får a11y-/bruksadvarsler i sin dev-build, automatisk strippet i prod.
2. Web components beholder ikke dupliserte advarsler som likevel er døde i publisert kode.
3. SSR-trygt (Next.js o.l.): ingen krasj på server eller i nettleser.

## Steg

### 1. Dev-warn-helper i indeks-react
Opprett `indeks-react/lib/ui/utils/devWarn.ts` (eller tilsvarende eksisterende
utils-mappe — sjekk `indeks-react/lib/ui/` for konvensjon):

```ts
// SSR-trygt: process kan mangle i enkelte runtimes; NODE_ENV kan være undefined.
export function devWarn(message: string): void {
if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
console.warn(message);
}
}
```

Begrunnelse for `typeof process`-guarden: ekstra robusthet hvis pakken havner i en
runtime uten `process` (edge/worker). Konsumentens bundler erstatter
`process.env.NODE_ENV` med en streng-literal, så `if (... !== 'production')` blir
`if (true/false)` og strippes i prod.

### 2. Legg advarsler i React-komponentene som eier de relevante propene
React-laget mottar verdiene som props, så sjekkene blir enklere og mer presise enn
DOM-inspeksjonen web component gjør i dag:

- **Manglende tilgjengelig navn** → `TextField.tsx`, `TextArea.tsx`
(`indeks-react/lib/ui/components/form/text-field|text-area/`): advar når verken
`label` eller `ariaLabel` er satt. Dette gjenoppretter advarselen som ble fjernet i
commit `278544a`, nå via `devWarn`.
- **`tooltip` uten `tooltipLabel`** → `Field.tsx`
(`indeks-react/lib/ui/components/form/field/`): advar når `tooltip` er satt men
`tooltipLabel` mangler.
- **Message uten MessageRegion** → `Message.tsx` (`.../message/Message.tsx:83`):
bytt `import.meta.env.DEV` → `devWarn(...)`. Dette er allerede i React-laget.
- **RadioGroup** (`.../radio-group/`): vurder advarsel for manglende legend dersom
React-laget har tilstrekkelig informasjon via props; hvis sjekken krever DOM-/children-
inspeksjon som bare web component har, hopp over (se "Avgrensning").

Bruk samme ordlyd som dagens web-advarsler der det er overlapp (i18n-vennlig:
meldingene er utvikler-rettede konsoll-tekster, ikke bruker-tekst).

### 3. Rydd i web components
`import.meta.env.DEV`-advarslene i `IxField.ts` (linjene 158, 185, 345) og
`IxRadioGroup.ts` (linjene 121, 225) er døde i publisert kode. Anbefaling:
- **Behold** dem som er ene og alene mulige i DOM-laget (f.eks. "fant ingen
`<input type="radio">`", "fant ingen `<input>/<select>/<textarea>`") — de hjelper
intern utvikling og vanilla/CDN-brukere som kjører kildenært, og er gratis i prod.
- **Fjern** de som nå dekkes ekvivalent i React (label/tooltip-label) for å unngå at
to lag hevder å eie samme advarsel. Endelig fjern/behold avklares i implementasjon.

### 4. Tester
- `indeks-react` bruker vitest (`indeks-react/package.json`). Legg til/gjenopprett
tester som verifiserer at `devWarn` kalles ved manglende label (jf. testene som ble
fjernet i `278544a`: `TextField.test.tsx`, `TextArea.test.tsx`).
- Mock `console.warn` og sett `process.env.NODE_ENV` i testen.

## Kritiske filer

- `indeks-react/lib/ui/utils/devWarn.ts` (ny)
- `indeks-react/lib/ui/components/form/text-field/TextField.tsx`
- `indeks-react/lib/ui/components/form/text-area/TextArea.tsx`
- `indeks-react/lib/ui/components/form/field/Field.tsx`
- `indeks-react/lib/ui/components/message/Message.tsx`
- (web, opprydding) `indeks-web/lib/components/field/IxField.ts`,
`indeks-web/lib/components/radio-group/IxRadioGroup.ts`

## Verifisering (lynchpin — må gjøres empirisk)

Det avgjørende ukjente er om **Vite lib-build beholder `process.env.NODE_ENV`** for
konsumenten, eller baker det til `"production"` ved Indeks' build (Vite bygger i
production mode). Verifiser:

1. Bygg React-pakken: `pnpm --filter @sb1/indeks-react build`
2. Grep output: `grep -c "process.env.NODE_ENV" indeks-react/dist/main.js`
- **>0** → riktig: literalen står igjen, konsumentens bundler styrer dev/prod. ✅
- **0 / erstattet med `"production"`** → Vite bakte det. Fallback:
- Legg til i `indeks-react/vite.config.ts` en `define` som hindrer erstatning, f.eks.
`define: { 'process.env.NODE_ENV': 'process.env.NODE_ENV' }`, eller bruk
`globalThis.process?.env?.NODE_ENV` i helperen. Re-grep til literalen overlever.
3. Røyktest dev: i `indeks-eksempel` (Vite dev), rendre en `<TextField>` uten label →
se advarsel i konsollen. Bygg eksempel for prod → bekreft at advarselen er borte.
4. SSR-røyktest (om praktisk): bekreft ingen `process is not defined` ved import i en
server-runtime; helperens `typeof process`-guard dekker dette.
5. Kjør `pnpm --filter @sb1/indeks-react test`.

## Versjonering

Legg til changeset (ikke bump `package.json` manuelt) for `@sb1/indeks-react`
(minor — gjenoppretter/utvider dev-advarsler) og evt. `@sb1/indeks-web` (patch — fjernet
død kode). Se prosjektets changeset-konvensjon.

## Avgrensning

Advarsler som krever DOM-/children-inspeksjon som bare web component har tilgang til,
flyttes ikke til React hvis React-propene ikke gir nok informasjon. CDN-brukere av rene
web components får fortsatt ingen dev-advarsler — det er en akseptert konsekvens av at
CDN-kanalen ikke har et konsument-byggesteg.
158 changes: 158 additions & 0 deletions indeks-css/css/components/accordion.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* Accordion — gruppe av native <details>-seksjoner som vises/skjules.
*
* Accordion bygger på native <details>/<summary>. All semantikk og tastatur
* kommer gratis fra native elementer — ingen JavaScript er nødvendig. Seksjonene
* er uavhengige (flere kan stå åpne samtidig).
*
* Stylingen treffer <details>/<summary> direkte inni et element med klassen
* .ix-accordion, så seksjonene trenger ikke egne klasser for å virke. Klassene
* (.ix-accordion__item/__header/__content) finnes likevel som eksplisitte kroker
* for tilfeller der man vil style en struktur uten direkte <details>-barn.
*
* Struktur:
* <div class="ix-accordion">
* <details>
* <summary>Tittel</summary>
* <div>Innhold</div>
* </details>
* …
* </div>
*/

/* Container: vertikal stabling av seksjoner i et avrundet, kantet kort. */
:where(.ix-accordion) {
display: block;
border: var(--ix-border-width-default) solid var(--ix-color-border-main-default);
border-radius: var(--ix-border-radius-md);
/* Klipp barna til de avrundede hjørnene slik at hover-/active-flaten på
* summary ikke flyter ut over rammen i toppen/bunnen. */
overflow: hidden;
background-color: var(--ix-color-surface-main-default);
color: var(--ix-color-foreground-main-default);
/* Lar block-size animeres mellom 0 og `auto` på ::details-content (se
* @supports-blokken nederst). Ignoreres trygt der det ikke støttes. */
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
interpolate-size: allow-keywords;
}

/* Hver seksjon = <details>. Skillelinje mellom seksjoner (ikke over den første). */
:where(.ix-accordion) > details,
:where(.ix-accordion__item) {
border-top: var(--ix-border-width-default) solid var(--ix-color-border-main-default);
}

:where(.ix-accordion) > details:first-child,
:where(.ix-accordion__item:first-child) {
border-top: 0;
}

/* Header = <summary>: hele klikkflaten. Tittel (+ ev. prefiks-ikon) til venstre,
* chevron til høyre (margin-inline-start:auto). */
:where(.ix-accordion) summary,
:where(.ix-accordion__header) {
display: flex;
align-items: center;
gap: var(--ix-spacing-sm);
padding: var(--ix-spacing-md);
/* Touch-mål min. 44 px (WCAG 2.5.8) — sikres av padding + innhold. */
min-height: 44px;
font-size: var(--ix-font-size-md);
font-weight: var(--ix-font-weight-medium);
cursor: pointer;
/* Skjul native disclosure-marker (trekant) i alle nettlesere. */
list-style: none;
}

:where(.ix-accordion) summary::-webkit-details-marker,
:where(.ix-accordion__header)::-webkit-details-marker {
display: none;
}

/* Default/Hover/Active — tydelige states på klikkflaten. */
:where(.ix-accordion) summary:hover,
:where(.ix-accordion__header:hover) {
background-color: var(--ix-color-surface-main-hover);
}

:where(.ix-accordion) summary:active,
:where(.ix-accordion__header:active) {
background-color: var(--ix-color-surface-main-active);
}

/* Focus — synlig, konsistent ring (samme som resten av systemet). */
:where(.ix-accordion) summary:focus-visible,
:where(.ix-accordion__header:focus-visible) {
outline: var(--ix-outline-default);
outline-offset: calc(var(--ix-outline-offset-default) * -1);
}

/* Chevron som indikerer åpen/lukket tilstand. Flex-barn skjøvet helt til høyre. */
:where(.ix-accordion) summary::after,
:where(.ix-accordion__header)::after {
content: '';
flex: 0 0 auto;
margin-inline-start: auto;
width: var(--ix-font-size-xl);
height: var(--ix-font-size-xl);
background-color: var(--ix-color-foreground-main-default);
transition: var(--ix-transition-all);
/* stylelint-disable plugin/no-unsupported-browser-features */
mask-size: cover;
-webkit-mask-size: cover;
mask-position: center;
-webkit-mask-position: center;
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-image: url(https://cdn.sparebank1.no/icons/keyboard_arrow_down.svg);
-webkit-mask-image: url(https://cdn.sparebank1.no/icons/keyboard_arrow_down.svg);
/* stylelint-enable plugin/no-unsupported-browser-features */
}

/* Expanded: chevron peker opp. */
:where(.ix-accordion) > details[open] summary::after,
:where(.ix-accordion__item[open]) .ix-accordion__header::after {
transform: rotate(180deg);
}

/* Innholdsområde = alt i <details> som ikke er <summary>. Luft over (mellom header
* og innhold), på sidene og under — ingen skillelinje mot headeren. */
:where(.ix-accordion) > details > :not(summary),
:where(.ix-accordion__content) {
padding: var(--ix-spacing-md);
}

:where(.ix-accordion) > details > :not(summary) > :first-child,
:where(.ix-accordion__content) > :first-child {
margin-top: 0;
}

:where(.ix-accordion) > details > :not(summary) > :last-child,
:where(.ix-accordion__content) > :last-child {
margin-bottom: 0;
}

/* Myk åpne/lukke-animasjon som progressiv enhancement. Krever interpolate-size +
* ::details-content + transition-behavior:allow-discrete (Chrome/Edge 129+,
* Safari 17.2+). Nettlesere uten støtte hopper bare åpent/lukket — fortsatt fullt
* funksjonelt. */
/* stylelint-disable plugin/no-unsupported-browser-features */
@supports (interpolate-size: allow-keywords) {
:where(.ix-accordion) > details::details-content {
block-size: 0;
overflow: hidden;
transition:
block-size var(--ix-transition-duration) var(--ix-transition-animation),
content-visibility var(--ix-transition-duration) allow-discrete;
}

:where(.ix-accordion) > details[open]::details-content {
block-size: auto;
}

@media (prefers-reduced-motion: reduce) {
:where(.ix-accordion) > details::details-content {
transition: none;
}
}
}
/* stylelint-enable plugin/no-unsupported-browser-features */
1 change: 1 addition & 0 deletions indeks-css/css/components/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
@import './spinner.css';
@import './divider.css';
@import './list-element.css';
@import './accordion.css';
@import './message.css';
@import './form/index.css';
Loading
Loading