Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 534a6a3b40 Version Packages (#2443)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-01 15:46:41 +01:00
51eee7f011 [Datepicker/Modal] Datepicker bruker nå Modal på mobil og for nested Modal (#2419)
* 🎨 Initial Modal-update

* 🐛 close(open handling for Modal

* 🔥 Fjernet openOnFocus

* 🔥 Fjernet outsideclick og useEscape

* 🐛 monthpicker onClose forwarding

* 🎨 Flyttet DateInput til egen parts-mappe

* 🏷️ DateInput imports

* 🚚 Flyttet DateWrapper ut til egen komponent

* 🎨 Monthpicker bruker dateWrapper

* 💄 Oppdatert styling for datepicker

* 📝 Oppdatert eksempler

* 📝 changeset

* 🐛 Fikset imports i date-hooks

* ♻️ Bedre context-bruk i Modal.tsx

* 📝 Bedre tekster for deprecation

* 🐛 Støtter nå jsdom

* 🐛 Fikset polyfill av datepicker

* 🎨 closeOnBackdropClick i datepicker

* 🏷️ 🐛 Fikset outsideClick boundary + fjernet bubbleEscape-prop

* 🔥 Fjernet console.log

* 🔥 Fjernet bubbleEscape-prop

* Update @navikt/core/react/src/date/hooks/useMonthPicker.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/date/datepicker/datepicker.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🎨 Bedre className for nested modal

* 🐛 Bedre check om popover er i datepicker

* 🔥 Fjernet unødvendig if rundt window-check

* 🎨 Bedre fallback for useMedia-hook

* 🎨 bedre navn i useMedia

* 📝 Datepicker-stories for size har preset-values

* Update @navikt/core/react/src/date/hooks/useDatepicker.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🎨 DateContext er nå default null hvis ikke satt

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/date/utils/labels.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🐛 Fikset small-size padding

* 🔥 Fjernet error-demo

* fokusgreier

* 🎨 bedre onClose-handling av modal

* 🐛 Håndterer focus ved cancel og select nå

* 🐛 Feil i typer for dateinput

* 🐛 setAnchorRef var optional

* 🐛 null-test

* 🎨 Fjernet jsdom sjekk tester for useMedia

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2023-11-01 15:38:57 +01:00
Vegard Haugstvedt 909ae6aed1 Add new useVirtualFocus hook, for use in Combobox (#2394)
* Add new useVirtualFocus hook, for use in Combobox. Might be useful for other components later, but will scope it for Combobox at first.

* Create filteredOptionsUtils, which has utiltity functions for creating IDs and for text lookups in lists.

* Get rid of data-value, and use filteredOptionsMap to look up the value of the currently (virtually) focused element instead

This allows use of e.g. numbers as options, which data-value didn't.

* Using useState instead of useRef to handle filteredOptionsRef

* Added changeset

* Force option id to be lower case, to avoid different casing from being interpreted as being different from the existing options

* Fix bug where it was not possible to select multiple custom (new) options in a multiselect

selectedOptionsContext is not available inside customOptionsContext, so need to take isMultiSelect from props instead

* Switch to use toLocalLowerCase and remove uneccessary use of string literal

* type mismatch

* Clean up types - reuse from ComboboxProps type definition, and fix resulting type mismatches

* Rename filteredOptionsRef variable returned from filteredOptionsContext to reflect that it is now actually a setState-function

* Expose virtualFocus as an object through filteredOptionsContext, instead of each separate method.

Done to remove need to wrap moveFocusUp and moveFocusDown in order to toggle isListOpen
2023-11-01 12:02:57 +01:00
Lars Eirik Korsgaard Hansen cd3859a91e 📝 add aria roles example to Alert (#2440) 2023-11-01 09:16:42 +01:00
Ken 338f14e7ac [aksel.nav.no] Refaktorert ErrorBoundary-bruk for fast-refresh (#2433)
* ♻️ errorboundary refactor

* ♻️ Converted to non-anonymous function errorboundary

* ♻️ Fikset ErrorBoundary Full-refresh

* [aksel.nav.no] ♻️ Component-structure (#2435)

* 🔥 Fjernet UUFeedback-modul

* 🚚 Flyttet filer i egne mapper

* 🚚 Oppdatert struktur for sanity-moduler

* 🚚 Oppdatert akselcube exports

* 🔥 Fjernet Feedback-modul

* 🚚 Rename SuggestionBlock

* 🚚 Flyttet relatert innhold

* 🐛 Fikset suggestionblock imports

* 🎨 Oppdatert bruk av ShowMore
2023-10-30 15:42:35 +01:00
Halvor Haugan aed1d082c3 🐛 Legg på onClose på Popover-eksempler (#2436) 2023-10-30 12:36:47 +01:00
Ken 31e9226b55 [Aksel.nav.no] Bruker alpha ShowMore lokalt på nettsiden (#2409)
* 📝 Story for TokenTable

* ✨ Tester lokal alpha-showmore på Aksel

* 🎨 Showmore brukes nå bare når det er over 10 tokens i listen

* 📝 yarn lock
2023-10-30 12:10:16 +01:00
156 changed files with 2252 additions and 2078 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# @navikt/aksel-icons
## 5.9.0
## 5.8.0
## 5.7.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "5.8.0",
"version": "5.9.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+2
View File
@@ -1,5 +1,7 @@
# @navikt/aksel-stylelint
## 5.9.0
## 5.8.0
## 5.7.6
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "5.8.0",
"version": "5.9.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0",
"@navikt/ds-css": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+7
View File
@@ -1,5 +1,12 @@
# @navikt/aksel
## 5.9.0
### Patch Changes
- Updated dependencies [[`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382)]:
- @navikt/ds-css@5.9.0
## 5.8.0
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "5.8.0",
"version": "5.9.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "5.8.0",
"@navikt/ds-css": "5.9.0",
"axios": "1.3.6",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-css
## 5.9.0
### Minor Changes
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
## 5.8.0
### Patch Changes
+26 -18
View File
@@ -149,18 +149,6 @@
width: fit-content;
}
/* Focus layering */
.navds-date__field-input:focus-visible,
.navds-date__field-button {
z-index: 1;
}
@supports not selector(:focus-visible) {
.navds-date__field-input:focus {
z-index: 1;
}
}
.navds-date .rdp-day_selected,
.navds-monthpicker__month--selected {
color: var(--ac-date-selected-text, var(--a-text-on-action));
@@ -201,11 +189,11 @@
}
.navds-date__field-input {
padding-right: var(--a-spacing-14);
padding-right: var(--a-spacing-12);
}
.navds-form-field--small .navds-date__field-input {
padding-right: var(--a-spacing-10);
padding-right: var(--a-spacing-8);
}
/* Error-handling */
@@ -282,10 +270,6 @@
pointer-events: none;
}
.navds-date__popover:where(.navds-popover) {
border: none;
}
/* Readonly */
.navds-date__field--readonly .navds-date__field-button {
cursor: default;
@@ -310,11 +294,35 @@
margin: 0;
}
.navds-date__modal.navds-date {
padding: 0;
}
.navds-date__modal-body {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: var(--a-spacing-4);
gap: var(--a-spacing-2);
}
.navds-date__modal-body > .navds-date {
padding: 0;
}
.navds-date__popover:where(.navds-popover) {
border: none;
}
@media (min-width: 480px) {
.navds-date {
padding: var(--a-spacing-5) var(--a-spacing-4);
}
.navds-date__modal-body {
padding: var(--a-spacing-6);
}
.navds-date__caption {
gap: var(--a-spacing-2);
}
+8
View File
@@ -130,6 +130,14 @@
margin-left: auto;
}
/* When Datepicker is used nested inside a Modal */
.navds-modal--polyfilled .navds-modal--polyfilled.navds-date__nested-modal {
min-width: fit-content;
max-width: 100vw;
max-height: 100vh;
animation: none;
}
@keyframes akselModalFadeIn {
from {
opacity: 0;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "5.8.0",
"version": "5.9.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^5.8.0",
"@navikt/ds-tokens": "^5.9.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+20
View File
@@ -1,5 +1,25 @@
# @navikt/ds-react
## 5.9.0
### Minor Changes
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
### Patch Changes
- Added useVirtualFocus hook - used in Combobox for now ([#2394](https://github.com/navikt/aksel/pull/2394))
- Updated dependencies []:
- @navikt/ds-tokens@5.9.0
- @navikt/aksel-icons@5.9.0
## 5.8.0
### Minor Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "5.8.0",
"version": "5.9.0",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,8 +38,8 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0",
"@navikt/aksel-icons": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
@@ -13,13 +13,13 @@ interface DateContextContextProps {
* Aria-connected ID
*/
ariaId?: string;
/**
* Flag for enabled-check
*/
defined: boolean;
}
export const DateContext = createContext<DateContextContextProps>({
open: false,
onOpen: () => null,
ariaId: undefined,
});
export const DateContext = createContext<DateContextContextProps | null>(null);
export const useDateInputContext = () => {
const context = useContext(DateContext);
@@ -1,11 +1,11 @@
import cl from "clsx";
import isWeekend from "date-fns/isWeekend";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { DateRange, DayPicker, isMatch } from "react-day-picker";
import { Popover } from "../../popover";
import { omit, useId } from "../../util";
import { DatePickerInput } from "../DateInput";
import { mergeRefs, omit, useId } from "../../util";
import { DateContext } from "../context";
import { DatePickerInput } from "../parts/DateInput";
import { DateWrapper } from "../parts/DateWrapper";
import { getLocaleFromString, labels } from "../utils";
import DatePickerStandalone from "./DatePickerStandalone";
import Caption from "./parts/Caption";
@@ -79,7 +79,6 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
onClose,
onOpenToggle,
strategy,
bubbleEscape = false,
onWeekNumberClick,
...rest
},
@@ -89,6 +88,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
const [open, setOpen] = useState(_open ?? false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
const [selectedDates, setSelectedDates] = React.useState<
Date | Date[] | DateRange | undefined
@@ -110,6 +110,44 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
rest?.onSelect?.(newSelected);
};
const DatePickerComponent = (
<DayPicker
locale={getLocaleFromString(locale)}
mode={mode}
onSelect={handleSelect}
selected={selected ?? selectedDates}
components={{
Caption: dropdownCaption ? DropdownCaption : Caption,
Head: TableHead,
HeadRow,
WeekNumber,
Row,
Day: DayButton,
}}
className={cl("navds-date", className)}
classNames={{
vhidden: "navds-sr-only",
}}
disabled={(day) => {
return (disableWeekends && isWeekend(day)) || isMatch(day, disabled);
}}
weekStartsOn={1}
initialFocus={false}
labels={labels as any}
modifiers={{
weekend: (day) => disableWeekends && isWeekend(day),
}}
modifiersClassNames={{
weekend: "rdp-day__weekend",
}}
showWeekNumber={showWeekNumber}
onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined}
fixedWeeks
showOutsideDays
{...omit(rest, ["onSelect"])}
/>
);
return (
<DateContext.Provider
value={{
@@ -119,72 +157,27 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
onOpenToggle?.();
},
ariaId,
defined: true,
}}
>
<div
ref={wrapperRef}
ref={mergedRef}
className={cl("navds-date__wrapper", wrapperClassName)}
>
{children}
{(_open ?? open) && (
<Popover
arrow={false}
anchorEl={wrapperRef.current}
open={_open ?? open}
onClose={() => {
onClose?.() ?? setOpen(false);
}}
placement="bottom-start"
id={ariaId}
role="dialog"
ref={ref}
strategy={strategy}
className="navds-date__popover"
bubbleEscape={bubbleEscape}
flip={false}
>
<DayPicker
locale={getLocaleFromString(locale)}
mode={mode}
onSelect={handleSelect}
selected={selected ?? selectedDates}
components={{
Caption: dropdownCaption ? DropdownCaption : Caption,
Head: TableHead,
HeadRow,
WeekNumber,
Row,
Day: DayButton,
}}
className={cl("navds-date", className)}
classNames={{
vhidden: "navds-sr-only",
}}
disabled={(day) => {
return (
(disableWeekends && isWeekend(day)) ||
isMatch(day, disabled)
);
}}
weekStartsOn={1}
initialFocus={false}
labels={labels as any}
modifiers={{
weekend: (day) => disableWeekends && isWeekend(day),
}}
modifiersClassNames={{
weekend: "rdp-day__weekend",
}}
showWeekNumber={showWeekNumber}
onWeekNumberClick={
mode === "multiple" ? onWeekNumberClick : undefined
}
fixedWeeks
showOutsideDays
{...omit(rest, ["onSelect"])}
/>
</Popover>
)}
<DateWrapper
open={_open ?? open}
anchor={wrapperRef.current}
onClose={() => onClose?.() ?? setOpen(false)}
locale={locale}
variant={mode}
popoverProps={{
id: ariaId,
strategy,
}}
>
{DatePickerComponent}
</DateWrapper>
</div>
</DateContext.Provider>
);
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react";
import isSameDay from "date-fns/isSameDay";
import React, { useId, useState } from "react";
import { useDatepicker, useRangeDatepicker } from "..";
import { BodyLong, Button, HGrid, HStack, Modal, VStack } from "../..";
import { BodyLong, Button, HGrid, Modal, VStack } from "../..";
import DatePicker, { DatePickerProps } from "./DatePicker";
const disabledDays = [
@@ -18,7 +18,6 @@ export default {
type DefaultStoryProps = DatePickerProps & {
size: "medium" | "small";
openOnFocus: boolean;
inputfield: boolean;
standalone: boolean;
};
@@ -30,13 +29,11 @@ export const Default: StoryObj<DefaultStoryProps> = {
const rangeCtx = useRangeDatepicker({
fromDate: new Date("Aug 23 2020"),
toDate: new Date("Aug 23 2023"),
openOnFocus: props.openOnFocus,
});
const singleCtx = useDatepicker({
fromDate: new Date("Aug 23 2020"),
toDate: new Date("Aug 23 2023"),
openOnFocus: props.openOnFocus,
});
const newProps = {
@@ -111,7 +108,6 @@ export const Default: StoryObj<DefaultStoryProps> = {
disableWeekends: false,
showWeekNumber: false,
mode: "single",
openOnFocus: true,
inputfield: true,
standalone: false,
},
@@ -186,21 +182,6 @@ export const UseRangedDatepicker = () => {
);
};
export const OpenOnFocus = () => {
const { datepickerProps, inputProps } = useDatepicker({
onDateChange: console.log,
openOnFocus: false,
});
return (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker {...datepickerProps}>
<DatePicker.Input {...inputProps} label="Velg dato" />
</DatePicker>
</div>
);
};
export const NB = () => (
<DatePicker.Standalone locale="nb" today={new Date("2006-07-01")} />
);
@@ -333,15 +314,32 @@ export const Size = () => {
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
defaultSelected: new Date("Feb 23 2023"),
});
const { datepickerProps: d2, inputProps: i2 } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
defaultSelected: new Date("Feb 23 2023"),
});
return (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input size="medium" {...inputProps} label="Velg dato" />
<DatePicker.Input
placeholder="10/10/2023"
size="medium"
{...inputProps}
label="Velg dato"
/>
</DatePicker>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input size="small" {...inputProps} label="Velg dato" />
<DatePicker {...d2} dropdownCaption>
<DatePicker.Input
placeholder="10/10/2023"
size="small"
{...i2}
label="Velg dato"
/>
</DatePicker>
</div>
);
@@ -441,33 +439,26 @@ export const ModalDemo = () => {
});
return (
<Modal open header={{ heading: "Modal-demo" }} width="1024px">
<Modal open header={{ heading: "Modal-demo" }}>
<Modal.Body style={{ position: "relative" }}>
<BodyLong>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Maiores nisi
incidunt ipsum cupiditate nostrum nesciunt, corrupti nihil at atque
animi ab aut. Quam iusto harum eligendi magnam nulla repudiandae
molestias.
<BodyLong spacing>
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
</BodyLong>
<HGrid gap="6" columns={{ xs: 1, lg: 2 }}>
<HStack justify="center" align="start">
<DatePicker.Standalone
fromDate={new Date("Aug 23 2019")}
toDate={new Date("Feb 23 2024")}
/>
</HStack>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input {...inputProps} label="Velg dato" />
</DatePicker>
<HStack justify="center">
<DatePicker.Standalone
fromDate={new Date("Aug 23 2019")}
toDate={new Date("Feb 23 2024")}
/>
</HStack>
</HGrid>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input
{...inputProps}
label="Velg dato"
description="Format: dd.mm.yyyy"
/>
</DatePicker>
</Modal.Body>
<Modal.Footer>
<Button>Neste</Button>
<Button variant="secondary">Tilbake</Button>
<Button variant="tertiary">Avbryt</Button>
</Modal.Footer>
</Modal>
);
};
ModalDemo.parameters = { chromatic: { pauseAnimationAtEnd: true } };
@@ -101,9 +101,4 @@ export interface DatePickerDefaultProps
* @default See Popover
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
}
@@ -1,17 +1,15 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import isWeekend from "date-fns/isWeekend";
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useState } from "react";
import { DayClickEventHandler, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
isValidDate,
parseDate,
} from "../utils";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export interface UseDatepickerOptions
extends Pick<
@@ -59,10 +57,10 @@ export interface UseDatepickerOptions
*/
allowTwoDigitYear?: boolean;
/**
* Opens datepicker on input-focus
* @default true
* Will be removed in a future major-version
* @deprecated
*/
openOnFocus?: boolean;
openOnFocus?: false;
}
interface UseDatepickerValue {
@@ -76,7 +74,14 @@ interface UseDatepickerValue {
inputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Resets all states (callback)
*/
@@ -143,14 +148,11 @@ export const useDatepicker = (
onValidate,
defaultMonth,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLInputElement>(null);
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
// Initialize states
@@ -172,14 +174,6 @@ export const useDatepicker = (
[defaultMonth, defaultSelected, selectedDay, today]
);
useOutsideClickHandler(open, handleOpen, [
daypickerRef,
inputRef.current,
inputRef.current?.nextSibling,
]);
useEscape(open, handleOpen, inputRef);
const updateDate = (date?: Date) => {
onDateChange?.(date);
setSelectedDay(date);
@@ -207,7 +201,6 @@ export const useDatepicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && handleOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -243,7 +236,7 @@ export const useDatepicker = (
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
if (day && !selected) {
handleOpen(false);
inputRef.current && inputRef.current.focus();
anchorRef?.focus();
}
if (!required && selected) {
@@ -319,11 +312,13 @@ export const useDatepicker = (
toDate,
today,
open,
onClose: () => {
handleOpen(false);
anchorRef?.focus();
},
onOpenToggle: () => handleOpen(!open),
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
const inputProps = {
@@ -331,7 +326,7 @@ export const useDatepicker = (
onFocus: handleFocus,
onBlur: handleBlur,
value: inputValue,
ref: inputRef,
setAnchorRef,
};
return { datepickerProps, inputProps, reset, selectedDay, setSelected };
@@ -1,30 +0,0 @@
import { useCallback, useEffect, RefObject } from "react";
export const useEscape = (
open: boolean,
setOpen: (openState: boolean) => void,
focusRef: RefObject<HTMLElement>
) => {
const handleClose = useCallback(() => {
setOpen(false);
focusRef?.current && focusRef.current.focus();
}, [focusRef, setOpen]);
const escape = useCallback(
(event: KeyboardEvent) => {
if (open && event.key === "Escape") {
event.preventDefault(); // This prevents modal from closing when using datepicker inside modal
handleClose();
}
},
[handleClose, open]
);
useEffect(() => {
window.addEventListener("keydown", escape, false);
return () => {
window.removeEventListener("keydown", escape, false);
};
}, [escape]);
};
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import { DateInputProps } from "../DateInput";
import React, { useCallback, useMemo, useState } from "react";
import { MonthPickerProps } from "../monthpicker/types";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
@@ -8,8 +8,6 @@ import {
isValidDate,
parseDate,
} from "../utils";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export interface UseMonthPickerOptions
extends Pick<
@@ -45,8 +43,8 @@ export interface UseMonthPickerOptions
*/
allowTwoDigitYear?: boolean;
/**
* Opens datepicker on input-focus
* @default true
* Will be removed in a future major-version
* @deprecated
*/
openOnFocus?: boolean;
}
@@ -60,7 +58,12 @@ interface UseMonthPickerValue {
* Use: <MonthPicker.Input {...inputProps} />
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
ref: React.RefObject<HTMLInputElement>;
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Currently selected Date
@@ -138,17 +141,14 @@ export const useMonthpicker = (
onValidate,
defaultYear,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
const today = useMemo(() => new Date(), []);
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLInputElement>(null);
const [monthpickerRef, setMonthpickerRef] = useState<HTMLDivElement>();
// Initialize states
const [year, setYear] = useState(defaultSelected ?? defaultYear ?? today);
const [selectedMonth, setSelectedMonth] = useState(defaultSelected);
@@ -169,14 +169,6 @@ export const useMonthpicker = (
[defaultSelected, defaultYear, selectedMonth, today]
);
useOutsideClickHandler(open, handleOpen, [
monthpickerRef,
inputRef.current,
inputRef.current?.nextSibling,
]);
useEscape(open, handleOpen, inputRef);
const updateMonth = (date?: Date) => {
onMonthChange?.(date);
setSelectedMonth(date);
@@ -204,7 +196,7 @@ export const useMonthpicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && handleOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -239,8 +231,8 @@ export const useMonthpicker = (
const handleMonthClick = (month?: Date) => {
if (month) {
handleOpen(false);
inputRef.current && inputRef.current.focus();
setYear(month);
anchorRef?.focus();
}
if (!required && !month) {
@@ -314,9 +306,11 @@ export const useMonthpicker = (
toDate,
open,
onOpenToggle: () => handleOpen(!open),
onClose: () => {
handleOpen(false);
anchorRef?.focus();
},
disabled,
bubbleEscape: true,
ref: setMonthpickerRef,
};
const inputProps = {
@@ -324,7 +318,7 @@ export const useMonthpicker = (
onFocus: handleFocus,
onBlur: handleBlur,
value: inputValue,
ref: inputRef,
setAnchorRef,
};
return { monthpickerProps, inputProps, reset, selectedMonth, setSelected };
@@ -1,34 +0,0 @@
import { useCallback, useEffect } from "react";
export const useOutsideClickHandler = (
open: boolean,
setOpen: (openState: boolean) => void,
refs: Array<any>
) => {
const handleFocusIn = useCallback(
(e) => {
const composed = e.composedPath?.()?.[0];
if (!e?.target || !e?.target?.nodeType || !composed) {
return;
}
if (
!refs.some(
(element) =>
element?.contains(e.target) || element?.contains(composed)
)
) {
open && setOpen(false);
}
},
[open, refs, setOpen]
);
useEffect(() => {
window.addEventListener("focusin", handleFocusIn);
window.addEventListener("pointerdown", handleFocusIn);
return () => {
window?.removeEventListener?.("focusin", handleFocusIn);
window?.removeEventListener?.("pointerdown", handleFocusIn);
};
}, [handleFocusIn]);
};
@@ -1,10 +1,10 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import checkIsBefore from "date-fns/isBefore";
import isWeekend from "date-fns/isWeekend";
import React, { useRef, useState } from "react";
import React, { useState } from "react";
import { DateRange, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
@@ -12,8 +12,6 @@ import {
parseDate,
} from "../utils";
import { DateValidationT, UseDatepickerOptions } from "./useDatepicker";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export type RangeValidationT = {
from: DateValidationT;
@@ -74,14 +72,28 @@ interface UseRangeDatepickerValue {
fromInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Use: <DatePicker.Input label="to" {...toInputProps}/>
*/
toInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef?: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Resets all states (callback)
*/
@@ -218,14 +230,11 @@ export const useRangeDatepicker = (
onValidate,
defaultMonth,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const locale = getLocaleFromString(_locale);
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const inputRefTo = useRef<HTMLInputElement>(null);
const inputRefFrom = useRef<HTMLInputElement>(null);
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
const locale = getLocaleFromString(_locale);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -255,20 +264,6 @@ export const useRangeDatepicker = (
const [open, setOpen] = useState(false);
useOutsideClickHandler(open, setOpen, [
daypickerRef,
inputRefTo.current,
inputRefFrom.current,
inputRefTo.current?.nextSibling,
inputRefFrom.current?.nextSibling,
]);
useEscape(
open,
setOpen,
selectedRange?.from && !selectedRange?.to ? inputRefTo : inputRefFrom
);
const updateRange = (range?: DateRange) => {
onRangeChange?.(range);
setSelectedRange(range);
@@ -324,7 +319,6 @@ export const useRangeDatepicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && setOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -378,6 +372,7 @@ export const useRangeDatepicker = (
const handleSelect = (range) => {
if (range?.from && range?.to) {
setOpen(false);
anchorRef?.focus();
}
const prevToRange =
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
@@ -545,10 +540,12 @@ export const useRangeDatepicker = (
mode: "range" as const,
open,
onOpenToggle: () => setOpen((x) => !x),
onClose: () => {
setOpen(false);
anchorRef?.focus();
},
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
const fromInputProps = {
@@ -556,7 +553,7 @@ export const useRangeDatepicker = (
onFocus: (e) => handleFocus(e, RANGE.FROM),
onBlur: (e) => handleBlur(e, RANGE.FROM),
value: fromInputValue,
ref: inputRefFrom,
setAnchorRef,
};
const toInputProps = {
@@ -564,7 +561,7 @@ export const useRangeDatepicker = (
onFocus: (e) => handleFocus(e, RANGE.TO),
onBlur: (e) => handleBlur(e, RANGE.TO),
value: toInputValue,
ref: inputRefTo,
setAnchorRef,
};
return {
+1 -1
View File
@@ -1,4 +1,3 @@
export { type DateInputProps } from "./DateInput";
export {
default as DatePicker,
type DatePickerProps,
@@ -15,3 +14,4 @@ export {
export { default as MonthPicker } from "./monthpicker/MonthPicker";
export { type MonthPickerStandaloneProps } from "./monthpicker/MonthPickerStandalone";
export { type MonthPickerProps } from "./monthpicker/types";
export { type DateInputProps } from "./parts/DateInput";
@@ -1,10 +1,10 @@
import cl from "clsx";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { RootProvider } from "react-day-picker";
import { Popover } from "../../popover";
import { useId } from "../../util";
import { MonthPickerInput } from "../DateInput";
import { mergeRefs, useId } from "../../util";
import { DateContext, SharedMonthProvider } from "../context";
import { MonthPickerInput } from "../parts/DateInput";
import { DateWrapper } from "../parts/DateWrapper";
import { getLocaleFromString } from "../utils";
import MonthCaption from "./MonthCaption";
import MonthPickerStandalone from "./MonthPickerStandalone";
@@ -74,7 +74,6 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
year,
onYearChange,
strategy = "absolute",
bubbleEscape = false,
},
ref
) => {
@@ -82,6 +81,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
const [open, setOpen] = useState(_open ?? false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
const [selectedMonth, setSelectedMonth] = useState<Date | undefined>(
defaultSelected
@@ -107,51 +107,47 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
onOpenToggle?.();
},
ariaId,
defined: true,
}}
>
<div
ref={wrapperRef}
ref={mergedRef}
className={cl("navds-date__wrapper", wrapperClassName)}
>
{children}
{(_open ?? open) && (
<Popover
arrow={false}
anchorEl={wrapperRef.current}
open={_open ?? open}
onClose={() => onClose?.() ?? setOpen(false)}
placement="bottom-start"
role="dialog"
ref={ref}
id={ariaId}
className="navds-date navds-date__popover"
strategy={strategy}
bubbleEscape={bubbleEscape}
flip={false}
<DateWrapper
open={_open ?? open}
anchor={wrapperRef.current}
onClose={() => onClose?.() ?? setOpen(false)}
locale={locale}
variant="month"
popoverProps={{
id: ariaId,
strategy,
}}
>
<RootProvider
locale={getLocaleFromString(locale)}
selected={selected ?? selectedMonth}
toDate={toDate}
fromDate={fromDate}
month={selected ?? selectedMonth}
>
<RootProvider
locale={getLocaleFromString(locale)}
selected={selected ?? selectedMonth}
toDate={toDate}
fromDate={fromDate}
month={selected ?? selectedMonth}
>
<div className={cl("rdp-month", className)}>
<SharedMonthProvider
dropdownCaption={dropdownCaption}
disabled={disabled}
selected={selected ?? selectedMonth}
onSelect={handleSelect}
year={year}
onYearChange={onYearChange}
>
<MonthCaption />
<MonthSelector />
</SharedMonthProvider>
</div>
</RootProvider>
</Popover>
)}
<div className={cl("rdp-month", className)}>
<SharedMonthProvider
dropdownCaption={dropdownCaption}
disabled={disabled}
selected={selected ?? selectedMonth}
onSelect={handleSelect}
year={year}
onYearChange={onYearChange}
>
<MonthCaption />
<MonthSelector />
</SharedMonthProvider>
</div>
</RootProvider>
</DateWrapper>
</div>
</DateContext.Provider>
);
@@ -75,9 +75,4 @@ export interface MonthPickerProps extends React.HTMLAttributes<HTMLDivElement> {
* @default "absolute"
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
}
@@ -1,11 +1,11 @@
import { CalendarIcon } from "@navikt/aksel-icons";
import cl from "clsx";
import React, { forwardRef, InputHTMLAttributes } from "react";
import { FormFieldProps, useFormField } from "../form/useFormField";
import { useDateInputContext } from "./context";
import { ReadOnlyIcon } from "../form/ReadOnlyIcon";
import { BodyShort, ErrorMessage, Label } from "../typography";
import { omit } from "../util";
import React, { forwardRef, InputHTMLAttributes, useRef } from "react";
import { ReadOnlyIcon } from "../../form/ReadOnlyIcon";
import { FormFieldProps, useFormField } from "../../form/useFormField";
import { BodyShort, ErrorMessage, Label } from "../../typography";
import { omit } from "../../util";
import { useDateInputContext } from "../context";
export interface DateInputProps
extends FormFieldProps,
@@ -28,6 +28,10 @@ export interface DateInputProps
* @private
*/
variant?: "datepicker" | "monthpicker";
/**
* @private
*/
setAnchorRef?: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>;
}
const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
@@ -37,9 +41,12 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
label,
description,
variant = "datepicker",
setAnchorRef,
...rest
} = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const isDatepickerVariant = variant === "datepicker";
const conditionalVariables = {
@@ -50,7 +57,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
},
};
const { onOpen, ariaId, open } = useDateInputContext();
const context = useDateInputContext();
const {
inputProps,
@@ -108,7 +115,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
{...omit(rest, ["error", "errorId", "size"])}
{...inputProps}
autoComplete="off"
aria-controls={open ? ariaId : undefined}
aria-controls={context?.open ? context.ariaId : undefined}
readOnly={readOnly}
className={cl(
"navds-date__field-input",
@@ -116,19 +123,23 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
"navds-body-short",
`navds-body-short--${size}`
)}
size={isDatepickerVariant ? 10 : 14}
size={isDatepickerVariant ? 11 : 14}
/>
<button
disabled={inputProps.disabled || readOnly}
tabIndex={readOnly ? -1 : open ? -1 : 0}
onClick={() => onOpen()}
tabIndex={readOnly ? -1 : context?.open ? -1 : 0}
onClick={() => {
context?.onOpen();
setAnchorRef?.(buttonRef.current);
}}
type="button"
className="navds-date__field-button"
ref={buttonRef}
>
<CalendarIcon
pointerEvents="none"
title={
open
context?.open
? conditionalVariables.iconTitle.close
: conditionalVariables.iconTitle.open
}
@@ -0,0 +1,80 @@
import cl from "clsx";
import React, { useContext, useRef } from "react";
import { Button } from "../../button";
import { Modal } from "../../modal";
import { ModalContext } from "../../modal/ModalContext";
import { Popover } from "../../popover";
import { useMedia } from "../../util/useMedia";
import { modalCloseButtonLabel, modalLabel } from "../utils/labels";
type DateWrapperProps = {
open: boolean;
children: React.ReactNode;
onClose: () => void;
anchor: HTMLDivElement | null;
locale: "nb" | "nn" | "en";
variant: "single" | "multiple" | "range" | "month";
popoverProps: {
id?: string;
strategy?: "absolute" | "fixed";
};
};
export const DateWrapper = ({
open,
children,
onClose,
anchor,
locale,
variant,
popoverProps,
}: DateWrapperProps) => {
const modalRef = useRef<HTMLDialogElement>(null);
const isInModal = useContext(ModalContext) !== null;
const hideModal =
useMedia("screen and (min-width: 768px)", true) && !isInModal;
if (hideModal) {
return (
<Popover
arrow={false}
anchorEl={anchor}
open={open}
onClose={onClose}
placement="bottom-start"
role="dialog"
className={cl("navds-date__popover", {
"navds-date": variant === "month",
})}
flip={false}
{...popoverProps}
>
{children}
</Popover>
);
}
return (
<Modal
ref={modalRef}
open={open}
onClose={onClose}
aria-label={modalLabel(locale, variant)}
className={cl("navds-date__modal", {
"navds-date__nested-modal": isInModal,
"navds-date": variant === "month",
})}
closeOnBackdropClick
>
<div className="navds-date__modal-body">
{children}
<Button
variant="tertiary"
onClick={() => modalRef?.current?.close()}
size="small"
>
{modalCloseButtonLabel(locale)}
</Button>
</div>
</Modal>
);
};
@@ -133,3 +133,86 @@ export const labelWeek = (localeCode?: string): string => {
return `Uke:`;
}
};
const modalLabelSingle = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg dato`;
case "nn":
return `Vel dato`;
case "en-GB":
return `Pick a date`;
default:
return `Velg dato`;
}
};
const modalLabelMultiple = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg datoer`;
case "nn":
return `Vel datoar`;
case "en-GB":
return `Pick dates`;
default:
return `Velg datoer`;
}
};
const modalLabelRanged = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg start- og sluttdato`;
case "nn":
return `Vel start- og sluttdato`;
case "en-GB":
return `Pick a start and end date`;
default:
return `Velg start- og sluttdato`;
}
};
const modalLabelMonth = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg måned`;
case "nn":
return `Vel månad`;
case "en-GB":
return `Pick a month`;
default:
return `Velg måned`;
}
};
export const modalLabel = (
localeCode: string,
variant: "single" | "multiple" | "range" | "month"
) => {
switch (variant) {
case "single":
return modalLabelSingle(localeCode);
case "multiple":
return modalLabelMultiple(localeCode);
case "range":
return modalLabelRanged(localeCode);
case "month":
return modalLabelMonth(localeCode);
default:
return modalLabelSingle(localeCode);
}
};
export const modalCloseButtonLabel = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Lukk`;
case "nn":
return `Lukk`;
case "en-GB":
return `Close`;
default:
return `Lukk`;
}
};
@@ -33,7 +33,7 @@ export const Combobox = forwardRef<
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
const { currentOption, toggleIsListOpen } = useFilteredOptionsContext();
const { activeDecendantId, toggleIsListOpen } = useFilteredOptionsContext();
const { selectedOptions } = useSelectedOptionsContext();
const {
@@ -92,7 +92,7 @@ export const Combobox = forwardRef<
"navds-combobox__wrapper-inner navds-text-field__input",
{
"navds-combobox__wrapper-inner--virtually-unfocused":
currentOption !== null,
activeDecendantId !== null,
}
)}
onClick={focusInput}
@@ -65,7 +65,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
size,
}}
>
<CustomOptionsProvider>
<CustomOptionsProvider value={{ isMultiSelect }}>
<SelectedOptionsProvider
value={{
allowNewValues,
@@ -81,7 +81,6 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
filteredOptions,
isListOpen,
isLoading,
isMultiSelect,
options,
}}
>
@@ -6,6 +6,7 @@ import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsCon
import { useInputContext } from "../Input/inputContext";
import { Loader } from "../../../loader";
import { BodyShort, Label } from "../../../typography";
import filteredOptionsUtil from "./filtered-options-util";
const FilteredOptions = () => {
const {
@@ -18,25 +19,25 @@ const FilteredOptions = () => {
isLoading,
isListOpen,
filteredOptions,
filteredOptionsIndex,
filteredOptionsRef,
setFilteredOptionsRef,
isMouseLastUsedInputDevice,
setIsMouseLastUsedInputDevice,
isValueNew,
setFilteredOptionsIndex,
toggleIsListOpen,
activeDecendantId,
virtualFocus,
} = useFilteredOptionsContext();
const { isMultiSelect, selectedOptions, toggleOption } =
useSelectedOptionsContext();
return (
<ul
ref={filteredOptionsRef}
ref={setFilteredOptionsRef}
className={cl("navds-combobox__list", {
"navds-combobox__list--closed": !isListOpen,
"navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
})}
id={`${id}-filtered-options`}
id={filteredOptionsUtil.getFilteredOptionsId(id)}
role="listbox"
tabIndex={-1}
>
@@ -45,7 +46,8 @@ const FilteredOptions = () => {
className="navds-combobox__list-item--loading"
role="option"
aria-selected={false}
id={`${id}-is-loading`}
id={filteredOptionsUtil.getIsLoadingId(id)}
data-no-focus="true"
>
<Loader aria-label="Søker..." />
</li>
@@ -54,8 +56,12 @@ const FilteredOptions = () => {
<li
tabIndex={-1}
onMouseMove={() => {
if (filteredOptionsIndex !== -1) {
setFilteredOptionsIndex(-1);
if (
activeDecendantId !== filteredOptionsUtil.getAddNewOptionId(id)
) {
virtualFocus.moveFocusToElement(
filteredOptionsUtil.getAddNewOptionId(id)
);
setIsMouseLastUsedInputDevice(true);
}
}}
@@ -64,10 +70,10 @@ const FilteredOptions = () => {
if (!isMultiSelect && !selectedOptions.includes(value))
toggleIsListOpen(false);
}}
id={`${id}-combobox-new-option`}
id={filteredOptionsUtil.getAddNewOptionId(id)}
className={cl("navds-combobox__list-item__new-option", {
"navds-combobox__list-item__new-option--focus":
filteredOptionsIndex === -1,
activeDecendantId === filteredOptionsUtil.getAddNewOptionId(id),
})}
role="option"
aria-selected={false}
@@ -86,24 +92,30 @@ const FilteredOptions = () => {
className="navds-combobox__list-item__no-options"
role="option"
aria-selected={false}
id={`${id}-no-hits`}
id={filteredOptionsUtil.getNoHitsId(id)}
data-no-focus="true"
>
Ingen søketreff
</li>
)}
{filteredOptions.map((option, index) => (
{filteredOptions.map((option) => (
<li
className={cl("navds-combobox__list-item", {
"navds-combobox__list-item--focus": index === filteredOptionsIndex,
"navds-combobox__list-item--focus":
activeDecendantId === filteredOptionsUtil.getOptionId(id, option),
"navds-combobox__list-item--selected":
selectedOptions.includes(option),
})}
id={`${id}-option-${option.replace(" ", "-")}`}
id={filteredOptionsUtil.getOptionId(id, option)}
key={option}
tabIndex={-1}
onMouseMove={() => {
if (filteredOptionsIndex !== index) {
setFilteredOptionsIndex(index);
if (
activeDecendantId !== filteredOptionsUtil.getOptionId(id, option)
) {
virtualFocus.moveFocusToElement(
filteredOptionsUtil.getOptionId(id, option)
);
setIsMouseLastUsedInputDevice(true);
}
}}
@@ -0,0 +1,38 @@
const normalizeText = (text: string): string =>
typeof text === "string" ? text.toLocaleLowerCase().trim() : "";
const isPartOfText = (value, text) =>
normalizeText(text).startsWith(normalizeText(value ?? ""));
const isValueInList = (value, list) =>
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
const getMatchingValuesFromList = (value, list) =>
list?.filter((listItem) => isPartOfText(value, listItem));
const getFilteredOptionsId = (comboboxId: string) =>
`${comboboxId}-filtered-options`;
const getOptionId = (comboboxId: string, option: string) =>
`${comboboxId.toLocaleLowerCase()}-option-${option
.replace(" ", "-")
.toLocaleLowerCase()}`;
const getAddNewOptionId = (comboboxId: string) =>
`${comboboxId}-combobox-new-option`;
const getIsLoadingId = (comboboxId: string) => `${comboboxId}-is-loading`;
const getNoHitsId = (comboboxId: string) => `${comboboxId}-no-hits`;
export default {
normalizeText,
isPartOfText,
isValueInList,
getMatchingValuesFromList,
getFilteredOptionsId,
getAddNewOptionId,
getOptionId,
getIsLoadingId,
getNoHitsId,
};
@@ -1,11 +1,9 @@
import React, {
useState,
useEffect,
useMemo,
createContext,
useContext,
useCallback,
useRef,
SetStateAction,
} from "react";
import cl from "clsx";
@@ -13,26 +11,29 @@ import { useCustomOptionsContext } from "../customOptionsContext";
import { useInputContext } from "../Input/inputContext";
import usePrevious from "../../../util/usePrevious";
import { useClientLayoutEffect } from "../../../util";
import filteredOptionsUtils from "./filtered-options-util";
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
import { ComboboxProps } from "../types";
const normalizeText = (text: string): string =>
typeof text === "string" ? `${text}`.toLowerCase().trim() : "";
const isPartOfText = (value, text) =>
normalizeText(text).startsWith(normalizeText(value ?? ""));
const isValueInList = (value, list) =>
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
const getMatchingValuesFromList = (value, list) =>
list?.filter((listItem) => isPartOfText(value, listItem));
type FilteredOptionsProps = {
children: any;
value: Pick<
ComboboxProps,
| "allowNewValues"
| "filteredOptions"
| "isListOpen"
| "isLoading"
| "options"
>;
};
type FilteredOptionsContextType = {
activeDecendantId?: string;
allowNewValues?: boolean;
ariaDescribedBy?: string;
filteredOptionsRef: React.RefObject<HTMLUListElement>;
filteredOptionsIndex: number | null;
setFilteredOptionsIndex: (index: number) => void;
setFilteredOptionsRef: React.Dispatch<
React.SetStateAction<HTMLUListElement | null>
>;
isListOpen: boolean;
isLoading?: boolean;
filteredOptions: string[];
@@ -40,19 +41,18 @@ type FilteredOptionsContextType = {
setIsMouseLastUsedInputDevice: React.Dispatch<SetStateAction<boolean>>;
isValueNew: boolean;
toggleIsListOpen: (newState?: boolean) => void;
currentOption: string | null;
resetFilteredOptionsIndex: () => void;
moveFocusUp: () => void;
moveFocusDown: () => void;
moveFocusToInput: () => void;
moveFocusToEnd: () => void;
currentOption?: string;
shouldAutocomplete?: boolean;
virtualFocus: VirtualFocusType;
};
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
{} as FilteredOptionsContextType
);
export const FilteredOptionsProvider = ({ children, value: props }) => {
export const FilteredOptionsProvider = ({
children,
value: props,
}: FilteredOptionsProps) => {
const {
allowNewValues,
filteredOptions: externalFilteredOptions,
@@ -60,7 +60,9 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
isLoading,
options,
} = props;
const filteredOptionsRef = useRef<HTMLUListElement | null>(null);
const [filteredOptionsRef, setFilteredOptionsRef] =
useState<HTMLUListElement | null>(null);
const virtualFocus = useVirtualFocus(filteredOptionsRef);
const {
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
value,
@@ -70,9 +72,6 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
shouldAutocomplete,
} = useInputContext();
const [filteredOptionsIndex, setFilteredOptionsIndex] = useState<
number | null
>(null);
const [isInternalListOpen, setInternalListOpen] = useState(false);
const { customOptions } = useCustomOptionsContext();
@@ -81,8 +80,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
return externalFilteredOptions;
}
const opts = [...customOptions, ...options];
setFilteredOptionsIndex(null);
return getMatchingValuesFromList(searchTerm, opts);
return filteredOptionsUtils.getMatchingValuesFromList(searchTerm, opts);
}, [customOptions, externalFilteredOptions, options, searchTerm]);
const previousSearchTerm = usePrevious(searchTerm);
@@ -90,10 +88,26 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
useState(false);
const filteredOptionsMap = useMemo(
() =>
options.reduce(
(map, _option) => ({
...map,
[filteredOptionsUtils.getOptionId(id, _option)]: _option,
}),
{
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
? value
: undefined,
}
),
[allowNewValues, id, options, value]
);
useClientLayoutEffect(() => {
if (
shouldAutocomplete &&
normalizeText(searchTerm) !== "" &&
filteredOptionsUtils.normalizeText(searchTerm) !== "" &&
(previousSearchTerm?.length || 0) < searchTerm.length &&
filteredOptions.length > 0
) {
@@ -115,29 +129,30 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
return isExternalListOpen ?? isInternalListOpen;
}, [isExternalListOpen, isInternalListOpen]);
const toggleIsListOpen = useCallback((newState?: boolean) => {
setFilteredOptionsIndex(null);
setInternalListOpen((oldState) => newState ?? !oldState);
}, []);
const isValueNew = useMemo(
() => Boolean(value) && !isValueInList(value, filteredOptions),
[value, filteredOptions]
const toggleIsListOpen = useCallback(
(newState?: boolean) => {
virtualFocus.moveFocusToTop();
setInternalListOpen((oldState) => newState ?? !oldState);
},
[virtualFocus]
);
const getMinimumIndex = useCallback(() => {
return isValueNew && allowNewValues ? -1 : 0;
}, [allowNewValues, isValueNew]);
const isValueNew = useMemo(
() =>
Boolean(value) &&
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, value)],
[filteredOptionsMap, id, value]
);
const ariaDescribedBy = useMemo(() => {
let activeOption;
if (!isLoading && filteredOptions.length === 0) {
activeOption = `${id}-no-hits`;
activeOption = filteredOptionsUtils.getNoHitsId(id);
} else if ((value && value !== "") || isLoading) {
if (shouldAutocomplete && filteredOptions[0]) {
activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
activeOption = filteredOptionsUtils.getOptionId(id, filteredOptions[0]);
} else if (isListOpen && isLoading) {
activeOption = `${id}-is-loading`;
activeOption = filteredOptionsUtils.getIsLoadingId(id);
}
}
return cl(activeOption, partialAriaDescribedBy) || undefined;
@@ -151,102 +166,21 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
id,
]);
const currentOption = useMemo(() => {
if (filteredOptionsIndex == null) {
return null;
}
if (filteredOptionsIndex === -1) {
return value;
}
return filteredOptions[filteredOptionsIndex];
}, [filteredOptionsIndex, filteredOptions, value]);
const currentOption = useMemo(
() =>
filteredOptionsMap[virtualFocus.activeElement?.getAttribute("id") || -1],
[filteredOptionsMap, virtualFocus]
);
const resetFilteredOptionsIndex = () => {
setFilteredOptionsIndex(getMinimumIndex());
};
const scrollToOption = useCallback((newIndex: number) => {
if (
filteredOptionsRef.current &&
filteredOptionsRef.current.children[newIndex]
) {
const child = filteredOptionsRef.current.children[newIndex];
const { top, bottom } = child.getBoundingClientRect();
const parentRect = filteredOptionsRef.current.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom) {
child.scrollIntoView({ block: "nearest" });
}
}
}, []);
useEffect(() => {
if (filteredOptionsIndex !== null && isListOpen) {
scrollToOption(filteredOptionsIndex);
}
}, [filteredOptionsIndex, isListOpen, scrollToOption]);
const moveFocusToInput = useCallback(() => {
setFilteredOptionsIndex(null);
toggleIsListOpen(false);
}, [toggleIsListOpen]);
const moveFocusToEnd = useCallback(() => {
const lastIndex = filteredOptions.length - 1;
toggleIsListOpen(true);
setFilteredOptionsIndex(lastIndex);
}, [filteredOptions.length, toggleIsListOpen]);
const moveFocusUp = useCallback(() => {
if (filteredOptionsIndex === null) {
return;
}
if (filteredOptionsIndex === getMinimumIndex()) {
toggleIsListOpen(false);
setFilteredOptionsIndex(null);
} else {
const newIndex = Math.max(getMinimumIndex(), filteredOptionsIndex - 1);
setFilteredOptionsIndex(newIndex);
}
}, [filteredOptionsIndex, getMinimumIndex, toggleIsListOpen]);
const moveFocusDown = useCallback(() => {
if (filteredOptionsIndex === null || !isListOpen) {
toggleIsListOpen(true);
if (allowNewValues || filteredOptions.length >= 1) {
setFilteredOptionsIndex(getMinimumIndex());
}
return;
}
const newIndex = Math.min(
filteredOptionsIndex + 1,
Math.max(getMinimumIndex(), filteredOptions.length - 1)
);
setFilteredOptionsIndex(newIndex);
}, [
allowNewValues,
filteredOptions.length,
filteredOptionsIndex,
getMinimumIndex,
isListOpen,
toggleIsListOpen,
]);
const activeDecendantId = useMemo(() => {
if (filteredOptionsIndex === null) {
return undefined;
} else if (filteredOptionsIndex === -1) {
return `${id}-combobox-new-option`;
} else {
return `${id}-option-${currentOption?.replace(" ", "-")}`;
}
}, [filteredOptionsIndex, currentOption, id]);
const activeDecendantId = useMemo(
() => virtualFocus.activeElement?.getAttribute("id") || undefined,
[virtualFocus.activeElement]
);
const filteredOptionsState = {
activeDecendantId,
allowNewValues,
filteredOptionsRef,
filteredOptionsIndex,
setFilteredOptionsIndex,
setFilteredOptionsRef,
shouldAutocomplete,
isListOpen,
isLoading,
@@ -256,11 +190,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
isValueNew,
toggleIsListOpen,
currentOption,
resetFilteredOptionsIndex,
moveFocusUp,
moveFocusDown,
moveFocusToInput,
moveFocusToEnd,
virtualFocus,
ariaDescribedBy,
};
@@ -0,0 +1,87 @@
import { Dispatch, SetStateAction, useState } from "react";
export type VirtualFocusType = {
activeElement: HTMLElement | undefined;
getElementById: (id: string) => HTMLElement | undefined;
isFocusOnTheTop: boolean;
isFocusOnTheBottom: boolean;
setIndex: Dispatch<SetStateAction<number>>;
moveFocusUp: () => void;
moveFocusDown: () => void;
moveFocusToElement: (id: string) => void;
moveFocusToTop: () => void;
moveFocusToBottom: () => void;
};
const useVirtualFocus = (
containerRef: HTMLElement | null
): VirtualFocusType => {
const [index, setIndex] = useState(-1);
const listOfAllChildren: Array<HTMLElement> = containerRef?.children
? Array.prototype.slice.call(containerRef?.children)
: [];
const elementsAbleToReceiveFocus = listOfAllChildren.filter(
(child) => child.getAttribute("data-no-focus") !== "true"
);
const activeElement = elementsAbleToReceiveFocus[index];
const getElementById = (id: string) =>
listOfAllChildren.find((element) => element.id === id);
const isFocusOnTheTop = index === 0;
const isFocusOnTheBottom = index === elementsAbleToReceiveFocus.length - 1;
const scrollToOption = (newIndex: number) => {
const indexOfElementToScrollTo = Math.min(
Math.max(newIndex, 0),
containerRef?.children.length || 0
);
if (containerRef?.children[indexOfElementToScrollTo]) {
const child = containerRef.children[indexOfElementToScrollTo];
const { top, bottom } = child.getBoundingClientRect();
const parentRect = containerRef.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom) {
child.scrollIntoView({ block: "nearest" });
}
}
};
const _moveFocusAndScrollTo = (_index: number) => {
setIndex(_index);
scrollToOption(_index);
};
const moveFocusUp = () => _moveFocusAndScrollTo(Math.max(index - 1, -1));
const moveFocusDown = () =>
_moveFocusAndScrollTo(
Math.min(index + 1, elementsAbleToReceiveFocus.length - 1)
);
const moveFocusToTop = () => _moveFocusAndScrollTo(-1);
const moveFocusToBottom = () =>
_moveFocusAndScrollTo(elementsAbleToReceiveFocus.length - 1);
const moveFocusToElement = (id: string) => {
const thisElement = elementsAbleToReceiveFocus.find(
(_element) => _element.getAttribute("id") === id
);
const indexOfElement = thisElement
? elementsAbleToReceiveFocus.indexOf(thisElement)
: -1;
if (indexOfElement >= 0) {
setIndex(indexOfElement);
}
};
return {
activeElement,
getElementById,
isFocusOnTheTop,
isFocusOnTheBottom,
setIndex,
moveFocusUp,
moveFocusDown,
moveFocusToElement,
moveFocusToTop,
moveFocusToBottom,
};
};
export default useVirtualFocus;
@@ -9,6 +9,7 @@ import cl from "clsx";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import { useInputContext } from "./inputContext";
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
@@ -34,15 +35,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
isValueNew,
toggleIsListOpen,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
moveFocusDown,
ariaDescribedBy,
moveFocusToInput,
moveFocusToEnd,
setFilteredOptionsIndex,
setIsMouseLastUsedInputDevice,
shouldAutocomplete,
virtualFocus,
} = useFilteredOptionsContext();
const onEnter = useCallback(
@@ -57,8 +53,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
event.preventDefault();
// Selecting a value from the dropdown / FilteredOptions
toggleOption(currentOption, event);
if (!isMultiSelect && !isTextInSelectedOptions(currentOption))
if (!isMultiSelect && !isTextInSelectedOptions(currentOption)) {
toggleIsListOpen(false);
}
} else if (shouldAutocomplete && isTextInSelectedOptions(value)) {
event.preventDefault();
// Trying to set the same value that is already set, so just clearing the input
@@ -104,10 +101,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
onEnter(e);
break;
case "Home":
moveFocusToInput();
virtualFocus.moveFocusToTop();
break;
case "End":
moveFocusToEnd();
virtualFocus.moveFocusToBottom();
break;
default:
break;
@@ -128,14 +125,20 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
// so we don't interfere with text editing
if (e.target.selectionStart === value?.length) {
e.preventDefault();
moveFocusDown();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusDown();
}
} else if (e.key === "ArrowUp") {
// Check that the FilteredOptions list is open and has virtual focus.
// Otherwise ignore keystrokes, so it doesn't interfere with text editing
if (isListOpen && filteredOptionsIndex !== null) {
if (isListOpen && activeDecendantId) {
e.preventDefault();
moveFocusUp();
if (virtualFocus.isFocusOnTheTop) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusUp();
}
}
},
@@ -143,11 +146,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
value,
selectedOptions,
removeSelectedOption,
moveFocusDown,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
activeDecendantId,
setIsMouseLastUsedInputDevice,
toggleIsListOpen,
virtualFocus,
]
);
@@ -159,13 +162,14 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
} else if (filteredOptions.length === 0) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusToTop();
onChange(event);
},
[filteredOptions.length, onChange, toggleIsListOpen]
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen]
);
const onBlur = () => {
setFilteredOptionsIndex(-1);
virtualFocus.moveFocusToTop();
};
return (
@@ -180,7 +184,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
onBlur={onBlur}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
aria-controls={`${inputProps.id}-filtered-options`}
aria-controls={filteredOptionsUtil.getFilteredOptionsId(inputProps.id)}
aria-expanded={!!isListOpen}
autoComplete="off"
aria-autocomplete={shouldAutocomplete ? "both" : "list"}
@@ -1,6 +1,5 @@
import React, { useState, useCallback, createContext, useContext } from "react";
import { useInputContext } from "./Input/inputContext";
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
type CustomOptionsContextType = {
customOptions: string[];
@@ -13,13 +12,19 @@ const CustomOptionsContext = createContext<CustomOptionsContextType>(
{} as CustomOptionsContextType
);
export const CustomOptionsProvider = ({ children }) => {
export const CustomOptionsProvider = ({
children,
value,
}: {
children: any;
value: { isMultiSelect?: boolean };
}) => {
const [customOptions, setCustomOptions] = useState<string[]>([]);
const { focusInput } = useInputContext();
const { isMultiSelect } = useSelectedOptionsContext();
const { isMultiSelect } = value;
const removeCustomOption = useCallback(
(option) => {
(option: string) => {
setCustomOptions((prevCustomOptions) =>
prevCustomOptions.filter((o) => o !== option)
);
@@ -29,7 +34,7 @@ export const CustomOptionsProvider = ({ children }) => {
);
const addCustomOption = useCallback(
(option) => {
(option: string) => {
if (isMultiSelect) {
setCustomOptions((prevOptions) => [...prevOptions, option]);
} else {
@@ -1,7 +1,7 @@
import { InformationIcon } from "@navikt/aksel-icons";
import { Meta } from "@storybook/react";
import React from "react";
import { BodyLong, GuidePanel, VStack } from "../index";
import { Meta } from "@storybook/react";
import { InformationIcon } from "@navikt/aksel-icons";
export default {
title: "ds-react/GuidePanel",
+4 -1
View File
@@ -8,6 +8,7 @@ import React, {
useRef,
} from "react";
import { createPortal } from "react-dom";
import { DateContext } from "../date/context";
import { useProvider } from "../provider";
import { Detail, Heading } from "../typography";
import { mergeRefs, useId } from "../util";
@@ -99,7 +100,9 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
const rootElement = useProvider()?.rootElement;
const portalNode = useFloatingPortalNode({ root: rootElement });
if (useContext(ModalContext)) {
const dateContext = useContext(DateContext);
const modalContext = useContext(ModalContext);
if (modalContext && !dateContext) {
console.error("Modals should not be nested");
}
+4 -12
View File
@@ -18,6 +18,7 @@ import React, {
useMemo,
useRef,
} from "react";
import { DateContext } from "../date/context";
import { ModalContext } from "../modal/ModalContext";
import { mergeRefs, useClientLayoutEffect, useEventListener } from "../util";
import PopoverContent, { PopoverContentType } from "./PopoverContent";
@@ -73,11 +74,6 @@ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
* @default "absolute"
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
/**
* Changes placement of the floating element in order to keep it in view.
* @default true
@@ -124,7 +120,6 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
placement = "top",
offset,
strategy: userStrategy,
bubbleEscape = false,
flip: _flip = true,
...rest
},
@@ -132,8 +127,9 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
) => {
const arrowRef = useRef<HTMLDivElement | null>(null);
const isInModal = useContext(ModalContext) !== null;
const isInDatepicker = useContext(DateContext) !== null;
const chosenStrategy = userStrategy ?? (isInModal ? "fixed" : "absolute");
const chosenFlip = isInModal ? true : _flip;
const chosenFlip = isInDatepicker ? false : _flip;
const {
x,
@@ -160,11 +156,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
const { getFloatingProps } = useInteractions([
useClick(context),
useDismiss(context, {
bubbles: {
escapeKey: bubbleEscape,
},
}),
useDismiss(context),
]);
useClientLayoutEffect(() => {
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import { useMedia } from "../useMedia";
function TestComponent({ fallback }: { fallback?: boolean }) {
const media = useMedia("screen and (min-width: 1024px)", fallback);
return <div data-testid="media-id">{`${media}`}</div>;
}
describe("useMedia", () => {
test("Should return 'undefined' when no fallback is given", async () => {
render(<TestComponent />);
expect(screen.getByTestId("media-id").innerHTML).toEqual("undefined");
});
test("Should return fallback", async () => {
render(<TestComponent fallback={true} />);
expect(screen.getByTestId("media-id").innerHTML).toEqual("true");
});
});
+38
View File
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
export const noMatchMedia =
typeof window !== "undefined" && window.matchMedia === undefined;
/**
* @example useMedia("screen and (min-width: 1024px)")
* @param media string
* @param fallback boolean
* @returns boolean | undefined
*/
export const useMedia = (
media: string,
fallback?: boolean
): boolean | undefined => {
const [matches, setMatches] = useState(fallback);
useEffect(() => {
if (noMatchMedia) {
return;
}
const mediaQueryList = window.matchMedia(media);
setMatches(mediaQueryList.matches);
const listener = (evt: MediaQueryListEvent) => {
setMatches(evt.matches);
};
mediaQueryList.addEventListener("change", listener);
return () => {
mediaQueryList.removeEventListener("change", listener);
};
}, [media]);
return matches;
};
+2
View File
@@ -1,5 +1,7 @@
# @navikt/ds-tailwind
## 5.9.0
## 5.8.0
## 5.7.6
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "5.8.0",
"version": "5.9.0",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -22,7 +22,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^5.8.0",
"@navikt/ds-tokens": "^5.9.0",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+2
View File
@@ -1,5 +1,7 @@
# @navikt/ds-tokens
## 5.9.0
## 5.8.0
## 5.7.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "5.8.0",
"version": "5.9.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 5.9.0
### @navikt/ds-css
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
### @navikt/ds-react
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
* Added useVirtualFocus hook - used in Combobox for now ([#2394](https://github.com/navikt/aksel/pull/2394))
## 5.8.0
### @navikt/ds-css
+5 -5
View File
@@ -15,11 +15,11 @@
"website"
],
"dependencies": {
"@navikt/aksel-icons": "^5.8.0",
"@navikt/ds-css": "^5.8.0",
"@navikt/ds-react": "^5.8.0",
"@navikt/ds-tailwind": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0"
"@navikt/aksel-icons": "^5.9.0",
"@navikt/ds-css": "^5.9.0",
"@navikt/ds-react": "^5.9.0",
"@navikt/ds-tailwind": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
@@ -6,29 +6,26 @@ import {
PortableTextReactComponents,
} from "@portabletext/react";
import cl from "clsx";
import ExpansionCard from "components/sanity-modules/ExpansionCard";
import Accordion from "components/sanity-modules/accordion/Accordion";
import Alert from "components/sanity-modules/alert/Alert";
import Bilde from "components/sanity-modules/bilde/Bilde";
import InnholdsKort from "components/sanity-modules/cards/InnholdsKort";
import { InlineCode } from "components/website-modules/InlineCode";
import { KBD } from "components/website-modules/KBD";
import CodeExamples from "components/sanity-modules/code-examples/CodeExamples";
import SnippetLazy from "components/sanity-modules/code-snippet/SnippetLazy";
import DoDont from "components/sanity-modules/do-dont/DoDont";
import ExpansionCard from "components/sanity-modules/expansioncard/ExpansionCard";
import PropsSeksjon from "components/sanity-modules/props/PropsSeksjon";
import RelatertInnhold from "components/sanity-modules/relatert-innhold/RelatertInnhold";
import SideModul from "components/sanity-modules/side-modul/SideModul";
import Tabell from "components/sanity-modules/tabell/Tabell";
import TastaturModul from "components/sanity-modules/tastatur-tabell/TastaturTabell";
import Tips from "components/sanity-modules/tips/Tips";
import TokenTable from "components/sanity-modules/token-tabell/TokenTable";
import Video from "components/sanity-modules/video/Video";
import InlineCode from "components/website-modules/InlineCode";
import KBD from "components/website-modules/KBD";
import NextLink from "next/link";
import { Children } from "react";
import {
Accordion,
Alert,
Bilde,
CodeExamples,
DoDont,
PropsSeksjon,
RelatertInnhold,
SideModul,
Snippet,
Tabell,
TastaturModul,
Tips,
TokenTable,
UuFeedback,
Video,
} from ".";
const serializers: Partial<PortableTextReactComponents> = {
types: {
@@ -40,7 +37,7 @@ const serializers: Partial<PortableTextReactComponents> = {
bilde: ({ value }) => <Bilde node={value} />,
alert: ({ value }) => <Alert node={value} />,
expansioncard: ({ value }) => <ExpansionCard node={value} />,
kode: ({ value }) => <Snippet node={value} />,
kode: ({ value }) => <SnippetLazy node={value} />,
tabell_v2: ({ value }) => <Tabell node={value} />,
accordion: ({ value }) => <Accordion node={value} />,
props_seksjon: ({ value }) => <PropsSeksjon node={value} />,
@@ -49,7 +46,6 @@ const serializers: Partial<PortableTextReactComponents> = {
video: ({ value }) => <Video node={value} />,
tips: ({ value }) => <Tips node={value} />,
kode_eksempler: ({ value }) => <CodeExamples node={value} />,
uufeedback: ({ value }) => <UuFeedback node={value} />,
},
unknownType: () => null,
block: {
-18
View File
@@ -1,22 +1,4 @@
export * from "./assets";
export { default as Accordion } from "./sanity-modules/Accordion";
export { default as Alert } from "./sanity-modules/Alert";
export { default as Bilde } from "./sanity-modules/Bilde";
export * from "./sanity-modules/cards";
export * from "./sanity-modules/code";
export * from "./sanity-modules/code-examples";
export { default as DoDont } from "./sanity-modules/DoDont";
export { default as IntroKomponent } from "./sanity-modules/IntroSeksjon";
export { default as PropsSeksjon } from "./sanity-modules/props/PropsSeksjon";
export { default as PropTable } from "./sanity-modules/props/PropTabell";
export { default as SideModul } from "./sanity-modules/SideModul";
export { default as Tabell } from "./sanity-modules/Tabell";
export { default as TastaturModul } from "./sanity-modules/TastaturTabell";
export { default as Tips } from "./sanity-modules/Tips";
export { default as TokenTable } from "./sanity-modules/TokenTable";
export * from "./sanity-modules/UuFeedback";
export { default as Video } from "./sanity-modules/Video";
export * from "./website-modules/BreadCrumbs";
export { default as Feedback } from "./website-modules/feedback";
export * from "./website-modules/toc/TOC";
export * from "./website-modules/utils";
@@ -4,7 +4,6 @@ import { capitalize } from "@/utils";
import { Box, Detail, Heading, Link } from "@navikt/ds-react";
import cl from "clsx";
import { Sidebar } from "components/layout/sidebar/Sidebar";
import Feedback from "components/website-modules/feedback";
import { TableOfContentsv2 } from "components/website-modules/toc/TOCv2";
import Image from "next/legacy/image";
import NextLink from "next/link";
@@ -114,7 +113,6 @@ export const WithSidebar = ({
{variant === "page" && <TableOfContentsv2 />}
<div className="w-full">
{children}
<Feedback docId={pageProps?._id} docType={pageProps?._type} />
{footer && <div className="w-full">{footer}</div>}
</div>
</div>
@@ -1,7 +1,7 @@
import { searchOptions } from "@/types";
import { XMarkIcon } from "@navikt/aksel-icons";
import { Button, Chips, Search } from "@navikt/ds-react";
import { KBD } from "components/website-modules/KBD";
import KBD from "components/website-modules/KBD";
import { useContext, useRef } from "react";
import { useShortcut } from "./hooks";
import {
@@ -3,7 +3,7 @@ import { SearchHitT, searchOptions } from "@/types";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { Chips } from "@navikt/ds-react";
import cl from "clsx";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import { Tag } from "components/sanity-modules/frontpage-blocks/latest-articles/Tag";
import { StatusTag } from "components/website-modules/StatusTag";
import Image from "next/legacy/image";
import { default as Link, default as NextLink } from "next/link";
@@ -1,7 +1,7 @@
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Heading } from "@navikt/ds-react";
import cl from "clsx";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import { Tag } from "components/sanity-modules/frontpage-blocks/latest-articles/Tag";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Hit } from "./Hit";
@@ -1,35 +0,0 @@
import React from "react";
import { withErrorBoundary } from "@/error-boundary";
import TokenView from "./token-view";
type SpesialT = {
_key: string;
_type: "spesial_seksjon";
modul?: "ikonsok" | "token_kategori";
logs?: any[];
komponenter?: any;
farge?: any;
token?: { title: string; kategori: string };
};
const SideModul = ({ node }: { node: SpesialT }) => {
if (!node || !node.modul) {
return null;
}
const GetModule = () => {
switch (node.modul) {
case "ikonsok":
return null;
case "token_kategori":
return <TokenView token={node.token} />;
default: {
return null;
}
}
};
return <div className="mb-16">{GetModule()}</div>;
};
export default withErrorBoundary(SideModul, "SideModul");
@@ -1,98 +0,0 @@
import { withErrorBoundary } from "@/error-boundary";
import { TokenTableT } from "@/types";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import core from "@navikt/ds-css/tokens.json";
import { BodyLong, Button, CopyButton, Label, Link } from "@navikt/ds-react";
import cl from "clsx";
import { AkselTable, AkselTableRow } from "components/website-modules/Table";
import NextLink from "next/link";
import { useState } from "react";
const TokenTable = ({ node }: { node: TokenTableT }) => {
const [open, setOpen] = useState(false);
const tokens: { [key: string]: string } | null =
node.title in core ? core[node.title] : null;
if (!tokens) {
return null;
}
const showMore = Object.keys(tokens).length > 8;
return (
<div className="mb-7">
<div
className={cl({
"after:to-bg-default relative max-h-80 overflow-y-hidden after:absolute after:inset-0 after:bg-gradient-to-b after:from-transparent after:via-transparent":
!open && showMore,
})}
>
<AkselTable th={[{ text: "Token" }, { text: "Fallback" }]}>
{Object.entries(tokens).map(([key, val]) => (
<AkselTableRow key={key} tr={[{ text: key }, { text: val }]} />
))}
</AkselTable>
{(open || !showMore) && (
<div className="border-border-subtle bg-surface-default relative -mt-9 w-full rounded-b-lg border border-t-0 p-2 pr-14">
<CopyButton
copyText={Object.entries(tokens).reduce(
(prev, cur) =>
prev +
`${cur[0]}: ${
cur[1].startsWith("--") ? `var(${cur[1]})` : `${cur[1]}`
};\n`,
""
)}
className="absolute right-2 top-2 z-10"
/>
<Label size="small" as="span" spacing>
Hva er dette?
</Label>
<BodyLong size="small">
Komponent-tokens gir deg muligheten til å sette opp theming eller
justere styling uten å måtte overskrive css-klasser. Les gjennom{" "}
<NextLink
href="/grunnleggende/styling/design-tokens#hec62d38bc813"
passHref
legacyBehavior
>
<Link>guiden vår</Link>
</NextLink>{" "}
eller utforsk alle{" "}
<NextLink
href="/grunnleggende/styling/design-tokens"
passHref
legacyBehavior
>
<Link>design tokens</Link>
</NextLink>
.
</BodyLong>
</div>
)}
</div>
{!open && showMore && (
<div className="bg-surface-default mx-auto grid w-fit -translate-y-[80%] place-content-center shadow-[0_0_8px_8px_var(--a-surface-default)]">
<Button
variant="secondary-neutral"
onClick={() => setOpen(true)}
className="group"
icon={
<ChevronDownIcon
className="group-hover:translate-y-05 transition-transform"
aria-hidden
fontSize="1.5rem"
/>
}
iconPosition="right"
>
Vis mer
</Button>
</div>
)}
</div>
);
};
export default withErrorBoundary(TokenTable, "Token-tabell");
@@ -1,83 +0,0 @@
import { FeedbackT } from "@/types";
import { IdContext } from "@/utils";
import { Button, Heading, Textarea } from "@navikt/ds-react";
import cl from "clsx";
import { useRouter } from "next/router";
import { useContext, useState } from "react";
export const UuFeedback = ({ node }: { node: { vis: boolean } }) => {
const [textValue, setTextValue] = useState("");
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [sent, setSent] = useState(false);
const idCtx = useContext(IdContext);
const { asPath, basePath } = useRouter();
if (!node?.vis) {
return null;
}
const fetchFeedback = () => {
const body: FeedbackT = {
message: textValue,
url: `${basePath}${asPath}`,
type: "uu",
docId: idCtx?.id,
};
fetch("/api/feedback", {
method: "POST",
body: JSON.stringify(body),
});
};
const handleSend = (e) => {
e.preventDefault();
if (textValue === "") {
setErrorMsg(
"Tilbakemeldingen kan ikke være tom. Legg til tekst i feltet."
);
return;
}
setErrorMsg(null);
setSent(true);
fetchFeedback();
setTextValue("");
};
return (
<div className="my-8">
{!sent && (
<form
className={cl(
"animate-fadeIn mt-4 flex w-full max-w-sm flex-col gap-4"
)}
>
<Textarea
error={errorMsg}
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
maxLength={600}
minRows={3}
label="Tilbakemelding"
description="Ikke skriv inn navn eller andre personopplysninger"
autoComplete="off"
/>
<Button className="mr-auto" onClick={handleSend}>
Send inn tilbakemelding
</Button>
</form>
)}
<div aria-live="polite">
{sent && (
<>
<Heading size="small" as="p" className="mt-8">
Takk for tilbakemeldingen!
</Heading>
</>
)}
</div>
</div>
);
};
@@ -1,10 +1,13 @@
import { Accordion as DsAccordion } from "@navikt/ds-react";
import React from "react";
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { AccordionT } from "@/types";
import { Accordion as DsAccordion } from "@navikt/ds-react";
const Accordion = ({ node }: { node: AccordionT }) => {
type AccordionProps = {
node: AccordionT;
};
const Accordion = ({ node }: AccordionProps) => {
if (!node || node.list.length === 0) {
return null;
}
@@ -25,4 +28,10 @@ const Accordion = ({ node }: { node: AccordionT }) => {
);
};
export default withErrorBoundary(Accordion, "Accordion");
export default function Component(props: AccordionProps) {
return (
<ErrorBoundary boundaryName="Accordion">
<Accordion {...props} />
</ErrorBoundary>
);
}
@@ -1,8 +1,13 @@
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { AlertT } from "@/types";
import { Alert as DsAlert, Heading } from "@navikt/ds-react";
const Alert = ({ node }: { node: AlertT }) => {
type AlertProps = {
node: AlertT;
};
const Alert = ({ node }: AlertProps) => {
return (
<div className="mb-7 max-w-2xl">
<DsAlert variant={node.variant}>
@@ -23,4 +28,10 @@ const Alert = ({ node }: { node: AlertT }) => {
);
};
export default Alert;
export default function Component(props: AlertProps) {
return (
<ErrorBoundary boundaryName="Alert">
<Alert {...props} />
</ErrorBoundary>
);
}
@@ -1,12 +1,17 @@
/* eslint-disable @next/next/no-img-element */
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { urlFor } from "@/sanity/interface";
import { BildeT } from "@/types";
import { BodyLong, Link } from "@navikt/ds-react";
import cl from "clsx";
import NextLink from "next/link";
const Bilde = ({ node, className }: { node: BildeT; className?: string }) => {
type BildeProps = {
node: BildeT;
className?: string;
};
const Bilde = ({ node, className }: BildeProps) => {
if (!node || !node.asset) {
return null;
}
@@ -65,4 +70,10 @@ const Bilde = ({ node, className }: { node: BildeT; className?: string }) => {
);
};
export default withErrorBoundary(Bilde, "Bilde");
export default function Component(props: BildeProps) {
return (
<ErrorBoundary boundaryName="Bilde">
<Bilde {...props} />
</ErrorBoundary>
);
}
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import {
@@ -12,6 +12,14 @@ import { useFormatedDate } from "components/website-modules/utils/getDate";
import NextLink from "next/link";
import { abbrName } from "../..";
type ArtikkelCardProps = ResolveContributorsSingleT<
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
> & {
source?: string;
variant: string;
level?: "2" | "3";
};
const ArtikkelCard = ({
slug,
source,
@@ -24,9 +32,7 @@ const ArtikkelCard = ({
tema,
level = "2",
...rest
}: ResolveContributorsSingleT<
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
> & { source?: string; variant: string; level?: "2" | "3" }) => {
}: ArtikkelCardProps) => {
const date = useFormatedDate(
(rest as any)?.updateInfo?.lastVerified ?? publishedAt ?? _updatedAt
);
@@ -89,4 +95,10 @@ const ArtikkelCard = ({
);
};
export default withErrorBoundary(ArtikkelCard, "ArtikkelCard");
export default function Component(props: ArtikkelCardProps) {
return (
<ErrorBoundary boundaryName="ArtikkelCard">
<ArtikkelCard {...props} />
</ErrorBoundary>
);
}
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { AkselBloggDocT, ResolveContributorsT, ResolveSlugT } from "@/types";
import { getAuthors } from "@/utils";
@@ -6,11 +6,11 @@ import { BodyLong, BodyShort, Heading, Link } from "@navikt/ds-react";
import { useFormatedDate } from "components/website-modules/utils/getDate";
import NextLink from "next/link";
const BloggCard = ({
blog,
}: {
type BloggCardProps = {
blog: ResolveContributorsT<ResolveSlugT<AkselBloggDocT>>;
}) => {
};
const BloggCard = ({ blog }: BloggCardProps) => {
const date = useFormatedDate(blog?.publishedAt ?? blog._createdAt);
return (
<li
@@ -46,4 +46,10 @@ const BloggCard = ({
);
};
export default withErrorBoundary(BloggCard, "BloggCard");
export default function Component(props: BloggCardProps) {
return (
<ErrorBoundary boundaryName="ArtikkelCard">
<BloggCard {...props} />
</ErrorBoundary>
);
}
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { urlFor } from "@/sanity/interface";
import { AkselTemaT } from "@/types";
@@ -7,7 +7,11 @@ import cl from "clsx";
import Image from "next/legacy/image";
import NextLink from "next/link";
const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
type GodPraksisCardProps = {
node: AkselTemaT;
};
const GodPraksisCard = ({ node }: GodPraksisCardProps) => {
if (
!node?.pictogram ||
!node?.slug?.current ||
@@ -47,4 +51,10 @@ const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
);
};
export default withErrorBoundary(GodPraksisCard, "GodPraksisCard");
export default function Component(props: GodPraksisCardProps) {
return (
<ErrorBoundary boundaryName="GodPraksisCard">
<GodPraksisCard {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,15 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { urlFor } from "@/sanity/interface";
import { AkselTemaT } from "@/types";
import Image from "next/legacy/image";
import NextLink from "next/link";
const GodPraksisCardSimple = ({ node }: { node: Partial<AkselTemaT> }) => {
type GodPraksisCardSimpleProps = {
node: Partial<AkselTemaT>;
};
const GodPraksisCardSimple = ({ node }: GodPraksisCardSimpleProps) => {
if (!node?.pictogram || !node?.slug?.current || !node?.title) {
return null;
}
@@ -36,4 +40,10 @@ const GodPraksisCardSimple = ({ node }: { node: Partial<AkselTemaT> }) => {
);
};
export default withErrorBoundary(GodPraksisCardSimple, "GodPraksisCardSimple");
export default function Component(props: GodPraksisCardSimpleProps) {
return (
<ErrorBoundary boundaryName="GodPraksisCardSimple">
<GodPraksisCardSimple {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,15 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { SanityBlockContent } from "@/sanity-block";
import { InnholdsKortPrinsipperT } from "@/types";
import { Heading } from "@navikt/ds-react";
import NextLink from "next/link";
const InnholdsKort = ({ node }: { node: InnholdsKortPrinsipperT }) => {
type InnholdsKortProps = {
node: InnholdsKortPrinsipperT;
};
const InnholdsKort = ({ node }: InnholdsKortProps) => {
if (!node.title || !node.body || !node.lenke) {
return null;
}
@@ -39,4 +43,10 @@ const InnholdsKort = ({ node }: { node: InnholdsKortPrinsipperT }) => {
);
};
export default withErrorBoundary(InnholdsKort, "InnholdsKort");
export default function Component(props: InnholdsKortProps) {
return (
<ErrorBoundary boundaryName="InnholdsKort">
<InnholdsKort {...props} />
</ErrorBoundary>
);
}
@@ -1 +0,0 @@
export { default as RelatertInnhold } from "./RelatertInnhold";
@@ -1,6 +1,6 @@
import { getKey } from "@/sb-util";
import type { Meta, StoryObj } from "@storybook/react";
import CodeExample from "./Examples";
import CodeExample from "./CodeExamples";
const meta = {
title: "Sanity-modules/CodeExample",
@@ -1,5 +1,5 @@
import { capitalize, Snippet } from "@/components";
import { withErrorBoundary } from "@/error-boundary";
import { capitalize } from "@/components";
import ErrorBoundary from "@/error-boundary";
import { CodeExamplesT } from "@/types";
import {
ExternalLinkIcon,
@@ -8,14 +8,19 @@ import {
} from "@navikt/aksel-icons";
import { BodyLong, Button, Chips, HStack } from "@navikt/ds-react";
import cl from "clsx";
import SnippetLazy from "components/sanity-modules/code-snippet/SnippetLazy";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { CodeSandbox } from "./CodeSandbox";
import { CodeSandbox } from "./parts/CodeSandbox";
const iframePadding = 192;
const iframeId = "example-iframe";
const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
type CodeExamplesProps = {
node: CodeExamplesT;
};
const ComponentExamples = ({ node }: CodeExamplesProps) => {
const [activeExample, setActiveExample] = useState(null);
const [frameState, setFrameState] = useState(300);
const [unloaded, setUnloaded] = useState(true);
@@ -203,7 +208,7 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
</HStack>
</div>
<Snippet
<SnippetLazy
node={{
code: { code: fil.innhold.trim(), language: "tsx" },
}}
@@ -217,4 +222,10 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
);
};
export default withErrorBoundary(ComponentExamples, "Eksempler kode");
export default function Component(props: CodeExamplesProps) {
return (
<ErrorBoundary boundaryName="Kodeeksempler">
<ComponentExamples {...props} />
</ErrorBoundary>
);
}
@@ -1 +0,0 @@
export { default as CodeExamples } from "./Examples";
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { CodeSnippetT } from "@/types";
import { ChevronRightIcon, TerminalIcon } from "@navikt/aksel-icons";
import { CopyButton, Label } from "@navikt/ds-react";
@@ -6,7 +6,11 @@ import cl from "clsx";
import { Highlight, Language } from "prism-react-renderer";
import theme from "./theme";
const CodeSnippet = ({ node: { code, title } }: { node: CodeSnippetT }) => {
type CodeSnippetProps = {
node: CodeSnippetT;
};
const CodeSnippet = ({ node: { code, title } }: CodeSnippetProps) => {
if (!code || !code.code) {
return null;
}
@@ -94,4 +98,10 @@ const CodeSnippet = ({ node: { code, title } }: { node: CodeSnippetT }) => {
);
};
export default withErrorBoundary(CodeSnippet, "Kode snippet");
export default function Component(props: CodeSnippetProps) {
return (
<ErrorBoundary boundaryName="Kodesnippet">
<CodeSnippet {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,7 @@
import dynamic from "next/dynamic";
const SnippetLazy = dynamic(() => import("./Snippet"), {
ssr: false,
});
export default SnippetLazy;
@@ -1,5 +0,0 @@
import dynamic from "next/dynamic";
export const Snippet = dynamic(() => import("./Snippet"), {
ssr: false,
});
@@ -1,5 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { urlFor } from "@/sanity/interface";
import { DoDontT } from "@/types";
import {
@@ -53,7 +53,11 @@ const Element = ({ block }: { block: DoDontT["blokker"][number] }) => {
);
};
const DoDont = ({ node }: { node: DoDontT }) => {
type DoDontProps = {
node: DoDontT;
};
const DoDont = ({ node }: DoDontProps) => {
if (!node) return null;
return (
@@ -101,4 +105,10 @@ function getText(s: string) {
}
}
export default withErrorBoundary(DoDont, "DoDont");
export default function Component(props: DoDontProps) {
return (
<ErrorBoundary boundaryName="Dodont">
<DoDont {...props} />
</ErrorBoundary>
);
}
@@ -1,17 +1,17 @@
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { ExpansionCard as DsExpansionCard } from "@navikt/ds-react";
import { withErrorBoundary } from "@/error-boundary";
const ExpansionCard = ({
node,
}: {
type ExpansionCardProps = {
node: {
heading: string;
heading_level: "h3" | "h4";
description?: string;
body: any[];
};
}) => {
};
const ExpansionCard = ({ node }: ExpansionCardProps) => {
if (!node.heading || !node.body || !node.heading_level) {
return null;
}
@@ -42,4 +42,10 @@ const ExpansionCard = ({
);
};
export default withErrorBoundary(ExpansionCard, "ExpansionCard");
export default function Component(props: ExpansionCardProps) {
return (
<ErrorBoundary boundaryName="ExpansionCard">
<ExpansionCard {...props} />
</ErrorBoundary>
);
}
@@ -1,8 +1,12 @@
import Latest, { LatestT } from "./Latest";
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import Latest, { LatestT } from "./latest-articles/Latest";
export type BlocksT = LatestT;
export const FrontpageBlock = ({ blocks }: { blocks: BlocksT[] }) => {
type FrontpageBlockProps = {
blocks: BlocksT[];
};
export const FrontpageBlock = ({ blocks }: FrontpageBlockProps) => {
if (!blocks || !blocks.length) {
return null;
}
@@ -20,4 +24,10 @@ export const FrontpageBlock = ({ blocks }: { blocks: BlocksT[] }) => {
);
};
export default withErrorBoundary(FrontpageBlock, "FrontpageBlock");
export default function Component(props: FrontpageBlockProps) {
return (
<ErrorBoundary boundaryName="FrontpageBlock">
<FrontpageBlock {...props} />
</ErrorBoundary>
);
}
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { urlFor } from "@/sanity/interface";
import { abbrName } from "@/utils";
@@ -29,15 +29,13 @@ export type ArticleT = {
contributors?: { title: string }[];
};
const Card = ({
article,
visible,
index,
}: {
type CardProps = {
article: ArticleT;
visible: boolean;
index: number;
}) => {
};
const Card = ({ article, visible, index }: CardProps) => {
const date = useFormatedDate(article.publishedAt ?? article._updatedAt);
const showFooter = ["aksel_artikkel", "aksel_blogg"].includes(article._type);
@@ -153,4 +151,10 @@ const Card = ({
);
};
export default withErrorBoundary(Card, "FrontpageBlockCard");
export default function Component(props: CardProps) {
return (
<ErrorBoundary boundaryName="FrontpageBlockCard">
<Card {...props} />
</ErrorBoundary>
);
}
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { Heading } from "@navikt/ds-react";
import cl from "clsx";
import { useEffect, useRef, useState } from "react";
@@ -17,7 +17,11 @@ export type LatestT = {
};
};
const Latest = ({ block }: { block: LatestT }) => {
type LatestArticlesProps = {
block: LatestT;
};
const Latest = ({ block }: LatestArticlesProps) => {
const highlights = block.highlights?.length;
const [intersected, setIntersected] = useState(false);
const section = useRef(null);
@@ -90,4 +94,10 @@ function getList(block: LatestT) {
});
}
export default withErrorBoundary(Latest, "Latest");
export default function Component(props: LatestArticlesProps) {
return (
<ErrorBoundary boundaryName="Latest-articles">
<Latest {...props} />
</ErrorBoundary>
);
}
@@ -1,15 +1,14 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { AkselGrunnleggendeDocT, AkselKomponentDocT } from "@/types";
import { Label } from "@navikt/ds-react";
const Intro = ({
node,
internal,
}: {
type IntroProps = {
node: AkselKomponentDocT["intro"] | AkselGrunnleggendeDocT["intro"];
internal?: boolean;
}) => {
};
const Intro = ({ node, internal }: IntroProps) => {
if (!node || !node.body || !node.brukes_til) {
return null;
}
@@ -61,4 +60,10 @@ const Intro = ({
);
};
export default withErrorBoundary(Intro, "Intro komponent");
export default function Component(props: IntroProps) {
return (
<ErrorBoundary boundaryName="Intro-modul">
<Intro {...props} />
</ErrorBoundary>
);
}
@@ -1,92 +0,0 @@
import { withErrorBoundary } from "@/error-boundary";
import { BodyShort, Detail, Heading, Tooltip } from "@navikt/ds-react";
import { Highlighter } from "./Highlight";
import { PropTableT } from "@/types";
const List = ({ prop, parent }: { prop: any; parent: string }) => {
if (prop?.description && prop.description.includes("@private")) {
return null;
}
return (
<Detail
as="div"
className="block overflow-x-auto border border-t-0 border-gray-300 p-2 font-mono first-of-type:border-t last-of-type:rounded-b"
>
<dt>
{!prop.defaultValue ? (
<span className="font-semibold">{`${prop.name}${
prop?.required ? "" : "?"
} `}</span>
) : (
<Tooltip
content={`default: ${prop.defaultValue}`}
arrow={false}
delay={0}
>
<span className="mr-2 cursor-pointer border-b border-dashed border-gray-600 font-semibold">{`${
prop.name
}${prop?.required ? "" : "?"}`}</span>
</Tooltip>
)}
<span>{prop.type ? <>{Highlighter({ type: prop.type })}</> : ""}</span>
</dt>
{prop.description && (
<dd className="whitespace-pre-wrap font-sans text-base">
{prop.description}
</dd>
)}
{prop.name === "ref" && prop.type.includes("Ref<") && (
<dd className="font-sans text-base">
{`${parent} extends ${prop.type.slice(
prop.type.indexOf("<") + 1,
prop.type.lastIndexOf(">")
)}`}
</dd>
)}
</Detail>
);
};
const PropTable = ({ komponent }: { komponent: PropTableT }) => {
return (
<div>
<Heading
size="xsmall"
level="3"
className="scroll-m-8 rounded-t-lg border border-b-0 border-gray-300 bg-gray-50 p-2"
id={`${komponent._key}`}
>
{komponent?.title ? komponent.title : "Props"}
</Heading>
<div className="toc-ignore relative mb-8">
{komponent?.propref?.proplist?.length === 0 && (
<div className="mb-8 rounded-b-lg border border-gray-300 p-2">
<BodyShort>Fant ingen props for denne komponenten.</BodyShort>
</div>
)}
<dl>
{komponent?.overridable && (
<List
prop={{
description: "OverridableComponent-api",
required: false,
name: "as",
type: "React.ElementType",
}}
parent={komponent?.title ?? ""}
/>
)}
{komponent?.propref?.proplist?.map((prop) => (
<List key={prop.name} prop={prop} parent={komponent?.title ?? ""} />
))}
</dl>
</div>
</div>
);
};
export default withErrorBoundary(PropTable, "Proptable");
@@ -1,9 +1,12 @@
import React from "react";
import { withErrorBoundary } from "@/error-boundary";
import PropTableV2 from "./PropTabell";
import ErrorBoundary from "@/error-boundary";
import { PropTableT } from "@/types";
import PropTableV2 from "./parts/PropTabell";
const PropsSeksjon = ({ node }: { node: any }) => {
type PropsSeksjonProps = {
node: any;
};
const PropsSeksjon = ({ node }: PropsSeksjonProps) => {
if (
!node ||
(node?.elementer?.length === 0 && node?.komponenter?.length === 0)
@@ -27,4 +30,10 @@ const PropsSeksjon = ({ node }: { node: any }) => {
);
};
export default withErrorBoundary(PropsSeksjon, "PropsSeksjon");
export default function Component(props: PropsSeksjonProps) {
return (
<ErrorBoundary boundaryName="PropsSeksjon">
<PropsSeksjon {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,48 @@
import { Detail, Tooltip } from "@navikt/ds-react";
import { Highlighter } from "./Highlight";
export const DtList = ({ prop, parent }: { prop: any; parent: string }) => {
if (prop?.description && prop.description.includes("@private")) {
return null;
}
return (
<Detail
as="div"
className="block overflow-x-auto border border-t-0 border-gray-300 p-2 font-mono first-of-type:border-t last-of-type:rounded-b"
>
<dt>
{!prop.defaultValue ? (
<span className="font-semibold">{`${prop.name}${
prop?.required ? "" : "?"
} `}</span>
) : (
<Tooltip
content={`default: ${prop.defaultValue}`}
arrow={false}
delay={0}
>
<span className="mr-2 cursor-pointer border-b border-dashed border-gray-600 font-semibold">{`${
prop.name
}${prop?.required ? "" : "?"}`}</span>
</Tooltip>
)}
<span>{prop.type ? <>{Highlighter({ type: prop.type })}</> : ""}</span>
</dt>
{prop.description && (
<dd className="whitespace-pre-wrap font-sans text-base">
{prop.description}
</dd>
)}
{prop.name === "ref" && prop.type.includes("Ref<") && (
<dd className="font-sans text-base">
{`${parent} extends ${prop.type.slice(
prop.type.indexOf("<") + 1,
prop.type.lastIndexOf(">")
)}`}
</dd>
)}
</Detail>
);
};
@@ -0,0 +1,60 @@
import ErrorBoundary from "@/error-boundary";
import { PropTableT } from "@/types";
import { BodyShort, Heading } from "@navikt/ds-react";
import { DtList } from "./DtList";
type PropTableProps = {
komponent: PropTableT;
};
const PropTable = ({ komponent }: PropTableProps) => {
return (
<div>
<Heading
size="xsmall"
level="3"
className="scroll-m-8 rounded-t-lg border border-b-0 border-gray-300 bg-gray-50 p-2"
id={`${komponent._key}`}
>
{komponent?.title ? komponent.title : "Props"}
</Heading>
<div className="toc-ignore relative mb-8">
{komponent?.propref?.proplist?.length === 0 && (
<div className="mb-8 rounded-b-lg border border-gray-300 p-2">
<BodyShort>Fant ingen props for denne komponenten.</BodyShort>
</div>
)}
<dl>
{komponent?.overridable && (
<DtList
prop={{
description: "OverridableComponent-api",
required: false,
name: "as",
type: "React.ElementType",
}}
parent={komponent?.title ?? ""}
/>
)}
{komponent?.propref?.proplist?.map((prop) => (
<DtList
key={prop.name}
prop={prop}
parent={komponent?.title ?? ""}
/>
))}
</dl>
</div>
</div>
);
};
export default function Component(props: PropTableProps) {
return (
<ErrorBoundary boundaryName="Proptable">
<PropTable {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,15 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { RelatertInnholdT } from "@/types";
import { NewspaperIcon } from "@navikt/aksel-icons";
import { Heading, Link } from "@navikt/ds-react";
import NextLink from "next/link";
const RelatertInnhold = ({ node }: { node: RelatertInnholdT }) => {
type RelatertInnholdProps = {
node: RelatertInnholdT;
};
const RelatertInnhold = ({ node }: RelatertInnholdProps) => {
if (!node || node?.lenker?.length === 0) {
return null;
}
@@ -47,4 +51,10 @@ const RelatertInnhold = ({ node }: { node: RelatertInnholdT }) => {
);
};
export default withErrorBoundary(RelatertInnhold, "RelatertInnhold");
export default function Component(props: RelatertInnholdProps) {
return (
<ErrorBoundary boundaryName="RelatertInnhold">
<RelatertInnhold {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,39 @@
import ErrorBoundary from "@/error-boundary";
import TokenView from "../token-view/TokenView";
type SideModulProps = {
node: {
_key: string;
_type: "spesial_seksjon";
modul?: "token_kategori";
logs?: any[];
komponenter?: any;
farge?: any;
token?: { title: string; kategori: string };
};
};
const SideModul = ({ node }: SideModulProps) => {
if (!node || !node.modul) {
return null;
}
const GetModule = () => {
switch (node.modul) {
case "token_kategori":
return <TokenView token={node.token} />;
default: {
return null;
}
}
};
return <div className="mb-16">{GetModule()}</div>;
};
export default function Component(props: SideModulProps) {
return (
<ErrorBoundary boundaryName="SideModul">
<SideModul {...props} />
</ErrorBoundary>
);
}
@@ -1,8 +1,10 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { TableT } from "@/types";
import { Table } from "@navikt/ds-react";
const TableComponent = ({ node }: { node: TableT }) => {
type TableProps = { node: TableT };
const TableComponent = ({ node }: TableProps) => {
if (!node || !node.rows || node.rows.length < 2) {
return null;
}
@@ -45,4 +47,10 @@ const TableComponent = ({ node }: { node: TableT }) => {
);
};
export default withErrorBoundary(TableComponent, "Tabell");
export default function Component(props: TableProps) {
return (
<ErrorBoundary boundaryName="Tabell">
<TableComponent {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,13 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { UUTableT } from "@/types";
import { KBD } from "components/website-modules/KBD";
import KBD from "components/website-modules/KBD";
import { AkselTable, AkselTableRow } from "components/website-modules/Table";
import React from "react";
const UuSeksjon = ({ node }: { node: UUTableT }) => {
type UUTableProps = { node: UUTableT };
const UuSeksjon = ({ node }: UUTableProps) => {
if (!node || !node?.tastatur) {
return null;
}
@@ -36,4 +38,10 @@ const UuSeksjon = ({ node }: { node: UUTableT }) => {
);
};
export default withErrorBoundary(UuSeksjon, "UuSeksjon");
export default function Component(props: UUTableProps) {
return (
<ErrorBoundary boundaryName="uu-seksjon">
<UuSeksjon {...props} />
</ErrorBoundary>
);
}
@@ -1,10 +1,12 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { TipsT } from "@/types";
import { LightBulbIcon } from "@navikt/aksel-icons";
import { Heading } from "@navikt/ds-react";
const Tips = ({ node }: { node: TipsT }) => {
type TipsProps = { node: TipsT };
const Tips = ({ node }: TipsProps) => {
if (!node || !node.body) {
return null;
}
@@ -25,4 +27,10 @@ const Tips = ({ node }: { node: TipsT }) => {
);
};
export default withErrorBoundary(Tips, "Tips");
export default function Component(props: TipsProps) {
return (
<ErrorBoundary boundaryName="Tips">
<Tips {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/react";
import TokenTable from "./TokenTable";
const meta = {
title: "Sanity-modules/TokenTable",
component: TokenTable,
tags: ["autodocs"],
} satisfies Meta<typeof TokenTable>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithShowMore: Story = {
args: {
node: {
kategori: "Core",
title: "button",
},
},
};
export const WithOutShowMore: Story = {
args: {
node: {
kategori: "Core",
title: "forms",
},
},
};
@@ -0,0 +1,92 @@
import ErrorBoundary from "@/error-boundary";
import { TokenTableT } from "@/types";
import core from "@navikt/ds-css/tokens.json";
import { Bleed, BodyLong, CopyButton, Label, Link } from "@navikt/ds-react";
import { AkselTable, AkselTableRow } from "components/website-modules/Table";
import NextLink from "next/link";
import ShowMore from "./parts/ShowMore";
type TokenTableProps = { node: TokenTableT };
const TokenTable = ({ node }: TokenTableProps) => {
const tokens: { [key: string]: string } | null =
node.title in core ? core[node.title] : null;
if (!tokens) {
return null;
}
const Wrapper = ({ children }) => {
if (Object.entries(tokens).length < 10) {
return <>{children}</>;
}
return (
<Bleed marginInline="4" marginBlock="4" asChild>
<ShowMore
as="section"
aria-label="Proptabell"
scrollBackOnCollapse
collapsedHeight="20rem"
>
{children}
</ShowMore>
</Bleed>
);
};
return (
<div className="mb-7">
<Wrapper>
<AkselTable th={[{ text: "Token" }, { text: "Fallback" }]}>
{Object.entries(tokens).map(([key, val]) => (
<AkselTableRow key={key} tr={[{ text: key }, { text: val }]} />
))}
</AkselTable>
<div className="border-border-subtle bg-surface-default relative -mt-9 w-full rounded-b-lg border border-t-0 p-2 pr-14">
<CopyButton
copyText={Object.entries(tokens).reduce(
(prev, cur) =>
prev +
`${cur[0]}: ${
cur[1].startsWith("--") ? `var(${cur[1]})` : `${cur[1]}`
};\n`,
""
)}
className="absolute right-2 top-2 z-10"
/>
<Label size="small" as="span" spacing>
Hva er dette?
</Label>
<BodyLong size="small">
Komponent-tokens gir deg muligheten til å sette opp theming eller
justere styling uten å måtte overskrive css-klasser. Les gjennom{" "}
<NextLink
href="/grunnleggende/styling/design-tokens#hec62d38bc813"
passHref
legacyBehavior
>
<Link>guiden vår</Link>
</NextLink>{" "}
eller utforsk alle{" "}
<NextLink
href="/grunnleggende/styling/design-tokens"
passHref
legacyBehavior
>
<Link>design tokens</Link>
</NextLink>
.
</BodyLong>
</div>
</Wrapper>
</div>
);
};
export default function Component(props: TokenTableProps) {
return (
<ErrorBoundary boundaryName="Token-tabell">
<TokenTable {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,149 @@
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
import { Button, mergeRefs, useId, type HeadingProps } from "@navikt/ds-react";
import cl from "clsx";
import React, { forwardRef, useMemo, useRef, useState } from "react";
export interface ShowMoreProps
extends Omit<React.HTMLAttributes<HTMLElement>, "onClick"> {
/**
* Override what element to render the wrapper as.
* @default aside
*/
as?: "aside" | "section";
/**
* Content. Is [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inert) when collapsed.
*/
children: React.ReactNode;
/**
* Changes button size
* @default medium
*/
size?: "medium" | "small";
/**
* Changes background color
* @default default
*/
variant?: "default" | "subtle" | "info";
/**
* Custom height of content when collapsed.
* @default 10rem
*/
collapsedHeight?: `${number}${string}` | number;
/**
* Heading text. Always available to screen readers.
* Used as accessible label unless you define `aria-label` or `aria-labelledby`.
*/
heading?: string;
/**
* Heading size
* @default medium
*/
headingSize?: HeadingProps["size"];
/**
* Heading level
* @default "1"
*/
headingLevel?: HeadingProps["level"];
/**
* Scroll back up to the component after collapsing.
* @default true
*/
scrollBackOnCollapse?: boolean;
}
/**
* A component for partially hiding less important content.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/show-more)
* @see 🏷️ {@link ShowMoreProps}
*
* @example
* <ShowMore heading="Facts about toads">
* Toads have dry, leathery skin, short legs, and large bumps covering the parotoid glands.
* </ShowMore>
*/
export const ShowMore = forwardRef<HTMLElement, ShowMoreProps>(
(
{
as: Component = "aside",
children,
size = "medium",
variant = "default",
collapsedHeight = "10rem",
/* heading,
headingSize = "medium",
headingLevel = "1", */
scrollBackOnCollapse = true,
className,
"aria-labelledby": ariaLabelledby,
...rest
},
ref
) => {
const localRef = useRef<HTMLElement>(null);
const mergedRef = useMemo(() => mergeRefs([localRef, ref]), [ref]);
const [isOpen, setIsOpen] = useState(false);
const ariaLabelId = useId();
const ChevronIcon = isOpen ? ChevronUpIcon : ChevronDownIcon;
return (
<Component
ref={mergedRef}
className={cl(
"navds-show-more",
`navds-show-more--${variant}`,
className,
{ "navds-show-more--closed": !isOpen }
)}
aria-labelledby={
!ariaLabelledby && !rest["aria-label"] ? ariaLabelId : ariaLabelledby
}
{...rest}
>
{/* <Heading size={headingSize} level={headingLevel} id={ariaLabelId}>
{heading}
</Heading> */}
<div className="navds-show-more__button-section">
<div className="navds-show-more__button-wrapper">
<Button
type="button"
variant="secondary-neutral"
className="navds-show-more__button"
icon={<ChevronIcon aria-hidden />}
iconPosition="right"
size={size}
onClick={() => {
setIsOpen(!isOpen);
if (scrollBackOnCollapse && isOpen) {
localRef.current?.scrollIntoView();
}
}}
>
{isOpen ? "Vis mindre" : "Vis mer"}
</Button>
</div>
</div>
<div
className="navds-show-more__content"
style={isOpen ? {} : { height: collapsedHeight }}
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60822
inert={isOpen ? undefined : ""}
>
{children}
</div>
</Component>
);
}
);
export default ShowMore;
/**
* Erfaringer med bruk:
* - Skal heading være mandatory?
* - Bleed teknikk for å fjerne padding fungerer ikke internt med ShowMore siden `overflow: hidden;`. Må legges rundt ShowMore
* Innholdet blir da "kuttet" når ShowMore er lukket (ser bra ut når den er åpen)
*/
@@ -1,11 +1,11 @@
import { FontView } from "./categories/Font";
import { GlobalView } from "./categories/Global";
import { SemanticView } from "./categories/Semantic";
import { ShadowView } from "./categories/Shadow";
import { SpacingView } from "./categories/Spacing";
import { ZindexView } from "./categories/Zindex";
import { BreakpointsView } from "./categories/Breakpoints";
import { ShapesView } from "./categories/Shapes";
import { BreakpointsView } from "./parts/categories/Breakpoints";
import { FontView } from "./parts/categories/Font";
import { GlobalView } from "./parts/categories/Global";
import { SemanticView } from "./parts/categories/Semantic";
import { ShadowView } from "./parts/categories/Shadow";
import { ShapesView } from "./parts/categories/Shapes";
import { SpacingView } from "./parts/categories/Spacing";
import { ZindexView } from "./parts/categories/Zindex";
export default {
title: "sanity-modules/Tokens",
@@ -0,0 +1,47 @@
import ErrorBoundary from "@/error-boundary";
import { BreakpointsView } from "./parts/categories/Breakpoints";
import { FontView } from "./parts/categories/Font";
import { GlobalView } from "./parts/categories/Global";
import { SemanticView } from "./parts/categories/Semantic";
import { ShadowView } from "./parts/categories/Shadow";
import { ShapesView } from "./parts/categories/Shapes";
import { SpacingView } from "./parts/categories/Spacing";
import { ZindexView } from "./parts/categories/Zindex";
type TokenViewProps = {
token: {
title: string;
kategori: string;
};
};
const TokenView = ({ token }: TokenViewProps) => {
switch (true) {
case token.title === "font":
return <FontView cat={token.title} />;
case token.title.startsWith("global"):
return <GlobalView cat={token.title} />;
case token.title.startsWith("semantic"):
return <SemanticView cat={token.title} />;
case token.title === "radius":
return <ShapesView cat={token.title} />;
case token.title === "shadow":
return <ShadowView cat={token.title} />;
case token.title === "spacing":
return <SpacingView cat={token.title} />;
case token.title === "z-index":
return <ZindexView cat={token.title} />;
case token.title === "breakpoints":
return <BreakpointsView cat={token.title} />;
default:
return null;
}
};
export default function Component(props: TokenViewProps) {
return (
<ErrorBoundary boundaryName="TokenView">
<TokenView {...props} />
</ErrorBoundary>
);
}
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff. Show more