Compare commits

..
1 Commits
Author SHA1 Message Date
Ken d0c85c69fd perf(date): Import date utilities from sub-modules 2026-10-30 14:33:12 +01:00
58 changed files with 360 additions and 428 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": patch
---
Date utilities: Import date-fns functions and locales from their own paths, so unbundled Node imports (SSR, Vitest/Jest) no longer load every date-fns locale.
+30 -16
View File
@@ -142,7 +142,7 @@
flex-direction: column;
overflow: hidden;
.aksel-combobox2:not(:has(.aksel-combobox2__modal)) & {
.aksel-combobox2:has(:not(.aksel-combobox2__modal)) & {
outline: 1px solid var(--ax-border-neutral-subtleA);
outline-offset: -1px;
border-radius: var(--ax-radius-8);
@@ -158,21 +158,26 @@
margin: -1px;
}
.aksel-combobox2__filter .aksel-search__wrapper:has(.aksel-search__input:focus-visible) {
outline: none;
position: relative;
.aksel-combobox2__filter .aksel-search__wrapper {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
&::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border: 2px solid var(--ax-border-focus);
}
&:has(.aksel-search__input:focus-visible) {
outline: none;
position: relative;
.aksel-combobox2:not(:has(.aksel-combobox2__modal)) &::after {
border-top-left-radius: var(--ax-radius-8);
border-top-right-radius: var(--ax-radius-8);
&::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border: 2px solid var(--ax-border-focus);
}
.aksel-combobox2:has(:not(.aksel-combobox2__modal)) &::after {
border-top-left-radius: var(--ax-radius-8);
border-top-right-radius: var(--ax-radius-8);
}
}
}
@@ -184,7 +189,7 @@
border-color: var(--ax-border-neutral-subtleA);
}
.aksel-combobox2__popup[data-size="medium"] & {
.aksel-combobox2[data-size="medium"] & {
min-height: 0;
}
@@ -216,6 +221,15 @@
text-align: center;
}
/************************************************
* Group *
************************************************/
.aksel-combobox2__group-label {
padding: var(--ax-space-8) var(--ax-space-8);
font-weight: bold;
}
/************************************************
* Option *
************************************************/
@@ -239,7 +253,7 @@
margin-top: var(--ax-space-2);
float: left;
.aksel-combobox2__popup[data-size="small"] & {
.aksel-combobox2[data-size="small"] & {
--__axc-combobox2-option-checkmark-size: 1rem;
}
@@ -1,4 +1,5 @@
import { type Locale, format } from "date-fns";
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import React, { useEffect, useRef } from "react";
import type { CalendarDay, Modifiers } from "react-day-picker";
import { cl } from "../../utils/helpers";
@@ -1,12 +1,10 @@
import {
type Locale,
format,
getMonth,
getYear,
setMonth,
setYear,
startOfMonth,
} from "date-fns";
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import { getMonth } from "date-fns/getMonth";
import { getYear } from "date-fns/getYear";
import { setMonth } from "date-fns/setMonth";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import React, { type ChangeEvent, useCallback } from "react";
import { type CalendarMonth, useDayPicker } from "react-day-picker";
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
@@ -1,4 +1,4 @@
import { isWeekend } from "date-fns";
import { isWeekend } from "date-fns/isWeekend";
import React, { useCallback, useState } from "react";
import {
type ClassNames,
@@ -1,4 +1,4 @@
import { isSameDay } from "date-fns";
import { isSameDay } from "date-fns/isSameDay";
import React, { forwardRef, useRef, useState } from "react";
import type { DateRange } from "react-day-picker";
import { useId } from "../../utils-external";
@@ -1,4 +1,5 @@
import { differenceInCalendarDays, isWeekend } from "date-fns";
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isWeekend } from "date-fns/isWeekend";
import React, { useCallback, useState } from "react";
import { type DayEventHandler, dateMatchModifiers } from "react-day-picker";
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
@@ -1,4 +1,6 @@
import { differenceInCalendarDays, isSameDay, isWeekend } from "date-fns";
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isSameDay } from "date-fns/isSameDay";
import { isWeekend } from "date-fns/isWeekend";
import React, { useState } from "react";
import { dateMatchModifiers } from "react-day-picker";
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
+121 -149
View File
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react";
import React from "react";
import { Dialog } from "../../dialog";
import { BodyShort, ErrorMessage } from "../../typography";
import { cl } from "../../utils/helpers";
@@ -36,114 +36,109 @@ interface ComboboxProps<
name?: string;
}
const Combobox = forwardRef(
<T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>>(
function Combobox<
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
>({
label,
hideLabel,
name,
size: sizeProp,
error,
errorId: errorIdProp,
disabled: disabledProp,
description,
id,
readOnly: readOnlyProp,
multiple,
...rest
}: ComboboxProps<T>) {
const {
inputProps,
errorId,
showErrorMsg,
hasError,
size,
inputDescriptionId,
readOnly,
} = useFormField(
{
label,
hideLabel,
name,
size: sizeProp,
description,
disabled: disabledProp,
error,
errorId: errorIdProp,
disabled: disabledProp,
description,
id,
readOnly: readOnlyProp,
multiple,
...rest
}: ComboboxProps<T>,
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const {
inputProps,
errorId,
showErrorMsg,
hasError,
size,
inputDescriptionId,
readOnly,
} = useFormField(
{
description,
disabled: disabledProp,
error,
errorId: errorIdProp,
id,
readOnly: readOnlyProp,
size: sizeProp,
},
"combobox",
);
size: sizeProp,
},
"combobox",
);
return (
<ComboboxRoot
size={size}
disabled={inputProps.disabled}
multiple={multiple}
{...rest}
return (
<ComboboxRoot
size={size}
disabled={inputProps.disabled}
multiple={multiple}
{...rest}
>
<ComboboxLabel
htmlFor={inputProps.id}
hide={hideLabel}
readOnly={readOnly}
>
<ComboboxLabel
htmlFor={inputProps.id}
hide={hideLabel}
readOnly={readOnly}
>
{label}
</ComboboxLabel>
{!!description && (
<BodyShort
className={cl(
"aksel-form-field__description aksel-combobox2__description",
{ "aksel-sr-only": hideLabel },
)}
id={inputDescriptionId}
size={size}
as="div"
>
{description}
</BodyShort>
)}
<ComboboxTrigger readOnly={readOnly} {...inputProps}>
<ComboboxField ref={ref} hasError={hasError} />
</ComboboxTrigger>
<div
className="aksel-form-field__error aksel-combobox2__error"
id={errorId}
aria-relevant="additions removals"
aria-live="polite"
>
{showErrorMsg && (
<ErrorMessage size={size} showIcon>
{error}
</ErrorMessage>
{label}
</ComboboxLabel>
{!!description && (
<BodyShort
className={cl(
"aksel-form-field__description aksel-combobox2__description",
{ "aksel-sr-only": hideLabel },
)}
</div>
{name &&
rest.value.map((val) => (
<input key={val} type="hidden" name={name} value={val} />
))}
<ComboboxOverlay
mobileHeader={
<Dialog.Header>
<Dialog.Title>{label}</Dialog.Title>
{!!description && (
<Dialog.Description>{description}</Dialog.Description>
)}
</Dialog.Header>
}
id={inputDescriptionId}
size={size}
as="div"
>
<ComboboxPopup>
<ComboboxFilter />
<ComboboxList />
</ComboboxPopup>
</ComboboxOverlay>
</ComboboxRoot>
);
},
) as <T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>>(
props: ComboboxProps<T> & React.RefAttributes<HTMLButtonElement>,
) => React.ReactElement;
{description}
</BodyShort>
)}
<ComboboxTrigger readOnly={readOnly} {...inputProps}>
<ComboboxField hasError={hasError} />
</ComboboxTrigger>
<div
className="aksel-form-field__error aksel-combobox2__error"
id={errorId}
aria-relevant="additions removals"
aria-live="polite"
>
{showErrorMsg && (
<ErrorMessage size={size} showIcon>
{error}
</ErrorMessage>
)}
</div>
{name &&
rest.value.map((val) => (
<input key={val} type="hidden" name={name} value={val} />
))}
<ComboboxOverlay
mobileHeader={
<Dialog.Header>
<Dialog.Title>{label}</Dialog.Title>
{!!description && (
<Dialog.Description>{description}</Dialog.Description>
)}
</Dialog.Header>
}
>
<ComboboxPopup>
<ComboboxFilter />
<ComboboxList />
</ComboboxPopup>
</ComboboxOverlay>
</ComboboxRoot>
);
}
// ----------- ComboboxSingle ----------- //
@@ -154,67 +149,44 @@ interface ComboboxSingleProps<
* Called when selected option changes.
*/
onChange?: (
newValue: NonNullable<ComboboxProps<T>["value"]>[number] | null,
newValue: NonNullable<ComboboxProps<T>["value"]>[number] | undefined,
) => void;
/**
* Value of the selected option.
*/
value?: NonNullable<ComboboxProps<T>["value"]>[number] | null;
value?: NonNullable<ComboboxProps<T>["value"]>[number];
/**
* Initial value when uncontrolled.
*/
defaultValue?: NonNullable<ComboboxProps<T>["value"]>[number];
}
/**
* Component for selecting a value from a list of options that can be filtered.
*
* **WARNING: This component is in active development and may receive breaking changes outside major releases!**
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/TODO)
* @see 🏷️ {@link ComboboxSingleProps}
*
* @example
* ```jsx
* <ComboboxSingle
* label="Select country"
* name="country"
* options={countries}
* />
* ```
*/
const ComboboxSingle = forwardRef(
<T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>>(
{ onChange, value, defaultValue, ...rest }: ComboboxSingleProps<T>,
ref: React.ForwardedRef<HTMLButtonElement>,
function ComboboxSingle<
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
>({ onChange, value, defaultValue, ...rest }: ComboboxSingleProps<T>) {
const [localValue, setLocalValue] = useControllableState({
defaultValue,
value,
onChange,
});
const onToggleOption: ComboboxProps<T>["onToggleOption"] = (
option,
isSelected,
) => {
const [localValue, setLocalValue] = useControllableState({
defaultValue,
value,
onChange,
});
setLocalValue(isSelected ? option.value : undefined);
}; // TODO: Dumt at ikke får hele option-objektet... Legge til i onChange? Hvis vi tilbyr onToggle så blir det så likt onChange...
const onToggleOption: ComboboxProps<T>["onToggleOption"] = (
option,
isSelected,
) => {
setLocalValue(isSelected ? option.value : null);
}; // TODO: Dumt at ikke får hele option-objektet... Legge til i onChange? Hvis vi tilbyr onToggle så blir det så likt onChange...
return (
<Combobox
{...rest}
ref={ref}
value={localValue ? [localValue] : []}
defaultValue={defaultValue ? [defaultValue] : []}
multiple={false}
onToggleOption={onToggleOption}
/>
);
},
) as <T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>>(
props: ComboboxSingleProps<T> & React.RefAttributes<HTMLButtonElement>,
) => React.ReactElement;
return (
<Combobox
{...rest}
value={localValue ? [localValue] : []}
defaultValue={defaultValue ? [defaultValue] : []}
multiple={false}
onToggleOption={onToggleOption}
/>
);
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ComboboxSingle {
@@ -157,7 +157,7 @@ export const Name = () => (
onSubmit={(event) => {
event.preventDefault();
const formData = new FormData(event.target);
const land = formData.get("land");
const land = formData.getAll("land");
console.log(land);
}}
>
@@ -168,7 +168,7 @@ export const Name = () => (
);
export const Controlled = () => {
const [value, setValue] = useState<MyOption["value"] | null>("opt-1");
const [value, setValue] = useState<MyOption["value"] | undefined>("opt-1");
return (
<VStack gap="space-8" minWidth="250px">
@@ -180,7 +180,7 @@ export const Controlled = () => {
/>
Selected: {value}
<div>
<button type="button" onClick={() => setValue(null)}>
<button type="button" onClick={() => setValue(undefined)}>
Clear
</button>
</div>
@@ -1,5 +1,5 @@
import type { Meta } from "@storybook/react-vite";
import React, { useRef, useState } from "react";
import React, { useState } from "react";
import { expect, userEvent, within } from "storybook/test";
import { ComboboxSingle } from "./Combobox";
import { ComboboxFilter } from "./filter/ComboboxFilter";
@@ -60,7 +60,7 @@ const groupedOptions = [
function BaseCombobox<
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
>(props: Partial<ComboboxSingle.Props<T>>) {
const [value, setValue] = useState<string | null>("opt-1");
const [value, setValue] = useState<string | undefined>("option-1");
return (
<ComboboxSingle
@@ -81,27 +81,6 @@ AccessibleNameAndDesc.play = async ({ canvasElement }) => {
await expect(trigger).toHaveAccessibleDescription("Landet hvor du er født.");
};
export const FocusWithRef = () => {
const ref = useRef<HTMLButtonElement>(null);
return (
<div>
<button type="button" onClick={() => ref.current?.focus()}>
Fokuser combobox
</button>
<ComboboxSingle ref={ref} options={countries} label="Velg land" />
</div>
);
};
FocusWithRef.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const combobox = await canvas.findByRole("combobox", { name: "Velg land" });
await expect(combobox).not.toHaveFocus();
await userEvent.click(
canvas.getByRole("button", { name: "Fokuser combobox" }),
);
await expect(combobox).toHaveFocus();
};
let optionMemoTestRenderCnt = 0;
const optionMemoTestRenderFn = (
optOrGroup: (typeof groupedOptions)[number],
@@ -114,7 +93,7 @@ const optionMemoTestRenderFn = (
return optOrGroup.label;
};
export const OptionMemoization = () => {
const [value, setValue] = useState(["opt-1"]);
const [value, setValue] = useState(["option-1"]);
return (
<div style={{ minHeight: "450px" }}>
@@ -1,7 +1,6 @@
import React, { forwardRef } from "react";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { cl } from "../../../utils/helpers";
import { useI18n } from "../../../utils/i18n/i18n.hooks";
import {
type ComboboxRootProps,
useComboboxRootContext,
@@ -15,14 +14,10 @@ interface ComboboxFieldProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
const ComboboxField = forwardRef<HTMLButtonElement, ComboboxFieldProps>(
({ children, hasError, ...rest }, ref) => {
const { options, value, size } = useComboboxRootContext();
const translate = useI18n("Combobox2");
const labels = getLabels(value, options);
const label =
children ??
(labels.length > 1
? translate("selectedCount", { count: labels.length })
: labels[0]);
children ?? (labels.length > 1 ? `${labels.length} valgt` : labels[0]);
return (
<div className="aksel-combobox2__field-container">
@@ -42,7 +42,6 @@ export const ComboboxFilter = (props: ComboboxFilterProps) => {
}
}}
aria-autocomplete="list"
aria-expanded="true"
inputMode="text"
/>
</Listbox.InputSlot>
@@ -55,7 +55,7 @@ function ComboboxListContent<
useComboboxRootContext();
const memoizedOnToggleOption = useEventCallback(onToggleOption);
const deferredFilterString = useDeferredValue(filterString);
const maybeAnnouceHits = useHitsAnnouncer(deferredFilterString);
const maybeAnnouceHits = useHitsAnnouncer(deferredFilterString, filterString);
const translate = useI18n("Combobox2");
const localOnToggleOption = useCallback(
@@ -80,7 +80,6 @@ function ComboboxListContent<
maybeAnnouceHits(filteredOptions);
if (filteredOptions.length === 0) {
// TODO: Consider moving outside of role=listbox
return (
<div className="aksel-combobox2__no-results">{translate("noHits")}</div>
);
@@ -18,9 +18,9 @@ function getNumberOfOptions<T extends ComboboxOptionData>(
}, 0);
}
function useHitsAnnouncer(deferredFilterString: string) {
function useHitsAnnouncer(deferredFilterString: string, filterString: string) {
const translate = useI18n("Combobox2");
const oldFilterStringRef = useRef(deferredFilterString);
const shouldAnnounceRef = useRef(false);
const announce = useAnnounce();
const announceHits = useMemo(() => {
@@ -50,20 +50,20 @@ function useHitsAnnouncer(deferredFilterString: string) {
ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>
)[],
) => {
// Announce number of hits when filter string changes.
// We can't do this in a useEffect b.c. of how ComboboxList is structured. Consider refactoring.
if (deferredFilterString === oldFilterStringRef.current) {
return;
// The options are filtered based on deferredFilterString, so we need to wait for it to settle.
if (deferredFilterString !== filterString) {
shouldAnnounceRef.current = true;
} else if (shouldAnnounceRef.current) {
if (filteredOptions.length === 0) {
announceHits.clear();
announceNoHits();
} else {
const hits = getNumberOfOptions(filteredOptions);
announceNoHits.clear();
announceHits(hits);
}
shouldAnnounceRef.current = false;
}
if (filteredOptions.length === 0) {
announceHits.clear();
announceNoHits();
} else {
const hits = getNumberOfOptions(filteredOptions);
announceNoHits.clear();
announceHits(hits);
}
oldFilterStringRef.current = deferredFilterString;
};
}
@@ -3,7 +3,6 @@ import { CheckmarkHeavyIcon } from "@navikt/aksel-icons";
import { Listbox } from "../../../utils/components/Listbox/root/ListboxRoot";
import type { ComboboxListProps } from "../list/ComboboxList";
import type { ComboboxOptionData } from "../root/ComboboxRoot";
import { highlightSubstring } from "./highlightSubstring";
export interface ComboboxOptionProps<T extends ComboboxOptionData> {
option: T;
@@ -50,6 +49,31 @@ function ComboboxOptionComponent<T extends ComboboxOptionData>({
);
}
const highlightSubstring = (text: string, substring: string) => {
const indexOfHighlightedText = text
.toLocaleLowerCase()
.indexOf(substring.toLocaleLowerCase());
if (indexOfHighlightedText === -1) {
// This can happen if the consumer has implemented their own filtering logic
return text;
}
const start = text.substring(0, indexOfHighlightedText);
const highlight = text.substring(
indexOfHighlightedText,
indexOfHighlightedText + substring.length,
);
const end = text.substring(indexOfHighlightedText + substring.length);
return (
// Aria-label is used to fix testing-library wrongly evaluating the accessible name of the option when highlighting text
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: Doesn't matter if aria-label doesn't work
<span aria-label={text}>
{start}
{highlight && <mark>{highlight}</mark>}
{end}
</span>
);
};
export const ComboboxOption = React.memo(
ComboboxOptionComponent,
) as typeof ComboboxOptionComponent;
@@ -1,27 +0,0 @@
import { describe, expect, test } from "vitest";
import { getHighlightRange } from "./highlightSubstring";
const highlighted = (text: string, substring: string) => {
const range = getHighlightRange(text, substring);
return range && text.substring(...range);
};
describe("getHighlightRange", () => {
test("matches case-insensitively", () => {
expect(getHighlightRange("Norge", "OR")).toEqual([1, 3]);
});
test("returns null when there is no match", () => {
expect(getHighlightRange("Norge", "x")).toBeUndefined();
});
test("handles characters that get longer when lowercased", () => {
expect(highlighted("İstanbul", "an")).toBe("an");
expect(highlighted("İzmir og Antalya", "an")).toBe("An");
expect(highlighted("İzmir", "İz")).toBe("İz");
});
test("includes the whole original character when only part of its lowercase form matches", () => {
expect(highlighted("İstanbul", "i")).toBe("İ");
});
});
@@ -1,50 +0,0 @@
import React from "react";
function highlightSubstring(text: string, substring: string) {
const range = getHighlightRange(text, substring);
if (!range) {
// This can happen if the consumer has implemented their own filtering logic
return text;
}
const [rangeStart, rangeEnd] = range;
return (
// Aria-label is used to fix testing-library wrongly evaluating the accessible name of the option when highlighting text
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: Doesn't matter if it doesn't work in normal browsers
<span aria-label={text}>
{text.substring(0, rangeStart)}
<mark>{text.substring(rangeStart, rangeEnd)}</mark>
{text.substring(rangeEnd)}
</span>
);
}
/**
* Finds the first case-insensitive occurrence of `substring` in `text`.
* Handles characters whose lowercase form has a different length than the original (e.g. "İ").
* @returns The `[start, end]` range of the match in `text`, or `undefined` if there is no match.
*/
function getHighlightRange(text: string, substring: string) {
const lowerSubstring = substring.toLocaleLowerCase();
const matchIndex = text.toLocaleLowerCase().indexOf(lowerSubstring);
if (matchIndex === -1) {
return;
}
const matchEnd = matchIndex + lowerSubstring.length;
let start = 0;
let origPos = 0;
let lowerPos = 0;
for (const char of text) {
if (lowerPos <= matchIndex) {
start = origPos;
}
origPos += char.length;
lowerPos += char.toLocaleLowerCase().length;
if (lowerPos >= matchEnd) {
return [start, origPos] as const;
}
}
}
export { highlightSubstring, getHighlightRange };
@@ -1,4 +1,4 @@
import React, { useLayoutEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Listbox } from "../../../utils/components/Listbox/root/ListboxRoot";
import { createStrictContext } from "../../../utils/helpers";
import { useComboboxRootContext } from "../root/ComboboxRoot";
@@ -29,7 +29,7 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
const rootContext = useComboboxRootContext();
const ref = useRef<HTMLDivElement | null>(null);
useLayoutEffect(
useEffect(
function onOpen() {
const canHover = window.matchMedia("(hover: hover)").matches;
@@ -1,10 +1,8 @@
import {
compareAsc,
compareDesc,
format,
isSameMonth,
setYear,
} from "date-fns";
import { compareAsc } from "date-fns/compareAsc";
import { compareDesc } from "date-fns/compareDesc";
import { format } from "date-fns/format";
import { isSameMonth } from "date-fns/isSameMonth";
import { setYear } from "date-fns/setYear";
import React, { useEffect, useRef } from "react";
import { dateIsInCurrentMonth, isMatch } from "../../utils/date";
import { cl } from "../../utils/helpers";
@@ -1,11 +1,9 @@
import {
format,
isAfter,
isBefore,
setYear,
startOfMonth,
startOfYear,
} from "date-fns";
import { format } from "date-fns/format";
import { isAfter } from "date-fns/isAfter";
import { isBefore } from "date-fns/isBefore";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import React from "react";
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
import { Button } from "../../button";
@@ -1,4 +1,5 @@
import { type Locale, isSameYear } from "date-fns";
import type { Locale } from "date-fns";
import { isSameYear } from "date-fns/isSameYear";
import React from "react";
import { clampDisplayYear } from "../../utils/date";
import { createStrictContext } from "../../utils/helpers";
@@ -1,4 +1,4 @@
import { setYear } from "date-fns";
import { setYear } from "date-fns/setYear";
import type { Matcher } from "../../utils/components/date/Date.typeutils";
import { isMatch } from "../../utils/date";
@@ -1,4 +1,7 @@
import { isSameMonth, setMonth, setYear, startOfMonth } from "date-fns";
import { isSameMonth } from "date-fns/isSameMonth";
import { setMonth } from "date-fns/setMonth";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import React, { useState } from "react";
import { BodyShort } from "../../typography";
import { isMatch } from "../../utils/date";
@@ -1,19 +1,17 @@
import {
type Locale,
addDays,
addMonths,
addYears,
differenceInDays,
differenceInMonths,
differenceInYears,
endOfMonth,
endOfYear,
format,
startOfDay,
startOfMonth,
startOfYear,
subDays,
} from "date-fns";
import type { Locale } from "date-fns";
import { addDays } from "date-fns/addDays";
import { addMonths } from "date-fns/addMonths";
import { addYears } from "date-fns/addYears";
import { differenceInDays } from "date-fns/differenceInDays";
import { differenceInMonths } from "date-fns/differenceInMonths";
import { differenceInYears } from "date-fns/differenceInYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { format } from "date-fns/format";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { subDays } from "date-fns/subDays";
import React, { useCallback, useMemo } from "react";
import { Detail } from "../../typography/Detail";
import { useDateLocale, useI18n } from "../../utils/i18n/i18n.hooks";
@@ -1,4 +1,4 @@
import { isBefore } from "date-fns";
import { isBefore } from "date-fns/isBefore";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../root/TimelineRoot.context";
import {
@@ -12,7 +12,7 @@ import {
useHover,
useInteractions,
} from "@floating-ui/react";
import { format } from "date-fns";
import { format } from "date-fns/format";
import React, { forwardRef, useMemo, useState } from "react";
import { useMergeRefs } from "../../utils/hooks";
import { useI18n } from "../../utils/i18n/i18n.hooks";
@@ -1,4 +1,6 @@
import { endOfDay, isSameDay, startOfDay } from "date-fns";
import { endOfDay } from "date-fns/endOfDay";
import { isSameDay } from "date-fns/isSameDay";
import { startOfDay } from "date-fns/startOfDay";
import React, { forwardRef, useMemo, useState } from "react";
import { consoleWarning } from "../../utils/helpers/consoleWarning";
import { TimelineAxisLabelsInternal } from "../axis-labels/TimelineAxisLabelsInternal";
@@ -1,5 +1,6 @@
import { renderHook } from "@testing-library/react";
import { addDays, isSameDay } from "date-fns";
import { addDays } from "date-fns/addDays";
import { isSameDay } from "date-fns/isSameDay";
import { describe, expect, test } from "vitest";
import {
useEarliestDate,
@@ -1,4 +1,8 @@
import { addDays, endOfDay, isAfter, startOfDay, subDays } from "date-fns";
import { addDays } from "date-fns/addDays";
import { endOfDay } from "date-fns/endOfDay";
import { isAfter } from "date-fns/isAfter";
import { startOfDay } from "date-fns/startOfDay";
import { subDays } from "date-fns/subDays";
import { useMemo } from "react";
import { horizontalPositionAndWidth } from "../utils/calc";
import { invisiblePeriods, withinADay } from "../utils/filter";
@@ -1,4 +1,4 @@
import { format } from "date-fns";
import { format } from "date-fns/format";
import React, { forwardRef } from "react";
import { BodyShort } from "../../typography/BodyShort";
import { cl } from "../../utils/helpers";
@@ -1,4 +1,4 @@
import { differenceInMilliseconds } from "date-fns";
import { differenceInMilliseconds } from "date-fns/differenceInMilliseconds";
const position = (date: Date, start: Date, endInclusive: Date) => {
const diff = differenceInMilliseconds(endInclusive, start);
@@ -1,4 +1,4 @@
import { differenceInDays } from "date-fns";
import { differenceInDays } from "date-fns/differenceInDays";
import type { PeriodProps } from "../Timeline.types";
import type { Spatial } from "./types.internal";
@@ -1,4 +1,4 @@
import { format } from "date-fns";
import { format } from "date-fns/format";
import { cl } from "../../utils/helpers";
import type { TFunction } from "../../utils/i18n/i18n.types";
import type { PeriodProps } from "../Timeline.types";
@@ -1,4 +1,4 @@
import { differenceInMilliseconds } from "date-fns";
import { differenceInMilliseconds } from "date-fns/differenceInMilliseconds";
import type { Period, PositionedPeriod } from "./types.internal";
const lastDate = (a: Date, b: Date): number => differenceInMilliseconds(a, b);
@@ -1,11 +1,9 @@
import {
addMonths,
addYears,
format,
isSameDay,
subMonths,
subYears,
} from "date-fns";
import { addMonths } from "date-fns/addMonths";
import { addYears } from "date-fns/addYears";
import { format } from "date-fns/format";
import { isSameDay } from "date-fns/isSameDay";
import { subMonths } from "date-fns/subMonths";
import { subYears } from "date-fns/subYears";
import React, { forwardRef } from "react";
import { Detail } from "../../typography/Detail";
import { useI18n } from "../../utils/i18n/i18n.hooks";
@@ -39,12 +39,4 @@ function ListboxOptions({ children, ...rest }: ListboxOptionsProps) {
);
}
/**
* TODO: Things that should be fixed before we allow focus directly
* on the listbox (i.e. when there's no filter input/combobox elm.)
* - Set aria-activedescendant
* - Consider aria-multiselectable
* - Probably aria-label(ledby)?
*/
export { ListboxOptions };
@@ -1,4 +1,6 @@
import { enGB, nb, nn } from "date-fns/locale";
import { enGB } from "date-fns/locale/en-GB";
import { nb } from "date-fns/locale/nb";
import { nn } from "date-fns/locale/nn";
import { createStrictContext } from "../../helpers";
import type { TFunction } from "../../i18n/i18n.types";
import en_translations from "../../i18n/locales/en";
@@ -1,11 +1,9 @@
import {
addYears,
endOfMonth,
endOfYear,
startOfDay,
startOfMonth,
startOfYear,
} from "date-fns";
import { addYears } from "date-fns/addYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { describe, expect, test } from "vitest";
import { calendarRange } from "./calendar-range";
@@ -1,11 +1,9 @@
import {
addYears,
endOfMonth,
endOfYear,
startOfDay,
startOfMonth,
startOfYear,
} from "date-fns";
import { addYears } from "date-fns/addYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
/**
* Generates the min and max-dates possible to show and navigate to in the calendar.
@@ -1,4 +1,4 @@
import { setYear } from "date-fns";
import { setYear } from "date-fns/setYear";
import { describe, expect, test } from "vitest";
import { dateIsInCurrentMonth, isValidDate } from "./check-dates";
@@ -1,9 +1,7 @@
import {
differenceInCalendarDays,
isThisMonth,
setYear,
startOfDay,
} from "date-fns";
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isThisMonth } from "date-fns/isThisMonth";
import { setYear } from "date-fns/setYear";
import { startOfDay } from "date-fns/startOfDay";
export const dateIsInCurrentMonth = (
date: Date,
@@ -1,4 +1,5 @@
import { startOfMonth, startOfYear } from "date-fns";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { describe, expect, test } from "vitest";
import { isDateOutsideRange } from "./check-dates";
import { clampDisplayMonth, clampDisplayYear } from "./clamp-dates";
@@ -1,4 +1,7 @@
import { isAfter, isBefore, startOfMonth, startOfYear } from "date-fns";
import { isAfter } from "date-fns/isAfter";
import { isBefore } from "date-fns/isBefore";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
/**
* Makes sure the month is within the min and max daterange to avoid showing disabled months
@@ -1,4 +1,4 @@
import { isSameDay } from "date-fns";
import { isSameDay } from "date-fns/isSameDay";
import { isDateRange } from "../components/date/Date.typeutils";
export const disableDate = (
@@ -1,4 +1,4 @@
import { nb } from "date-fns/locale";
import { nb } from "date-fns/locale/nb";
import { describe, expect, test } from "vitest";
import { getMonthOptions, getYearOptions } from "./dropdown-options";
@@ -1,16 +1,14 @@
import {
type Locale,
addYears,
eachMonthOfInterval,
endOfYear,
format,
getMonth,
getYear,
isBefore,
isSameYear,
startOfMonth,
startOfYear,
} from "date-fns";
import type { Locale } from "date-fns";
import { addYears } from "date-fns/addYears";
import { eachMonthOfInterval } from "date-fns/eachMonthOfInterval";
import { endOfYear } from "date-fns/endOfYear";
import { format } from "date-fns/format";
import { getMonth } from "date-fns/getMonth";
import { getYear } from "date-fns/getYear";
import { isBefore } from "date-fns/isBefore";
import { isSameYear } from "date-fns/isSameYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
/** Return the months to show in the dropdown. */
export function getMonthOptions({
@@ -1,4 +1,5 @@
import { type Locale, format } from "date-fns";
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import {
INPUT_DATE_STRING_FORMAT_DATE,
INPUT_DATE_STRING_FORMAT_MONTH,
@@ -1,4 +1,4 @@
import { nb } from "date-fns/locale";
import { nb } from "date-fns/locale/nb";
import { describe, expect, test } from "vitest";
import { formatDateForInput } from "./format-date";
import { parseDate } from "./parse-date";
@@ -1,9 +1,7 @@
import {
differenceInCalendarDays,
isDate,
isSameDay,
isSameMonth,
} from "date-fns";
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isDate } from "date-fns/isDate";
import { isSameDay } from "date-fns/isSameDay";
import { isSameMonth } from "date-fns/isSameMonth";
import {
type DateRange,
type Matcher,
@@ -1,4 +1,7 @@
import { type Locale, isBefore, parse, sub } from "date-fns";
import type { Locale } from "date-fns";
import { isBefore } from "date-fns/isBefore";
import { parse } from "date-fns/parse";
import { sub } from "date-fns/sub";
import { isValidDate } from "./check-dates";
export const INPUT_DATE_STRING_FORMAT_DATE = "dd.MM.yyyy";
@@ -1,5 +1,5 @@
import { getMonth } from "date-fns";
import { nb } from "date-fns/locale";
import { getMonth } from "date-fns/getMonth";
import { nb } from "date-fns/locale/nb";
import { describe, expect, test } from "vitest";
import { isValidDate } from "./check-dates";
import { parseDate } from "./parse-date";
@@ -1,4 +1,5 @@
import { isBefore, isSameDay } from "date-fns";
import { isBefore } from "date-fns/isBefore";
import { isSameDay } from "date-fns/isSameDay";
import type { DateRange } from "../components/date/Date.typeutils";
/**
@@ -5,8 +5,6 @@ const internalTranslations = {
selected: "{count} rows selected",
},
Combobox2: {
/** The "value" that is shown when multiple options are selected. */
selectedCount: "{count} valgt",
/** Label for filter input. Only visible to screen readers. */
filter: "Filtrer",
noHits: "Ingen treff",
@@ -1,4 +1,4 @@
import { enGB } from "date-fns/locale";
import { enGB } from "date-fns/locale/en-GB";
import type { Translations } from "../i18n.types";
export default {
@@ -1,4 +1,5 @@
import { type Locale, nb } from "date-fns/locale";
import type { Locale } from "date-fns";
import { nb } from "date-fns/locale/nb";
interface TranslationMap {
[component: string]: Record<string, string | Record<string, string> | Locale>;
@@ -1,4 +1,4 @@
import { nn } from "date-fns/locale";
import { nn } from "date-fns/locale/nn";
import type { Translations } from "../i18n.types";
export default {
+25
View File
@@ -167,6 +167,31 @@ module.exports = defineConfig([
"import/no-named-export": "error",
},
},
{
files: ["@navikt/core/react/src/**/*.{ts,tsx}"],
ignores: ["**/*.stories.tsx"],
rules: {
"@typescript-eslint/no-restricted-imports": [
"error",
{
paths: [
{
name: "date-fns",
allowTypeImports: true,
message:
'Import functions from their own path, e.g. `import { format } from "date-fns/format"`. The root barrel makes unbundled Node imports load all of date-fns.',
},
{
name: "date-fns/locale",
allowTypeImports: true,
message:
'Import locales from their own path, e.g. `import { nb } from "date-fns/locale/nb"`. The barrel makes unbundled Node imports load all locales.',
},
],
},
],
},
},
{
files: ["@navikt/**"],
ignores: ["**/*.stories.tsx"],