From 20a4bcbd0c0be4a06a5425282c8b76822dd9d9d0 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 23 Jan 2025 11:32:55 +0100 Subject: [PATCH 01/49] :memo: Bumped date-fns to v4 --- @navikt/core/react/package.json | 2 +- aksel.nav.no/website/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/@navikt/core/react/package.json b/@navikt/core/react/package.json index bccd24bda7..88c66fa62b 100644 --- a/@navikt/core/react/package.json +++ b/@navikt/core/react/package.json @@ -618,7 +618,7 @@ "@navikt/aksel-icons": "^7.10.0", "@navikt/ds-tokens": "^7.10.0", "clsx": "^2.1.0", - "date-fns": "^3.0.0", + "date-fns": "^4.0.0", "react-day-picker": "8.10.1" }, "devDependencies": { diff --git a/aksel.nav.no/website/package.json b/aksel.nav.no/website/package.json index b5861c8263..5953ab60cf 100644 --- a/aksel.nav.no/website/package.json +++ b/aksel.nav.no/website/package.json @@ -49,7 +49,7 @@ "boring-avatars": "1.10.1", "clsx": "^2.1.0", "codesandbox-import-utils": "^2.2.3", - "date-fns": "^3.0.0", + "date-fns": "^4.0.0", "fuse.js": "^7.0.0", "jscodeshift": "^0.15.1", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 173e5d828a..438bce3887 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3776,7 +3776,7 @@ __metadata: aksel: "workspace:^" clsx: "npm:^2.1.0" concurrently: "npm:9.0.1" - date-fns: "npm:^3.0.0" + date-fns: "npm:^4.0.0" fast-glob: "npm:3.2.11" jscodeshift: "npm:^0.15.1" jsdom: "npm:25.0.1" @@ -10124,10 +10124,10 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^3.0.0": - version: 3.6.0 - resolution: "date-fns@npm:3.6.0" - checksum: 10/cac35c58926a3b5d577082ff2b253612ec1c79eb6754fddef46b6a8e826501ea2cb346ecbd211205f1ba382ddd1f9d8c3f00bf433ad63cc3063454d294e3a6b8 +"date-fns@npm:^4.0.0": + version: 4.1.0 + resolution: "date-fns@npm:4.1.0" + checksum: 10/d5f6e9de5bbc52310f786099e18609289ed5e30af60a71e0646784c8185ddd1d0eebcf7c96b7faaaefc4a8366f3a3a4244d099b6d0866ee2bec80d1361e64342 languageName: node linkType: hard @@ -24761,7 +24761,7 @@ __metadata: codesandbox-import-utils: "npm:^2.2.3" copyfiles: "npm:^2.4.1" cross-env: "npm:^7.0.0" - date-fns: "npm:^3.0.0" + date-fns: "npm:^4.0.0" fuse.js: "npm:^7.0.0" jscodeshift: "npm:^0.15.1" jsdom: "npm:25.0.1" From 3399c08ac1e1723d4d6e1694d1c0a0943af478c3 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 23 Jan 2025 13:16:22 +0100 Subject: [PATCH 02/49] TEMP UPDATES --- @navikt/core/react/package.json | 2 +- .../react/src/date/datepicker/DatePicker.tsx | 30 +++++--- .../date/datepicker/DatePickerStandalone.tsx | 75 ++++++++++++++++++- .../date/datepicker/datepicker.stories.tsx | 10 +-- .../src/date/datepicker/parts/DayButton.tsx | 11 +-- .../date/datepicker/parts/DropdownCaption.tsx | 58 ++++++++------ .../react/src/date/datepicker/parts/Row.tsx | 2 +- .../src/date/datepicker/parts/WeekNumber.tsx | 36 +++++---- .../core/react/src/date/datepicker/types.ts | 13 +++- yarn.lock | 33 ++++++-- 10 files changed, 199 insertions(+), 71 deletions(-) diff --git a/@navikt/core/react/package.json b/@navikt/core/react/package.json index 88c66fa62b..99faf58005 100644 --- a/@navikt/core/react/package.json +++ b/@navikt/core/react/package.json @@ -619,7 +619,7 @@ "@navikt/ds-tokens": "^7.10.0", "clsx": "^2.1.0", "date-fns": "^4.0.0", - "react-day-picker": "8.10.1" + "react-day-picker": "9.5.0" }, "devDependencies": { "@testing-library/dom": "10.4.0", diff --git a/@navikt/core/react/src/date/datepicker/DatePicker.tsx b/@navikt/core/react/src/date/datepicker/DatePicker.tsx index 6b0c221f36..5314685790 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -83,6 +83,8 @@ export const DatePicker = forwardRef( onOpenToggle, strategy, onWeekNumberClick, + fromDate, + toDate, ...rest }, ref, @@ -122,17 +124,27 @@ export const DatePicker = forwardRef( const DatePickerComponent = ( ( + // + // ), */ + // /* Row, */ + // }} className={cl("navds-date", className)} classNames={{ vhidden: "navds-sr-only", @@ -152,7 +164,7 @@ export const DatePicker = forwardRef( onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} fixedWeeks showOutsideDays - {...omit(rest, ["onSelect"])} + {...omit(rest, ["onSelect", "role"])} /> ); diff --git a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx index 2f1d01ec27..5d64c6b505 100644 --- a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx @@ -2,6 +2,9 @@ import cl from "clsx"; import { isWeekend } from "date-fns"; import React, { forwardRef } from "react"; import { DateRange, DayPicker, isMatch } from "react-day-picker"; +import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; +import { Button } from "../../button"; +import { Select } from "../../form/select"; import { omit } from "../../util"; import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks"; import { DateTranslationContextProvider } from "../context"; @@ -93,16 +96,84 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< > ( + + ), + YearsDropdown: ({ options, value, onChange }) => ( + + ), + DropdownNav: ({ children }) => { + return
{children}
; + }, + PreviousMonthButton: ({ onClick, disabled: _disabled }) => ( + + ); + }, + + Nav: ({ + nextMonth, + onNextClick, + previousMonth, + onPreviousClick, + className: _className, + }) => { + return ( +
+
+ ); }, /* Caption: dropdownCaption ? DropdownCaption : Caption, @@ -176,7 +237,6 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< Row, */ }} className="navds-date" - classNames={{ vhidden: "navds-sr-only" }} disabled={(day) => { return ( (disableWeekends && isWeekend(day)) || isMatch(day, disabled) diff --git a/@navikt/core/react/src/util/i18n/locales/nb.ts b/@navikt/core/react/src/util/i18n/locales/nb.ts index f0dd6a091c..2db6204f48 100644 --- a/@navikt/core/react/src/util/i18n/locales/nb.ts +++ b/@navikt/core/react/src/util/i18n/locales/nb.ts @@ -7,135 +7,211 @@ interface TranslationMap { export default { global: { dateLocale: nb, + /** @default "Vis mer" */ showMore: "Vis mer", + /** @default "Vis mindre" */ showLess: "Vis mindre", + /** @default "Skrivebeskyttet" */ readOnly: "Skrivebeskyttet", + /** @default "Lukk" */ close: "Lukk", }, Alert: { + /** @default "Lukk varsel" */ closeAlert: "Lukk varsel", + /** @default "Lukk melding" */ closeMessage: "Lukk melding", + /** @default "Feil" */ error: "Feil", + /** @default "Informasjon" */ info: "Informasjon", + /** @default "Suksess" */ success: "Suksess", + /** @default "Advarsel" */ warning: "Advarsel", }, Chips: { Removable: { - /** Will be appended to the accessible name for the button. */ + /** Will be appended to the accessible name for the button. + * @default "slett" */ labelSuffix: "slett", }, }, Combobox: { - /** The input value will be appended to the end of this text, e.g. `Legg til "input value"`. */ + /** The input value will be appended to the end of this text, e.g. `Legg til "input value"`. + * @default "Legg til" */ addOption: "Legg til", - /** Loader title */ + /** Loader title + * @default "Søker…" */ loading: "Søker…", + /** @default "{selected} av maks {limit} er valgt." */ maxSelected: "{selected} av maks {limit} er valgt.", }, CopyButton: { + /** @default "Kopier" */ title: "Kopier", + /** @default "Kopiert!" */ activeText: "Kopiert!", }, DatePicker: { + /** @default "Velg dato" */ chooseDate: "Velg dato", + /** @default "Velg datoer" */ chooseDates: "Velg datoer", + /** @default "Velg start- og sluttdato" */ chooseDateRange: "Velg start- og sluttdato", + /** @default "Velg måned" */ chooseMonth: "Velg måned", + /** @default "Uke" */ week: "Uke", + /** @default "Uke {week}" */ weekNumber: "Uke {week}", + /** @default "Velg uke {week}" */ selectWeekNumber: "Velg uke {week}", + /** @default "Måned" */ month: "Måned", + /** @default "Gå til neste måned" */ goToNextMonth: "Gå til neste måned", + /** @default "Gå til forrige måned" */ goToPreviousMonth: "Gå til forrige måned", + /** @default "År" */ year: "År", + /** @default "Gå til neste år" */ goToNextYear: "Gå til neste år", + /** @default "Gå til forrige år" */ goToPreviousYear: "Gå til forrige år", + /** @default "Åpne datovelger" */ openDatePicker: "Åpne datovelger", + /** @default "Åpne månedsvelger" */ openMonthPicker: "Åpne månedsvelger", + /** @default "Lukk datovelger" */ closeDatePicker: "Lukk datovelger", + /** @default "Lukk månedsvelger" */ closeMonthPicker: "Lukk månedsvelger", }, ErrorSummary: { + /** @default "Du må rette disse feilene før du kan fortsette:" */ heading: "Du må rette disse feilene før du kan fortsette:", }, FileUpload: { dropzone: { + /** @default "Velg fil" */ button: "Velg fil", + /** @default "Velg filer" */ buttonMultiple: "Velg filer", + /** @default "Dra og slipp filen her" */ dragAndDrop: "Dra og slipp filen her", + /** @default "Dra og slipp filer her" */ dragAndDropMultiple: "Dra og slipp filer her", + /** @default "Slipp" */ drop: "Slipp", + /** @default "eller" */ or: "eller", + /** @default "Filopplasting er deaktivert" */ disabled: "Filopplasting er deaktivert", + /** @default "Du kan ikke laste opp flere filer" */ disabledFilelimit: "Du kan ikke laste opp flere filer", }, item: { + /** @default "Prøv å laste opp filen på nytt" */ retryButtonTitle: "Prøv å laste opp filen på nytt", + /** @default "Slett filen" */ deleteButtonTitle: "Slett filen", + /** @default "Laster opp…" */ uploading: "Laster opp…", + /** @default "Laster ned…" */ downloading: "Laster ned…", }, }, FormProgress: { + /** @default "Steg {activeStep} av {totalSteps}" */ step: "Steg {activeStep} av {totalSteps}", + /** @default "Vis alle steg" */ showAllSteps: "Vis alle steg", + /** @default "Skjul alle steg" */ hideAllSteps: "Skjul alle steg", }, FormSummary: { + /** @default "Endre svar" */ editAnswer: "Endre svar", }, GuidePanel: { + /** @default "Illustrasjon av veileder" */ illustrationLabel: "Illustrasjon av veileder", }, HelpText: { + /** @default "Mer informasjon" */ title: "Mer informasjon", }, Loader: { + /** @default "Venter…" */ title: "Venter…", }, Pagination: { + /** @default "Forrige" */ previous: "Forrige", + /** @default "Neste" */ next: "Neste", }, ProgressBar: { + /** @default "{current} av {max}" */ progress: "{current} av {max}", - progressUnknown: - "Fremdrift kan ikke beregnes, antatt tid er {seconds} sekunder.", + /** @default "Fremdrift kan ikke beregnes, antatt tid er {seconds} sekunder." */ + progressUnknown: "Fremdrift kan ikke beregnes, antatt tid er {seconds} sekunder.", }, Search: { + /** @default "Tøm feltet" */ clear: "Tøm feltet", + /** @default "Søk" */ search: "Søk", }, Textarea: { - /** Screen readers only */ + /** Screen readers only + * @default "Tekstområde med plass til {maxLength} tegn." */ maxLength: "Tekstområde med plass til {maxLength} tegn.", + /** @default "{chars} tegn for mye" */ charsTooMany: "{chars} tegn for mye", + /** @default "{chars} tegn igjen" */ charsLeft: "{chars} tegn igjen", }, Timeline: { + /** @default "dd.MM.yyyy" */ dateFormat: "dd.MM.yyyy", + /** @default "dd.MM" */ dayFormat: "dd.MM", + /** @default "MMM yy" */ monthFormat: "MMM yy", + /** @default "yyyy" */ yearFormat: "yyyy", Row: { + /** @default "Ingen perioder" */ noPeriods: "Ingen perioder", + /** @default "{start} til {end}" */ period: "{start} til {end}", }, Period: { + /** @default "Suksess" */ success: "Suksess", + /** @default "Advarsel" */ warning: "Advarsel", + /** @default "Fare" */ danger: "Fare", + /** @default "Info" */ info: "Info", + /** @default "Nøytral" */ neutral: "Nøytral", + /** @default "{status} fra {start} til {end}" */ period: "{status} fra {start} til {end}", }, Pin: { + /** @default "Pin: {date}" */ pin: "Pin: {date}", }, Zoom: { + /** @default "Zoom tidslinjen {start} til {end}" */ zoom: "Zoom tidslinjen {start} til {end}", + /** @default "Tilbakestill tidsperspektiv" */ reset: "Tilbakestill tidsperspektiv", }, }, From 0b96047e08b4df6e82941df3640de7859fcc4f44 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 23 Jan 2025 23:38:16 +0100 Subject: [PATCH 04/49] :sparkles: New Caption component for datepicker --- @navikt/core/css/date.css | 127 +++--------- .../react/src/date/datepicker/DatePicker.tsx | 71 ++++--- .../date/datepicker/DatePickerStandalone.tsx | 182 ++++-------------- .../datepicker/new-util/getMonthOptions.ts | 39 ++++ .../date/datepicker/new-util/getNavMonths.ts | 68 +++++++ .../datepicker/new-util/getYearOptions.ts | 44 +++++ .../src/date/datepicker/parts/Months.tsx | 131 +++++++++++++ .../date/datepicker/parts/NewDayButton.tsx | 35 ++++ 8 files changed, 430 insertions(+), 267 deletions(-) create mode 100644 @navikt/core/react/src/date/datepicker/new-util/getMonthOptions.ts create mode 100644 @navikt/core/react/src/date/datepicker/new-util/getNavMonths.ts create mode 100644 @navikt/core/react/src/date/datepicker/new-util/getYearOptions.ts create mode 100644 @navikt/core/react/src/date/datepicker/parts/Months.tsx create mode 100644 @navikt/core/react/src/date/datepicker/parts/NewDayButton.tsx diff --git a/@navikt/core/css/date.css b/@navikt/core/css/date.css index 28e0142899..0a6a6f7453 100644 --- a/@navikt/core/css/date.css +++ b/@navikt/core/css/date.css @@ -2,98 +2,24 @@ padding: var(--a-spacing-4) var(--a-spacing-3); } -@media (min-width: 480px) { - .navds-date { - padding: var(--a-spacing-5) var(--a-spacing-4); - } -} - -.navds-date__months { - position: relative; +.navds-date .rdp-day_range_middle.rdp-day_disabled { + color: var(--ac-date-middle-text, var(--a-text-on-action)); + background: var(--ac-date-middle-bg, var(--a-surface-action-selected)); } -.navds-date__month { +.navds-date .rdp-month, +.navds-date.rdp-month { display: grid; gap: var(--a-spacing-5); } -.navds-date-dropdowns { - display: flex; - width: fit-content; - gap: var(--a-spacing-2); -} - -.navds-date__nav { - position: absolute; - inset-block-start: 0; - inset-inline-end: 0; - display: flex; - align-items: center; - gap: var(--a-spacing-1); -} - -.navds-date__weekday { - text-transform: capitalize; - font-size: var(--a-font-size-small); -} - -.navds-date__month-dropdown select { +.navds-date__caption-label { text-transform: capitalize; } -.navds-date__day-button { - all: unset; - display: flex; - align-items: center; - justify-content: center; - width: 2.5rem; - height: 2.5rem; - border-radius: var(--a-border-radius-medium); - cursor: pointer; -} - -@media (min-width: 480px) { - .navds-date__day-button { - width: 3rem; - height: 3rem; - } -} - -/* -@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); -} */ - -.navds-date__day-button:hover { - background: var(--ac-date-hover-bg, var(--a-surface-action-subtle-hover)); -} - -.navds-date__day-button[data-pressed="true"] { - color: var(--ac-date-selected-text, var(--a-text-on-action)); - background: var(--ac-date-selected-bg, var(--a-surface-action-selected)); - cursor: pointer; -} - -/* - -.navds-date .rdp-day_range_middle.rdp-day_disabled { - color: var(--ac-date-middle-text, var(--a-text-on-action)); - background: var(--ac-date-middle-bg, var(--a-surface-action-selected)); -} - - -.navds-date__caption-label { +.navds-date .rdp-head_cell { text-transform: capitalize; + font-size: var(--a-font-size-small); } .navds-date .rdp-weeknumber { @@ -108,7 +34,7 @@ color: var(--a-text-subtle); } -.navds-date .rdp-weeknumber.rdp-day_button { +.navds-date .rdp-weeknumber.rdp-button { width: 2rem; height: 2rem; box-shadow: 0 0 0 1px var(--a-border-default); @@ -116,12 +42,24 @@ font-size: var(--a-font-size-small); } -.navds-date .rdp-weeknumber.rdp-day_button:active { +.navds-date .rdp-weeknumber.rdp-button:active { background-color: var(--a-surface-action-active); color: var(--a-text-on-action); box-shadow: none; } +.navds-date__caption__month .navds-select__container select { + text-transform: capitalize; +} + +.navds-date .rdp-button { + all: unset; + display: block; + width: 2.5rem; + height: 2.5rem; + text-align: center; + border-radius: var(--a-border-radius-medium); +} .navds-date .rdp-day_range_start { border-radius: var(--a-border-radius-xlarge) var(--a-border-radius-medium) var(--a-border-radius-medium) @@ -137,19 +75,19 @@ border-radius: var(--a-border-radius-xlarge); } -.navds-date .rdp-day_button:not(.rdp-day_selected, [disabled]):focus-visible, +.navds-date .rdp-button:not(.rdp-day_selected, [disabled]):focus-visible, .navds-date .navds-date__month-button:not(.rdp-day_selected, [disabled]):focus-visible { box-shadow: var(--a-shadow-focus); } @supports not selector(:focus-visible) { - .navds-date .rdp-day_button:not(.rdp-day_selected, [disabled]):focus, + .navds-date .rdp-button:not(.rdp-day_selected, [disabled]):focus, .navds-date .navds-date__month-button:not(.rdp-day_selected, [disabled]):focus { box-shadow: var(--a-shadow-focus); } } -.navds-date .rdp-day_button.rdp-day_selected:not([disabled]):focus-visible, +.navds-date .rdp-button.rdp-day_selected:not([disabled]):focus-visible, .navds-date .navds-date__month-button.rdp-day_selected:not([disabled]):focus-visible { box-shadow: inset 0 0 0 1px var(--a-surface-default), @@ -157,7 +95,7 @@ } @supports not selector(:focus-visible) { - .navds-date .rdp-day_button.rdp-day_selected:not([disabled]):focus, + .navds-date .rdp-button.rdp-day_selected:not([disabled]):focus, .navds-date .navds-date__month-button.rdp-day_selected:not([disabled]):focus { box-shadow: inset 0 0 0 1px var(--a-surface-default), @@ -165,7 +103,7 @@ } } - +/* Monthpicker */ .navds-date__month-button { all: unset; text-align: center; @@ -215,7 +153,7 @@ width: fit-content; } -.navds-date [aria-pressed="true"], +.navds-date .rdp-day_selected, .navds-monthpicker__month--selected { color: var(--ac-date-selected-text, var(--a-text-on-action)); background: var(--ac-date-selected-bg, var(--a-surface-action-selected)); @@ -229,7 +167,7 @@ color: var(--ac-date-disabled-text, var(--a-text-subtle)); } -.navds-date .rdp-day_button:where(:not(.rdp-day_selected, [disabled])):hover, +.navds-date .rdp-button:where(:not(.rdp-day_selected, [disabled])):hover, .navds-date__month-button:where(:not(.rdp-day_selected, [disabled])):hover { background: var(--ac-date-hover-bg, var(--a-surface-action-subtle-hover)); cursor: pointer; @@ -262,7 +200,7 @@ padding-right: var(--a-spacing-8); } - +/* Error-handling */ .navds-date__field--error .navds-date__field-input:not(:hover, :disabled) { border-color: var(--ac-date-input-error-border, var(--a-border-danger)); box-shadow: inset 0 0 0 1px var(--ac-date-input-error-border, var(--a-border-danger)); @@ -340,7 +278,7 @@ pointer-events: none; } - +/* Readonly */ .navds-date__field--readonly .navds-date__field-button { cursor: default; color: var(--a-gray-500); @@ -398,10 +336,9 @@ gap: var(--a-spacing-2); } - .navds-date .rdp-day_button, + .navds-date .rdp-button, .navds-date__caption-button { width: 3rem; height: 3rem; } } - */ diff --git a/@navikt/core/react/src/date/datepicker/DatePicker.tsx b/@navikt/core/react/src/date/datepicker/DatePicker.tsx index 5314685790..44abd4c2e6 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -1,7 +1,7 @@ import cl from "clsx"; -import { isWeekend } from "date-fns"; +import { isAfter, isBefore, isWeekend, startOfMonth } from "date-fns"; import React, { forwardRef, useState } from "react"; -import { DateRange, DayPicker, isMatch } from "react-day-picker"; +import { DateRange, DayPicker, dateMatchModifiers } from "react-day-picker"; import { omit } from "../../util"; import { useId } from "../../util/hooks"; import { useMergeRefs } from "../../util/hooks/useMergeRefs"; @@ -11,12 +11,6 @@ import { DatePickerInput } from "../parts/DateInput"; import { DateWrapper } from "../parts/DateWrapper"; import { getLocaleFromString, getTranslations } from "../utils"; import DatePickerStandalone from "./DatePickerStandalone"; -import Caption from "./parts/Caption"; -import DropdownCaption from "./parts/DropdownCaption"; -import { HeadRow } from "./parts/HeadRow"; -import Row from "./parts/Row"; -import TableHead from "./parts/TableHead"; -import WeekNumber from "./parts/WeekNumber"; import { ConditionalModeProps, DatePickerDefaultProps } from "./types"; export type DatePickerProps = DatePickerDefaultProps & ConditionalModeProps; @@ -85,6 +79,7 @@ export const DatePicker = forwardRef( onWeekNumberClick, fromDate, toDate, + month, ...rest }, ref, @@ -122,38 +117,55 @@ export const DatePicker = forwardRef( rest?.onSelect?.(newSelected); }; + const _locale = locale ? getLocaleFromString(locale) : langProviderLocale; + + /** + * Normalize the starting month so that its between the fromDate and toDate + */ + const normalizeMonth = (_month?: Date) => { + if (!_month) { + return undefined; + } + + let _today = _month; + + if (fromDate && isBefore(_today, fromDate)) { + _today = fromDate; + } else if (toDate && isAfter(_today, toDate)) { + _today = toDate; + } + + return startOfMonth(_today); + }; + const DatePickerComponent = ( ( - // - // ), */ - // /* Row, */ - // }} + selected={selected ?? selectedDates} + /* components={{ + MonthCaption: () => <>, + DayButton: (props) => , + Month: Months, + }} */ className={cl("navds-date", className)} classNames={{ vhidden: "navds-sr-only", }} disabled={(day) => { - return (disableWeekends && isWeekend(day)) || isMatch(day, disabled); + return ( + (disableWeekends && isWeekend(day)) || + dateMatchModifiers(day, disabled) + ); }} weekStartsOn={1} - initialFocus={false} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={false} + startMonth={fromDate} + endMonth={toDate} modifiers={{ weekend: (day) => disableWeekends && isWeekend(day), }} @@ -164,6 +176,7 @@ export const DatePicker = forwardRef( onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} fixedWeeks showOutsideDays + month={normalizeMonth(month)} {...omit(rest, ["onSelect", "role"])} /> ); diff --git a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx index 4dca78c4b4..c2dc480480 100644 --- a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx @@ -1,26 +1,13 @@ import cl from "clsx"; -import { format, isWeekend } from "date-fns"; +import { isAfter, isBefore, isWeekend, startOfMonth } from "date-fns"; import React, { forwardRef } from "react"; -import { - CalendarDay, - DateRange, - DayPicker, - Modifiers, - isMatch, -} from "react-day-picker"; -import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; -import { Button } from "../../button"; -import { Select } from "../../form/select"; +import { DateRange, DayPicker, dateMatchModifiers } from "react-day-picker"; import { omit } from "../../util"; import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks"; import { DateTranslationContextProvider } from "../context"; import { getLocaleFromString, getTranslations } from "../utils"; -import Caption from "./parts/Caption"; -import DropdownCaption from "./parts/DropdownCaption"; -import { HeadRow } from "./parts/HeadRow"; -import Row from "./parts/Row"; -import TableHead from "./parts/TableHead"; -import WeekNumber from "./parts/WeekNumber"; +import { Months } from "./parts/Months"; +import { DayButton } from "./parts/NewDayButton"; import { DatePickerDefaultProps, MultipleMode, @@ -71,6 +58,9 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< onSelect, fixedWeeks = false, onWeekNumberClick, + fromDate, + toDate, + month, ...rest }, ref, @@ -97,6 +87,25 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< const _locale = locale ? getLocaleFromString(locale) : langProviderLocale; + /** + * Normalize the starting month so that its between the fromDate and toDate + */ + const normalizeMonth = (_month?: Date) => { + if (!_month) { + return undefined; + } + + let _today = _month; + + if (fromDate && isBefore(_today, fromDate)) { + _today = fromDate; + } else if (toDate && isAfter(_today, toDate)) { + _today = toDate; + } + + return startOfMonth(_today); + }; + return (
( - - ), - YearsDropdown: ({ - options, - value, - onChange, - className: _className, - }) => ( - - ), - DropdownNav: ({ children, className: _className }) => { - return
{children}
; - }, - /* MonthCaption: ({ calendarMonth, displayIndex, children }) => { - return
test
; - }, */ - - DayButton: ({ day, modifiers, ..._rest }) => { - if (modifiers.hidden) { - return <>; - } - const dateTime = format(day.date, "cccc d", { - locale: _locale, - }); - - return ( - - ); - }, - - Nav: ({ - nextMonth, - onNextClick, - previousMonth, - onPreviousClick, - className: _className, - }) => { - return ( -
-
- ); - }, - - /* Caption: dropdownCaption ? DropdownCaption : Caption, - Head: TableHead, - HeadRow, - WeekNumber, - Row, */ + MonthCaption: () => <>, + DayButton: (props) => , + Month: Months, }} className="navds-date" disabled={(day) => { return ( - (disableWeekends && isWeekend(day)) || isMatch(day, disabled) + (disableWeekends && isWeekend(day)) || + dateMatchModifiers(day, disabled) ); }} weekStartsOn={1} - initialFocus={false} modifiers={{ weekend: (day) => disableWeekends && isWeekend(day), }} @@ -256,7 +147,12 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< } fixedWeeks={fixedWeeks} showOutsideDays - {...omit(rest, ["children", "id"])} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={false} + startMonth={fromDate} + endMonth={toDate} + month={normalizeMonth(month)} + {...omit(rest, ["children", "id", "role"])} />
diff --git a/@navikt/core/react/src/date/datepicker/new-util/getMonthOptions.ts b/@navikt/core/react/src/date/datepicker/new-util/getMonthOptions.ts new file mode 100644 index 0000000000..31c315cb70 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/new-util/getMonthOptions.ts @@ -0,0 +1,39 @@ +import { + eachMonthOfInterval, + endOfYear, + getMonth, + startOfMonth, + startOfYear, +} from "date-fns"; +import { Formatters } from "react-day-picker"; + +/** Return the months to show in the dropdown. */ +export function getMonthOptions( + displayMonth: Date, + navStart: Date | undefined, + navEnd: Date | undefined, + formatters: Pick, +): + | { + value: number; + label: string; + disabled: boolean; + }[] + | undefined { + const months = eachMonthOfInterval({ + start: startOfYear(displayMonth), + end: endOfYear(displayMonth), + }); + + const options = months.map((month) => { + const label = formatters.formatMonthDropdown(month); + const value = getMonth(month); + const disabled = + (navStart && month < startOfMonth(navStart)) || + (navEnd && month > startOfMonth(navEnd)) || + false; + return { value, label, disabled }; + }); + + return options; +} diff --git a/@navikt/core/react/src/date/datepicker/new-util/getNavMonths.ts b/@navikt/core/react/src/date/datepicker/new-util/getNavMonths.ts new file mode 100644 index 0000000000..4158189184 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/new-util/getNavMonths.ts @@ -0,0 +1,68 @@ +import { + addYears, + endOfMonth, + endOfYear, + startOfDay, + startOfMonth, + startOfYear, +} from "date-fns"; +import { DayPickerProps } from "react-day-picker"; + +/** Return the start and end months for the calendar navigation. */ +export function getNavMonths( + props: Pick< + DayPickerProps, + | "captionLayout" + | "endMonth" + | "startMonth" + | "today" + | "timeZone" + // Deprecated: + | "fromMonth" + | "fromYear" + | "toMonth" + | "toYear" + >, +): [start: Date | undefined, end: Date | undefined] { + let { startMonth, endMonth } = props; + + // Handle deprecated code + const { fromYear, toYear, fromMonth, toMonth } = props; + if (!startMonth && fromMonth) { + startMonth = fromMonth; + } + if (!startMonth && fromYear) { + startMonth = new Date(fromYear, 0, 1); + } + if (!endMonth && toMonth) { + endMonth = toMonth; + } + if (!endMonth && toYear) { + endMonth = new Date(toYear, 11, 31); + } + + const hasYearDropdown = + props.captionLayout === "dropdown" || + props.captionLayout === "dropdown-years"; + + const todayDate = props.today ?? new Date(); + + if (startMonth) { + startMonth = startOfMonth(startMonth); + } else if (fromYear) { + startMonth = new Date(fromYear, 0, 1); + } else if (!startMonth && hasYearDropdown) { + startMonth = startOfYear(addYears(todayDate, -100)); + } + if (endMonth) { + endMonth = endOfMonth(endMonth); + } else if (toYear) { + endMonth = new Date(toYear, 11, 31); + } else if (!endMonth && hasYearDropdown) { + endMonth = endOfYear(todayDate); + } + return [ + startMonth ? startOfDay(startMonth) : startMonth, + endMonth ? startOfDay(endMonth) : endMonth, + ]; +} diff --git a/@navikt/core/react/src/date/datepicker/new-util/getYearOptions.ts b/@navikt/core/react/src/date/datepicker/new-util/getYearOptions.ts new file mode 100644 index 0000000000..85776f1bc8 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/new-util/getYearOptions.ts @@ -0,0 +1,44 @@ +import { + addYears, + endOfYear, + getYear, + isBefore, + isSameYear, + startOfYear, +} from "date-fns"; +import { Formatters } from "react-day-picker"; + +/** Return the years to show in the dropdown. */ +export function getYearOptions( + navStart: Date | undefined, + navEnd: Date | undefined, + formatters: Pick, +): + | { + value: number; + label: string; + disabled: boolean; + }[] + | undefined { + if (!navStart) return undefined; + if (!navEnd) return undefined; + + const firstNavYear = startOfYear(navStart); + const lastNavYear = endOfYear(navEnd); + const years: Date[] = []; + + let year = firstNavYear; + while (isBefore(year, lastNavYear) || isSameYear(year, lastNavYear)) { + years.push(year); + year = addYears(year, 1); + } + + return years.map((_year) => { + const label = formatters.formatYearDropdown(_year); + return { + value: getYear(_year), + label, + disabled: false, + }; + }); +} diff --git a/@navikt/core/react/src/date/datepicker/parts/Months.tsx b/@navikt/core/react/src/date/datepicker/parts/Months.tsx new file mode 100644 index 0000000000..f6c59be1e4 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/Months.tsx @@ -0,0 +1,131 @@ +import { getMonth, getYear, setMonth, setYear, startOfMonth } from "date-fns"; +import React, { ChangeEvent, useCallback } from "react"; +import { CalendarMonth, useDayPicker } from "react-day-picker"; +import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; +import { Button } from "../../../button"; +import { Select } from "../../../form/select"; +import { BodyShort } from "../../../typography"; +import { useDateTranslationContext } from "../../context"; +import { getMonthOptions } from "../new-util/getMonthOptions"; +import { getNavMonths } from "../new-util/getNavMonths"; +import { getYearOptions } from "../new-util/getYearOptions"; + +const Months = ({ + children, + calendarMonth, + displayIndex, + ...rest +}: { + calendarMonth: CalendarMonth; + displayIndex: number; +} & React.HTMLAttributes) => { + const { dayPickerProps, goToMonth, formatters, previousMonth, nextMonth } = + useDayPicker(); + + const { month, captionLayout } = dayPickerProps; + + const translate = useDateTranslationContext().translate; + + console.info(month, displayIndex); + + const handleMonthChange = useCallback( + (date: Date, e: ChangeEvent) => { + const selectedMonth = Number(e.target.value); + const newMonth = setMonth(startOfMonth(date), selectedMonth); + goToMonth(newMonth); + }, + [goToMonth], + ); + + const handleYearChange = useCallback( + (date: Date, e: ChangeEvent) => { + const selectedYear = Number(e.target.value); + const newMonth = setYear(startOfMonth(date), selectedYear); + goToMonth(newMonth); + }, + [goToMonth], + ); + + const [navStart, navEnd] = getNavMonths(dayPickerProps); + + const months = getMonthOptions( + calendarMonth.date, + navStart, + navEnd, + formatters, + ); + + const dropdownYears = getYearOptions(navStart, navEnd, formatters); + + const Selects = () => ( +
+ + + +
+ ); + + const Label = () => ( + + {formatters.formatCaption(calendarMonth.date)} + + ); + + return ( +
+
+
+ {children} +
+ ); +}; + +export { Months }; diff --git a/@navikt/core/react/src/date/datepicker/parts/NewDayButton.tsx b/@navikt/core/react/src/date/datepicker/parts/NewDayButton.tsx new file mode 100644 index 0000000000..e8ea63662c --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/NewDayButton.tsx @@ -0,0 +1,35 @@ +import { Locale, format } from "date-fns"; +import React from "react"; +import { CalendarDay, Modifiers } from "react-day-picker"; + +const DayButton = ({ + day, + modifiers, + locale, + ...rest +}: { + day: CalendarDay; + modifiers: Modifiers; + locale: Locale; +} & React.ButtonHTMLAttributes) => { + if (modifiers.hidden) { + return <>; + } + const dateTime = format(day.date, "cccc d", { + locale, + }); + + return ( + + ); +}; + +export { DayButton }; From 1ab2ff9791b65b515eea697739662f6319c72011 Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 24 Jan 2025 17:38:53 +0100 Subject: [PATCH 05/49] :recycle: Extract util --- .../react/src/date/datepicker/DatePicker.tsx | 24 ++------------- .../date/datepicker/DatePickerStandalone.tsx | 24 ++------------- .../date/datepicker/new-util/clampMonth.ts | 30 +++++++++++++++++++ 3 files changed, 36 insertions(+), 42 deletions(-) create mode 100644 @navikt/core/react/src/date/datepicker/new-util/clampMonth.ts diff --git a/@navikt/core/react/src/date/datepicker/DatePicker.tsx b/@navikt/core/react/src/date/datepicker/DatePicker.tsx index 44abd4c2e6..26a374ab56 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -1,5 +1,5 @@ import cl from "clsx"; -import { isAfter, isBefore, isWeekend, startOfMonth } from "date-fns"; +import { isWeekend } from "date-fns"; import React, { forwardRef, useState } from "react"; import { DateRange, DayPicker, dateMatchModifiers } from "react-day-picker"; import { omit } from "../../util"; @@ -11,6 +11,7 @@ import { DatePickerInput } from "../parts/DateInput"; import { DateWrapper } from "../parts/DateWrapper"; import { getLocaleFromString, getTranslations } from "../utils"; import DatePickerStandalone from "./DatePickerStandalone"; +import { clampMonth } from "./new-util/clampMonth"; import { ConditionalModeProps, DatePickerDefaultProps } from "./types"; export type DatePickerProps = DatePickerDefaultProps & ConditionalModeProps; @@ -119,25 +120,6 @@ export const DatePicker = forwardRef( const _locale = locale ? getLocaleFromString(locale) : langProviderLocale; - /** - * Normalize the starting month so that its between the fromDate and toDate - */ - const normalizeMonth = (_month?: Date) => { - if (!_month) { - return undefined; - } - - let _today = _month; - - if (fromDate && isBefore(_today, fromDate)) { - _today = fromDate; - } else if (toDate && isAfter(_today, toDate)) { - _today = toDate; - } - - return startOfMonth(_today); - }; - const DatePickerComponent = ( ( onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} fixedWeeks showOutsideDays - month={normalizeMonth(month)} + month={clampMonth({ month, start: fromDate, end: toDate })} {...omit(rest, ["onSelect", "role"])} /> ); diff --git a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx index c2dc480480..d28b5e0a5f 100644 --- a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx @@ -1,11 +1,12 @@ import cl from "clsx"; -import { isAfter, isBefore, isWeekend, startOfMonth } from "date-fns"; +import { isWeekend } from "date-fns"; import React, { forwardRef } from "react"; import { DateRange, DayPicker, dateMatchModifiers } from "react-day-picker"; import { omit } from "../../util"; import { useDateLocale, useI18n } from "../../util/i18n/i18n.hooks"; import { DateTranslationContextProvider } from "../context"; import { getLocaleFromString, getTranslations } from "../utils"; +import { clampMonth } from "./new-util/clampMonth"; import { Months } from "./parts/Months"; import { DayButton } from "./parts/NewDayButton"; import { @@ -87,25 +88,6 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< const _locale = locale ? getLocaleFromString(locale) : langProviderLocale; - /** - * Normalize the starting month so that its between the fromDate and toDate - */ - const normalizeMonth = (_month?: Date) => { - if (!_month) { - return undefined; - } - - let _today = _month; - - if (fromDate && isBefore(_today, fromDate)) { - _today = fromDate; - } else if (toDate && isAfter(_today, toDate)) { - _today = toDate; - } - - return startOfMonth(_today); - }; - return (
diff --git a/@navikt/core/react/src/date/datepicker/new-util/clampMonth.ts b/@navikt/core/react/src/date/datepicker/new-util/clampMonth.ts new file mode 100644 index 0000000000..0b3b950adb --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/new-util/clampMonth.ts @@ -0,0 +1,30 @@ +import { isAfter, isBefore, startOfMonth } from "date-fns"; + +/** + * Makes sure the month is within the min and max daterange to avoid showing disabled months + */ +const clampMonth = ({ + month, + start, + end, +}: { + month?: Date; + start?: Date; + end?: Date; +}): Date | undefined => { + if (!month) { + return undefined; + } + + let monthToShow = month; + + if (start && isBefore(monthToShow, start)) { + monthToShow = start; + } else if (end && isAfter(monthToShow, end)) { + monthToShow = end; + } + + return startOfMonth(monthToShow); +}; + +export { clampMonth }; From 3a5a1cb7a2abe71158a4512372c7e27bb6579080 Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 24 Jan 2025 18:26:16 +0100 Subject: [PATCH 06/49] :recycle: Refactor standalone datepicker --- .../react/src/date/datepicker/DatePicker.tsx | 2 +- .../date/datepicker/DatePickerStandalone.tsx | 106 +++++------------- .../src/date/datepicker/ReactDayPicker.tsx | 92 +++++++++++++++ .../date/datepicker/datepicker.stories.tsx | 5 +- .../src/date/datepicker/parts/Months.tsx | 8 +- @navikt/core/react/src/date/test.txt | 83 ++++++++++++++ 6 files changed, 209 insertions(+), 87 deletions(-) create mode 100644 @navikt/core/react/src/date/datepicker/ReactDayPicker.tsx create mode 100644 @navikt/core/react/src/date/test.txt diff --git a/@navikt/core/react/src/date/datepicker/DatePicker.tsx b/@navikt/core/react/src/date/datepicker/DatePicker.tsx index 26a374ab56..8297be41f2 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -123,7 +123,7 @@ export const DatePicker = forwardRef( const DatePickerComponent = ( (defaultSelected); - - const mode = rest.mode ?? ("single" as any); - /** - * @param newSelected Date | Date[] | DateRange | undefined - */ - const handleSelect = (newSelected) => { - setSelectedDates(newSelected); - onSelect?.(newSelected); - }; - - const _locale = locale ? getLocaleFromString(locale) : langProviderLocale; + const [value, setValue] = useControllableState< + Date | Date[] | DateRange | undefined + >({ + defaultValue: defaultSelected, + value: selected, + onChange: (v) => onSelect?.(v as any), + }); return ( -
- - <>, - DayButton: (props) => , - Month: Months, - }} - className="navds-date" - disabled={(day) => { - return ( - (disableWeekends && isWeekend(day)) || - dateMatchModifiers(day, disabled) - ); - }} - weekStartsOn={1} - modifiers={{ - weekend: (day) => disableWeekends && isWeekend(day), - }} - modifiersClassNames={{ - weekend: "rdp-day__weekend", - }} - showWeekNumber={showWeekNumber} - onWeekNumberClick={ - mode === "multiple" ? onWeekNumberClick : undefined - } - fixedWeeks={fixedWeeks} - showOutsideDays - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={false} - startMonth={fromDate} - endMonth={toDate} - month={clampMonth({ month, start: fromDate, end: toDate })} - {...omit(rest, ["children", "id", "role"])} + +
+ - -
+
+ ); }, ); diff --git a/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx new file mode 100644 index 0000000000..38e0b70a82 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx @@ -0,0 +1,92 @@ +import cl from "clsx"; +import { isWeekend } from "date-fns"; +import React from "react"; +import { DayPicker, dateMatchModifiers } from "react-day-picker"; +import { omit } from "../../util"; +import { useDateLocale } from "../../util/i18n/i18n.hooks"; +import { getLocaleFromString } from "../utils"; +import { clampMonth } from "./new-util/clampMonth"; +import { Months } from "./parts/Months"; +import { DayButton } from "./parts/NewDayButton"; +import { ConditionalModeProps, DatePickerDefaultProps } from "./types"; + +type ReactDayPickerProps = DatePickerDefaultProps & + ConditionalModeProps & { + /** + * If datepicker should be fixed to 6 weeks, regardless of actual weeks in month + * @default true + */ + fixedWeeks?: boolean; + /** + * + */ + handleSelect: (newSelected: any) => void; + }; + +const ReactDayPicker = ({ + className, + dropdownCaption, + disabled = [], + disableWeekends = false, + showWeekNumber = false, + selected, + fixedWeeks = false, + onWeekNumberClick, + fromDate, + toDate, + month, + mode: _mode, + handleSelect, + locale: _locale, + ...rest +}: ReactDayPickerProps) => { + const langProviderLocale = useDateLocale(); + const locale = _locale ? getLocaleFromString(_locale) : langProviderLocale; + + const mode = _mode ?? ("single" as any); + + return ( + <>, + DayButton: (props) => , + Month: Months, + }} + className={cl("navds-date", className)} + disabled={(day) => { + return ( + (disableWeekends && isWeekend(day)) || + dateMatchModifiers(day, disabled) + ); + }} + weekStartsOn={1} + modifiers={{ + weekend: (day) => disableWeekends && isWeekend(day), + }} + modifiersClassNames={{ + weekend: "rdp-day__weekend", + }} + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={false} + showWeekNumber={showWeekNumber} + onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} + fixedWeeks={fixedWeeks} + showOutsideDays + startMonth={fromDate} + endMonth={toDate} + month={clampMonth({ month, start: fromDate, end: toDate })} + {...omit(rest, ["onSelect", "role", "id", "defaultSelected"])} + /> + ); +}; + +export { ReactDayPicker }; diff --git a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx index 2909108c3b..c1386e2c77 100644 --- a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx +++ b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx @@ -200,7 +200,10 @@ export const EN = () => ( ); export const Standalone = () => ( - + ); export const StandaloneRange = () => ( diff --git a/@navikt/core/react/src/date/datepicker/parts/Months.tsx b/@navikt/core/react/src/date/datepicker/parts/Months.tsx index f6c59be1e4..87e65fe9f6 100644 --- a/@navikt/core/react/src/date/datepicker/parts/Months.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/Months.tsx @@ -5,6 +5,7 @@ import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; import { Button } from "../../../button"; import { Select } from "../../../form/select"; import { BodyShort } from "../../../typography"; +import { omit } from "../../../util"; import { useDateTranslationContext } from "../../context"; import { getMonthOptions } from "../new-util/getMonthOptions"; import { getNavMonths } from "../new-util/getNavMonths"; @@ -13,7 +14,6 @@ import { getYearOptions } from "../new-util/getYearOptions"; const Months = ({ children, calendarMonth, - displayIndex, ...rest }: { calendarMonth: CalendarMonth; @@ -22,12 +22,10 @@ const Months = ({ const { dayPickerProps, goToMonth, formatters, previousMonth, nextMonth } = useDayPicker(); - const { month, captionLayout } = dayPickerProps; + const { captionLayout } = dayPickerProps; const translate = useDateTranslationContext().translate; - console.info(month, displayIndex); - const handleMonthChange = useCallback( (date: Date, e: ChangeEvent) => { const selectedMonth = Number(e.target.value); @@ -102,7 +100,7 @@ const Months = ({ ); return ( -
+
diff --git a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx index 7030c7f38a..5dd0a77e19 100644 --- a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx @@ -66,7 +66,7 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< >({ defaultValue: defaultSelected, value: selected, - onChange: (v) => onSelect?.(v as any), + onChange: (newValue) => onSelect?.(newValue as any), }); return ( diff --git a/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx index 38e0b70a82..a6864e220c 100644 --- a/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx +++ b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx @@ -18,7 +18,7 @@ type ReactDayPickerProps = DatePickerDefaultProps & */ fixedWeeks?: boolean; /** - * + * Update selected date */ handleSelect: (newSelected: any) => void; }; From cea218b003976ffa53e66e31478d5d6bf8129baa Mon Sep 17 00:00:00 2001 From: Ken Date: Fri, 24 Jan 2025 21:07:25 +0100 Subject: [PATCH 08/49] :recycle: Migrated weekday-click --- @navikt/core/css/date.css | 33 +++---- .../src/date/datepicker/ReactDayPicker.tsx | 85 ++++++++++++++-- .../date/datepicker/datepicker.stories.tsx | 10 +- .../src/date/datepicker/parts/Months.tsx | 18 +++- .../date/datepicker/parts/NewDayButton.tsx | 13 ++- .../src/date/datepicker/parts/WeekNumber.tsx | 99 ++++++++----------- .../core/react/src/date/datepicker/types.ts | 5 +- 7 files changed, 163 insertions(+), 100 deletions(-) diff --git a/@navikt/core/css/date.css b/@navikt/core/css/date.css index 0a6a6f7453..958fa0c1bb 100644 --- a/@navikt/core/css/date.css +++ b/@navikt/core/css/date.css @@ -22,30 +22,13 @@ font-size: var(--a-font-size-small); } -.navds-date .rdp-weeknumber { - font-size: var(--a-font-size-small); - display: flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; - border-radius: var(--a-border-radius-medium); - margin: var(--a-spacing-2); - color: var(--a-text-subtle); -} - -.navds-date .rdp-weeknumber.rdp-button { - width: 2rem; - height: 2rem; - box-shadow: 0 0 0 1px var(--a-border-default); +.navds-date__weeknumber-number { + font-size: 0.875rem; color: var(--a-text-subtle); - font-size: var(--a-font-size-small); } -.navds-date .rdp-weeknumber.rdp-button:active { - background-color: var(--a-surface-action-active); - color: var(--a-text-on-action); - box-shadow: none; +.navds-date__weeknumber:active .navds-date__weeknumber-number { + color: var(--a-text-on-neutral); } .navds-date__caption__month .navds-select__container select { @@ -303,6 +286,14 @@ margin: 0; } +span.rdp-weeknumber { + display: grid; + place-content: center; + width: 2rem; + height: 2rem; + margin: var(--a-spacing-2); +} + .navds-date__modal.navds-date { padding: 0; } diff --git a/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx index a6864e220c..927b123bab 100644 --- a/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx +++ b/@navikt/core/react/src/date/datepicker/ReactDayPicker.tsx @@ -1,15 +1,65 @@ import cl from "clsx"; -import { isWeekend } from "date-fns"; +import { isAfter, isBefore, isWeekend } from "date-fns"; import React from "react"; -import { DayPicker, dateMatchModifiers } from "react-day-picker"; +import { ClassNames, DayPicker, dateMatchModifiers } from "react-day-picker"; +import { Show } from "../../layout/responsive"; import { omit } from "../../util"; import { useDateLocale } from "../../util/i18n/i18n.hooks"; import { getLocaleFromString } from "../utils"; import { clampMonth } from "./new-util/clampMonth"; import { Months } from "./parts/Months"; import { DayButton } from "./parts/NewDayButton"; +import WeekNumber from "./parts/WeekNumber"; import { ConditionalModeProps, DatePickerDefaultProps } from "./types"; +/* +button button_previous, button_next +button_reset button_previous, button_next +caption month_caption +caption_between Removed +caption_dropdowns dropdowns +caption_end Removed +caption_start Removed +day_disabled disabled +day_hidden hidden +day_outside outside +head Removed +head_cell weekday +head_row weekdays +multiple_months Removed. Use data-multiple-months data attribute. +nav_button button_previous, button_next +nav_button_next button_next +nav_button_previous button_previous +nav_icon chevron, button_next, button_previous +row week +tbody weeks +table month_grid +tfoot footer +vhidden Removed + +cell day – ⚠️ The previous day element is now day_button. + +weeknumber week_number +with_weeknumber Removed. Use data-week-numbers data attribute. */ + +/* rdp-button_reset rdp-button rdp-day rdp-day_disabled */ +const LegacyClassNames: Partial = { + button_next: "button", + day: "rdp-cell", + day_button: "rdp-day rdp-button", + /* We set this directly on DayButton */ + disabled: "", + hidden: "rdp-day_hidden", + outside: "rdp-day_outside", + selected: "rdp-day_selected", + weekday: "rdp-head_cell", + weekdays: "rdp-head_row", + week: "rdp-row", + weeks: "rdp-tbody", + month_grid: "rdp-table", + week_number: "rdp-weeknumber", +}; + type ReactDayPickerProps = DatePickerDefaultProps & ConditionalModeProps & { /** @@ -53,19 +103,39 @@ const ReactDayPicker = ({ mode={mode as any} onSelect={handleSelect} selected={selected} - classNames={{ - vhidden: "navds-sr-only", - }} + classNames={LegacyClassNames} components={{ MonthCaption: () => <>, DayButton: (props) => , - Month: Months, + Month: (props) => , + Day: (props) => ( + + ), + WeekNumber: (props) => ( + + ), + WeekNumberHeader: (props) => ( + + + + ), }} className={cl("navds-date", className)} disabled={(day) => { + const isOutside = + (toDate && isAfter(day, toDate)) || + (fromDate && isBefore(day, fromDate)) || + false; + return ( (disableWeekends && isWeekend(day)) || - dateMatchModifiers(day, disabled) + dateMatchModifiers(day, disabled) || + isOutside ); }} weekStartsOn={1} @@ -78,7 +148,6 @@ const ReactDayPicker = ({ // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={false} showWeekNumber={showWeekNumber} - onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} fixedWeeks={fixedWeeks} showOutsideDays startMonth={fromDate} diff --git a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx index c1386e2c77..d0257fca38 100644 --- a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx +++ b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, within } from "@storybook/test"; -import { getWeek, isSameDay } from "date-fns"; +import { isSameDay } from "date-fns"; import React, { useId, useState } from "react"; import { Button } from "../../button"; import { HGrid } from "../../layout/grid"; @@ -405,8 +405,8 @@ export const StandaloneOptions = () => { export const WeekDayClick = () => { const [days, setDays] = useState([]); - const handleWeekClick = (week: number) => { - const hasDayInWeek = !!days.find((day) => getWeek(day) === week); + const handleWeekClick = (dates: Date[]) => { + const hasDayInWeek = !!days.find((x) => dates.find((y) => isSameDay(x, y))); const cleanup = days.filter((y) => !dates.find((z) => isSameDay(y, z))); if (hasDayInWeek) { @@ -421,7 +421,7 @@ export const WeekDayClick = () => { handleWeekClick(week)} + onWeekNumberClick={(_, dates) => handleWeekClick(dates)} onSelect={(dates) => dates && setDays(dates)} selected={days} today={new Date("Nov 23 2022")} @@ -429,7 +429,7 @@ export const WeekDayClick = () => { handleWeekClick(week)} + onWeekNumberClick={(_, dates) => handleWeekClick(dates)} onSelect={(dates) => dates && setDays(dates)} selected={days} today={new Date("Nov 23 2022")} diff --git a/@navikt/core/react/src/date/datepicker/parts/Months.tsx b/@navikt/core/react/src/date/datepicker/parts/Months.tsx index 87e65fe9f6..92c7486c02 100644 --- a/@navikt/core/react/src/date/datepicker/parts/Months.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/Months.tsx @@ -1,4 +1,11 @@ -import { getMonth, getYear, setMonth, setYear, startOfMonth } from "date-fns"; +import { + Locale, + getMonth, + getYear, + setMonth, + setYear, + startOfMonth, +} from "date-fns"; import React, { ChangeEvent, useCallback } from "react"; import { CalendarMonth, useDayPicker } from "react-day-picker"; import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; @@ -14,10 +21,12 @@ import { getYearOptions } from "../new-util/getYearOptions"; const Months = ({ children, calendarMonth, + locale, ...rest }: { calendarMonth: CalendarMonth; displayIndex: number; + locale: Locale; } & React.HTMLAttributes) => { const { dayPickerProps, goToMonth, formatters, previousMonth, nextMonth } = useDayPicker(); @@ -95,13 +104,18 @@ const Months = ({ role="status" className="navds-date__caption-label" > - {formatters.formatCaption(calendarMonth.date)} + {formatters.formatCaption(calendarMonth.date, { locale })} ); return (
+ {captionLayout?.startsWith("dropdown") && ( + + {formatters.formatCaption(calendarMonth.date)} + + )} ); }; diff --git a/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx index 1afbdaf6fa..57599f2fac 100644 --- a/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx @@ -1,82 +1,63 @@ -/* https://github.com/gpbl/react-day-picker/blob/7f78cd5/src/components/WeekNumber/WeekNumber.tsx#L21 */ +import cl from "clsx"; import React from "react"; -import { - CalendarWeek, - Button as RDPButton, - useDayPicker, -} from "react-day-picker"; +import { CalendarWeek } from "react-day-picker"; import { Button } from "../../../button"; -import { useThemeInternal } from "../../../theme/Theme"; +import { Show } from "../../../layout/responsive"; import { Detail } from "../../../typography"; import { useDateTranslationContext } from "../../context"; import { MultipleMode } from "../types"; -export interface WeekNumberProps { - /** The number of the week. */ - number: number; - /** The dates in the week. */ - dates: Date[]; -} - -/** - * https://github.com/gpbl/react-day-picker/tree/main/src/components/WeekNumber - */ function WeekNumber({ week: { weekNumber, days }, onWeekNumberClick, + className, + style, }: { week: CalendarWeek; onWeekNumberClick: MultipleMode["onWeekNumberClick"]; -}): JSX.Element { - const { styles, classNames } = useDayPicker(); - const themeContext = useThemeInternal(false); +} & React.ThHTMLAttributes): JSX.Element { const translate = useDateTranslationContext().translate; if (!onWeekNumberClick) { return ( - - {weekNumber} - - ); - } - - if (themeContext) { - return ( -
+ {children}
); diff --git a/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx index 57599f2fac..c205f4b6e3 100644 --- a/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx @@ -1,8 +1,8 @@ import cl from "clsx"; -import React from "react"; -import { CalendarWeek } from "react-day-picker"; +import React, { useMemo } from "react"; +import { CalendarWeek, useDayPicker } from "react-day-picker"; import { Button } from "../../../button"; -import { Show } from "../../../layout/responsive"; +import { Hide, Show } from "../../../layout/responsive"; import { Detail } from "../../../typography"; import { useDateTranslationContext } from "../../context"; import { MultipleMode } from "../types"; @@ -12,15 +12,33 @@ function WeekNumber({ onWeekNumberClick, className, style, + showOnDesktop, }: { week: CalendarWeek; onWeekNumberClick: MultipleMode["onWeekNumberClick"]; + showOnDesktop: boolean; } & React.ThHTMLAttributes): JSX.Element { const translate = useDateTranslationContext().translate; - if (!onWeekNumberClick) { + const { getModifiers } = useDayPicker(); + + const hideWeek = useMemo(() => { + if ( + days.filter((day) => { + const mods = getModifiers(day); + return !(mods.hidden || mods.outside || mods.disabled); + }).length === 0 + ) { + return true; + } + return false; + }, [days, getModifiers]); + + const DisplayMode = showOnDesktop ? Show : Hide; + + if (!onWeekNumberClick || hideWeek) { return ( - + - + ); } return ( - +