Migrering (React 18+)
Veiledning for konsumenter som oppgraderer @entur/*-pakker for React 18+
Alle @entur/*-pakker krever nå React 18.0.0 eller høyere som peer dependency. Dette er en breaking change.
Automatisk migrering med Claude Code
Designsystemet tilbyr en migreringsguide skrevet for AI-kodeagenter. Lim inn meldingen under i Claude Code – ingen installasjon eller oppsett kreves. Den fungerer også i andre agenter som kan hente URL-er, som GitHub Copilot og Cursor.
Oppgrader @entur/*-pakkene i dette prosjektet til siste major-versjon.
Les og følg denne migreringsguiden:
https://raw.githubusercontent.com/entur/design-system/main/skills/migrate-react-18/SKILL.md
Relative lenker i guiden (f.eks. references/breaking-changes.md) ligger under
https://raw.githubusercontent.com/entur/design-system/main/skills/migrate-react-18/Breaking changes
Peer dependencies
Alle pakker krever nå:
- react: >=18.0.0 (tidligere >=16.8.0)
- react-dom: >=18.0.0 (tidligere >=16.8.0)
ESM exports
Alle @entur/*-pakker har nå et strengt exports-felt i package.json. Dette gir bundlere korrekt ESM/CJS-oppløsning, men begrenser hvilke importstier som fungerer.
Hva har endret seg
- Hver pakke har nå "exports" med eksplisitte types-, import-, require- og default-betingelser.
- Deep imports til dist/ som ikke er eksplisitt listet, vil feile.
- CSS kan importeres via en ren ./styles-subpath.
Hva konsumenter må gjøre
Fjern custom alias/resolve-konfigurasjon – exports-feltet håndterer dette nativt.
Fiks dype dist/-imports:
// ❌ Før (vil feile)
import { Button } from '@entur/button/dist/Button';
// ✅ Etter
import { Button } from '@entur/button';CSS-imports – både gammel og ny sti fungerer:
// ✅ Ny (anbefalt)
import '@entur/button/styles';
// ✅ Fungerer fortsatt
import '@entur/button/dist/styles.css';@entur/tokens stil-imports:
// ✅ Ny (anbefalt)
@use '@entur/tokens/styles/base.scss' as *;
@use '@entur/tokens/styles/semantic.scss' as *;
// ✅ Fungerer fortsatt
@use '@entur/tokens/dist/base.scss' as *;
@use '@entur/tokens/dist/semantic.scss' as *;@entur/utils SCSS-imports:
// ✅ Ny (anbefalt)
@use '@entur/utils/styles/breakpoints' as breakpoint;
@use '@entur/utils/styles/color-utils' as util;
// ✅ Fungerer fortsatt
@use '@entur/utils/dist/breakpoints.scss' as breakpoint;
@use '@entur/utils/dist/color-utils.scss' as util;@entur/modal
Intern implementasjon er migrert fra @reach/dialog (utdatert) til det native HTML-elementet <dialog>. Dette fjerner alle tredjepartsavhengigheter for modal.
Hva har endret seg internt
- @reach/dialog → Nativt <dialog>-element med showModal() / close()
- Fokusfelle håndteres nå nativt av nettleseren (innebygd i <dialog>)
- Fokusrestaurering ved lukking håndteres nativt
- Bakgrunnsstyling bruker ::backdrop pseudo-element i stedet for en egen overlay-div
- Scroll-lås bruker html:has(dialog[open]) CSS i stedet for JavaScript
Breaking changes
- onDismiss er nå påkrevd på Modal. Native <dialog> tillater alltid lukking via Escape – onDismiss sikrer at parent-state holdes synkronisert.
- Ny showCloseButton-prop (default: true) – styrer om lukkeknappen vises øverst til høyre.
Hva konsumenter må gjøre
- Legg til onDismiss hvis det mangler – det er nå en påkrevd prop.
- CSS som målretter <div>-overlay – oppdater til å målrette <dialog> eller bruk klassen .eds-modal__overlay.
- Fjern referanser til data-reach-dialog-*-attributter.
Hva er bevart
- Modal med open, onDismiss, size, title, closeLabel, closeOnClickOutside, initialFocusRef, align
- ModalOverlay, ModalContent og Drawer – alle props bevart
- Fokusfelle, fokusrestaurering, Escape-lukking og klikk-utenfor fungerer som før
- Alle CSS-klassenavn (eds-modal__*, eds-drawer__*) er uendret
@entur/tab
Intern implementasjon er migrert fra @reach/tabs (utdatert) til en nativ ARIA-implementasjon uten tredjepartsavhengigheter.
Hva har endret seg internt
- @reach/tabs → Nativ ARIA-implementasjon (null avhengigheter)
- Tab-tilstand styres nå via React Context (TabsContext)
- ARIA-attributter (role, aria-selected, aria-controls, aria-labelledby) settes direkte
- Tastaturnavigasjon (ArrowLeft, ArrowRight, Home, End) er implementert nativt i TabList
Hva er bevart
- Tabs med index, defaultIndex, onChange, as (kontrollert og ukontrollert)
- TabList, Tab, TabPanel og TabPanels – alle props bevart
- Tastaturnavigasjon, ARIA-roller og alle CSS-klassenavn (eds-tabs, eds-tab, eds-tab-list osv.)
Hva konsumenter må gjøre
- Fjern data-reach-*-selektorer – erstatt med .eds-tab, .eds-tab-list, .eds-tab-panel, eller rolle-selektorer ([role="tab"] osv.).
- Fjern ikke-standard props – typer er nå strengere. Alle standard HTML-attributter fungerer fortsatt.
- Ikke hardkod genererte ID-er i tester – bruk aria-controls/aria-labelledby for å finne tilknyttede elementer.
- Render hver Tab fra TabList og hvert TabPanel fra TabPanels – indeksen kommer fra markupen, ikke fra rekkefølgen i DOM.
Indeks kommer fra markupen
TabList og TabPanels deler ut én indeks per barn, og panelet med indeks n hører til taben med indeks n. Med @reach/tabs registrerte hver tab og hvert panel seg selv og fikk indeksen fra rekkefølgen i DOM, så det hadde ingenting å si hvor dypt de lå i markupen.
Fragmenter, Suspense-grenser og wrapper-elementer forbruker ingen indeks selv, så de kan brukes fritt. Egne komponenter kan ikke inspiseres, og får derfor én indeks hver – flere paneler bak samme komponent deler indeks, og åpnes og lukkes samtidig. Ligger panelene bak en egen komponent, kan du enten løfte komponenten ut av TabPanels, eller sende inn en index-prop på hver Tab eller hvert TabPanel. Index-propen overstyrer indeksen forelderen deler ut, og virker uansett hvordan panelet er pakket inn.
// Fragmenter, Suspense og wrapper-elementer fungerer
<TabPanels>
<div className="mitt-oppsett">
<TabPanel>Én</TabPanel>
<TabPanel>To</TabPanel>
</div>
</TabPanels>
// Begge panelene får indeks 0 og vises samtidig
const MinePaneler = () => (
<>
<TabPanel>Én</TabPanel>
<TabPanel>To</TabPanel>
</>
);
<TabPanels>
<MinePaneler />
</TabPanels>
// Paneler bak en egen komponent får alle samme indeks.
// Løft komponenten ut av TabPanels …
<Wrapper>
<TabPanels>
<PanelEn />
<PanelTo />
</TabPanels>
</Wrapper>
// … eller send inn index på hvert panel
<TabPanels>
<Wrapper>
<PanelEn index={0} />
<PanelTo index={1} />
</Wrapper>
</TabPanels>
const PanelEn = ({ index }: { index?: number }) => (
<TabPanel index={index}>…</TabPanel>
);En egen komponent forbruker én indeks selv om den ikke rendrer et panel, så en overskriftskomponent blant panelene forskyver resten. Betingelser må stå på både taben og panelet – står de bare på én side, og ikke sist i listen, havner en tab sammen med feil panel. Bruk én TabList og én TabPanels per Tabs; en ekstra container nummererer fra 0 igjen, uten at noe advarer.
Tab utenfor TabList og TabPanel utenfor TabPanels faller tilbake til indeks 0 – med mindre de får en index-prop – og logger en advarsel i konsollen. TabList og TabPanels logger en feilmelding når flere barn deler indeks, og en advarsel når den valgte taben ikke har noe panel mens senere indekser er i bruk (et panel som fortsatt laster ser likt ut fra utsiden). Tabs logger en feilmelding når et panel har en indeks ingen tab kan nå. Alt logges kun i utviklingsmodus – aldri i produksjon.
Nye funksjoner
- keepMounted-prop på TabPanels – beholder alle paneler i DOM med hidden-attributtet.
- SSR-kompatibel – useId() produserer stabile ID-er mellom server og klient.
- aria-label og aria-labelledby er nå eksplisitt typet på TabList.
@entur/expand
Intern implementasjon er migrert fra react-collapse (ikke vedlikeholdt siden 2021) til en CSS grid-animasjon uten avhengigheter.
Breaking changes – innhold forblir i DOM når det er lukket
Tidligere ble lukket innhold fjernet fra DOM. Nå forblir lukket innhold i DOM, men skjules med aria-hidden="true" og inert. For å gjenopprette gammel oppførsel:
// Gammel oppførsel: innhold fjernes fra DOM når det lukkes
<ExpandablePanel unmountOnClose={true} title="...">
{children}
</ExpandablePanel>Dette påvirker også SideNavigationGroup i @entur/menu.
Nye props
- ExpandablePanel: open, onToggle (kontrollert modus), unmountOnClose
- ExpandableText: unmountOnClose
- AccordionItem: unmountOnClose
- Accordion: openId, onToggle, defaultOpenId (kontrollert modus)
- BaseExpand: unmountOnClose
Alle expand-komponenter støtter nå ref-forwarding via React.forwardRef.
Hva konsumenter må gjøre
- De fleste konsumenter trenger ingen endringer – ny standardoppførsel er bedre for tilgjengelighet og ytelse
- Hvis du er avhengig av at lukket innhold fjernes fra DOM, legg til unmountOnClose={true}
- For kontrollert accordion-oppførsel kan du nå bruke openId og onToggle på Accordion
@entur/layout
LayoutWrapper fjernet
LayoutWrapper er fjernet. Bruk Grid fra @entur/layout/beta direkte:
// ❌ Før
import { LayoutWrapper } from '@entur/layout';
<LayoutWrapper>{children}</LayoutWrapper>
// ✅ Etter
import { Grid } from '@entur/layout/beta';
<Grid
templateColumns={{
base: 'repeat(4, 1fr)',
m: 'repeat(8, 1fr)',
lg: 'repeat(12, 1fr)',
}}
columnGap={{ base: 's-m', m: 'm-l' }}
>
{children}
</Grid>Responsive breakpoint-nøkler endret
Beta Grid bruker nye responsive breakpoint-nøkler:
- sm → base
- md → m
- lg → lg (uendret)
- xl → xl (uendret)
// ❌ Før
<Grid.Item colSpan={{ sm: '1 / -1', md: '1 / -1', lg: '3 / -3' }}>
// ✅ Etter
<Grid.Item colSpan={{ base: '1 / -1', m: '1 / -1', lg: '3 / -3' }}>@entur/utils
useRandomId er avviklet
useRandomId fra @entur/utils er nå avviklet (deprecated). React 18 tilbyr useId() nativt. Erstatt slik:
// ❌ Før
import { useRandomId } from '@entur/utils';
const id = useRandomId('eds-my-component');
// ✅ Etter
import { useId } from 'react';
const id = `eds-my-component${useId()}`;useRandomId fungerer fortsatt (den bruker useId() internt), men vil bli fjernet i en fremtidig major-versjon.
Tredjepartsavhengigheter
Fjernede avhengigheter
- @reach/dialog → Erstattet av nativt <dialog>-element
- @reach/tabs → Erstattet av nativ ARIA-implementasjon
- react-collapse → Erstattet av CSS grid-animasjon