From 51eee7f011aa016973bb952313cf65a1c55aa382 Mon Sep 17 00:00:00 2001 From: Ken <26967723+KenAJoh@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:38:57 +0100 Subject: [PATCH] =?UTF-8?q?[Datepicker/Modal]=20Datepicker=20bruker=20n?= =?UTF-8?q?=C3=A5=20Modal=20p=C3=A5=20mobil=20og=20for=20nested=20Modal=20?= =?UTF-8?q?(#2419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :art: Initial Modal-update * :bug: close(open handling for Modal * :fire: Fjernet openOnFocus * :fire: Fjernet outsideclick og useEscape * :bug: monthpicker onClose forwarding * :art: Flyttet DateInput til egen parts-mappe * :label: DateInput imports * :truck: Flyttet DateWrapper ut til egen komponent * :art: Monthpicker bruker dateWrapper * :lipstick: Oppdatert styling for datepicker * :memo: Oppdatert eksempler * :memo: changeset * :bug: Fikset imports i date-hooks * :recycle: Bedre context-bruk i Modal.tsx * :memo: Bedre tekster for deprecation * :bug: Støtter nå jsdom * :bug: Fikset polyfill av datepicker * :art: closeOnBackdropClick i datepicker * :label: :bug: Fikset outsideClick boundary + fjernet bubbleEscape-prop * :fire: Fjernet console.log * :fire: Fjernet bubbleEscape-prop * Update @navikt/core/react/src/date/hooks/useMonthPicker.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * Update @navikt/core/react/src/date/datepicker/datepicker.stories.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * :art: Bedre className for nested modal * :bug: Bedre check om popover er i datepicker * :fire: Fjernet unødvendig if rundt window-check * :art: Bedre fallback for useMedia-hook * :art: bedre navn i useMedia * :memo: Datepicker-stories for size har preset-values * Update @navikt/core/react/src/date/hooks/useDatepicker.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * :art: DateContext er nå default null hvis ikke satt * Update @navikt/core/react/src/date/parts/DateWrapper.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * Update @navikt/core/react/src/date/parts/DateWrapper.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * Update @navikt/core/react/src/date/parts/DateWrapper.tsx Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * Update @navikt/core/react/src/date/utils/labels.ts Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> * :bug: Fikset small-size padding * :fire: Fjernet error-demo * fokusgreier * :art: bedre onClose-handling av modal * :bug: Håndterer focus ved cancel og select nå * :bug: Feil i typer for dateinput * :bug: setAnchorRef var optional * :bug: null-test * :art: Fjernet jsdom sjekk tester for useMedia --------- Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com> Co-authored-by: Halvor Haugan --- .changeset/yellow-buckets-grab-copy-2.md | 6 + .changeset/yellow-buckets-grab-copy-3.md | 6 + .changeset/yellow-buckets-grab-copy.md | 6 + .changeset/yellow-buckets-grab.md | 6 + @navikt/core/css/date.css | 44 ++++--- @navikt/core/css/modal.css | 8 ++ .../src/date/context/useDateInputContext.tsx | 10 +- .../react/src/date/datepicker/DatePicker.tsx | 123 +++++++++--------- .../date/datepicker/datepicker.stories.tsx | 83 ++++++------ .../core/react/src/date/datepicker/types.ts | 5 - .../react/src/date/hooks/useDatepicker.tsx | 45 +++---- .../core/react/src/date/hooks/useEscape.tsx | 30 ----- .../react/src/date/hooks/useMonthPicker.tsx | 42 +++--- .../src/date/hooks/useOutsideClickHandler.tsx | 34 ----- .../src/date/hooks/useRangeDatepicker.tsx | 57 ++++---- @navikt/core/react/src/date/index.ts | 2 +- .../src/date/monthpicker/MonthPicker.tsx | 82 ++++++------ .../core/react/src/date/monthpicker/types.ts | 5 - .../react/src/date/{ => parts}/DateInput.tsx | 35 +++-- .../core/react/src/date/parts/DateWrapper.tsx | 80 ++++++++++++ @navikt/core/react/src/date/utils/labels.ts | 83 ++++++++++++ .../src/guide-panel/guidepanel.stories.tsx | 4 +- @navikt/core/react/src/modal/Modal.tsx | 5 +- @navikt/core/react/src/popover/Popover.tsx | 16 +-- .../src/util/__tests__/useMedia.test.tsx | 19 +++ @navikt/core/react/src/util/useMedia.ts | 38 ++++++ .../pages/eksempler/datepicker/modal.tsx | 37 ------ 27 files changed, 516 insertions(+), 395 deletions(-) create mode 100644 .changeset/yellow-buckets-grab-copy-2.md create mode 100644 .changeset/yellow-buckets-grab-copy-3.md create mode 100644 .changeset/yellow-buckets-grab-copy.md create mode 100644 .changeset/yellow-buckets-grab.md delete mode 100644 @navikt/core/react/src/date/hooks/useEscape.tsx delete mode 100644 @navikt/core/react/src/date/hooks/useOutsideClickHandler.tsx rename @navikt/core/react/src/date/{ => parts}/DateInput.tsx (80%) create mode 100644 @navikt/core/react/src/date/parts/DateWrapper.tsx create mode 100644 @navikt/core/react/src/util/__tests__/useMedia.test.tsx create mode 100644 @navikt/core/react/src/util/useMedia.ts delete mode 100644 aksel.nav.no/website/pages/eksempler/datepicker/modal.tsx diff --git a/.changeset/yellow-buckets-grab-copy-2.md b/.changeset/yellow-buckets-grab-copy-2.md new file mode 100644 index 0000000000..9f4de46bad --- /dev/null +++ b/.changeset/yellow-buckets-grab-copy-2.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. diff --git a/.changeset/yellow-buckets-grab-copy-3.md b/.changeset/yellow-buckets-grab-copy-3.md new file mode 100644 index 0000000000..178bf9607f --- /dev/null +++ b/.changeset/yellow-buckets-grab-copy-3.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. diff --git a/.changeset/yellow-buckets-grab-copy.md b/.changeset/yellow-buckets-grab-copy.md new file mode 100644 index 0000000000..272b3067a8 --- /dev/null +++ b/.changeset/yellow-buckets-grab-copy.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. diff --git a/.changeset/yellow-buckets-grab.md b/.changeset/yellow-buckets-grab.md new file mode 100644 index 0000000000..fb01f3a9e9 --- /dev/null +++ b/.changeset/yellow-buckets-grab.md @@ -0,0 +1,6 @@ +--- +"@navikt/ds-react": minor +"@navikt/ds-css": minor +--- + +Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. diff --git a/@navikt/core/css/date.css b/@navikt/core/css/date.css index 6280171d0d..19e43adb7b 100644 --- a/@navikt/core/css/date.css +++ b/@navikt/core/css/date.css @@ -149,18 +149,6 @@ width: fit-content; } -/* Focus layering */ -.navds-date__field-input:focus-visible, -.navds-date__field-button { - z-index: 1; -} - -@supports not selector(:focus-visible) { - .navds-date__field-input:focus { - z-index: 1; - } -} - .navds-date .rdp-day_selected, .navds-monthpicker__month--selected { color: var(--ac-date-selected-text, var(--a-text-on-action)); @@ -201,11 +189,11 @@ } .navds-date__field-input { - padding-right: var(--a-spacing-14); + padding-right: var(--a-spacing-12); } .navds-form-field--small .navds-date__field-input { - padding-right: var(--a-spacing-10); + padding-right: var(--a-spacing-8); } /* Error-handling */ @@ -282,10 +270,6 @@ pointer-events: none; } -.navds-date__popover:where(.navds-popover) { - border: none; -} - /* Readonly */ .navds-date__field--readonly .navds-date__field-button { cursor: default; @@ -310,11 +294,35 @@ margin: 0; } +.navds-date__modal.navds-date { + padding: 0; +} + +.navds-date__modal-body { + display: flex; + flex-direction: column; + align-items: flex-end; + padding: var(--a-spacing-4); + gap: var(--a-spacing-2); +} + +.navds-date__modal-body > .navds-date { + padding: 0; +} + +.navds-date__popover:where(.navds-popover) { + border: none; +} + @media (min-width: 480px) { .navds-date { padding: var(--a-spacing-5) var(--a-spacing-4); } + .navds-date__modal-body { + padding: var(--a-spacing-6); + } + .navds-date__caption { gap: var(--a-spacing-2); } diff --git a/@navikt/core/css/modal.css b/@navikt/core/css/modal.css index 7aee644ec8..a2feb90149 100644 --- a/@navikt/core/css/modal.css +++ b/@navikt/core/css/modal.css @@ -130,6 +130,14 @@ margin-left: auto; } +/* When Datepicker is used nested inside a Modal */ +.navds-modal--polyfilled .navds-modal--polyfilled.navds-date__nested-modal { + min-width: fit-content; + max-width: 100vw; + max-height: 100vh; + animation: none; +} + @keyframes akselModalFadeIn { from { opacity: 0; diff --git a/@navikt/core/react/src/date/context/useDateInputContext.tsx b/@navikt/core/react/src/date/context/useDateInputContext.tsx index cb940ab0c9..3b034e76d9 100644 --- a/@navikt/core/react/src/date/context/useDateInputContext.tsx +++ b/@navikt/core/react/src/date/context/useDateInputContext.tsx @@ -13,13 +13,13 @@ interface DateContextContextProps { * Aria-connected ID */ ariaId?: string; + /** + * Flag for enabled-check + */ + defined: boolean; } -export const DateContext = createContext({ - open: false, - onOpen: () => null, - ariaId: undefined, -}); +export const DateContext = createContext(null); export const useDateInputContext = () => { const context = useContext(DateContext); diff --git a/@navikt/core/react/src/date/datepicker/DatePicker.tsx b/@navikt/core/react/src/date/datepicker/DatePicker.tsx index 04673fd7b4..5b048ba326 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -1,11 +1,11 @@ import cl from "clsx"; import isWeekend from "date-fns/isWeekend"; -import React, { forwardRef, useRef, useState } from "react"; +import React, { forwardRef, useMemo, useRef, useState } from "react"; import { DateRange, DayPicker, isMatch } from "react-day-picker"; -import { Popover } from "../../popover"; -import { omit, useId } from "../../util"; -import { DatePickerInput } from "../DateInput"; +import { mergeRefs, omit, useId } from "../../util"; import { DateContext } from "../context"; +import { DatePickerInput } from "../parts/DateInput"; +import { DateWrapper } from "../parts/DateWrapper"; import { getLocaleFromString, labels } from "../utils"; import DatePickerStandalone from "./DatePickerStandalone"; import Caption from "./parts/Caption"; @@ -79,7 +79,6 @@ export const DatePicker = forwardRef( onClose, onOpenToggle, strategy, - bubbleEscape = false, onWeekNumberClick, ...rest }, @@ -89,6 +88,7 @@ export const DatePicker = forwardRef( const [open, setOpen] = useState(_open ?? false); const wrapperRef = useRef(null); + const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]); const [selectedDates, setSelectedDates] = React.useState< Date | Date[] | DateRange | undefined @@ -110,6 +110,44 @@ export const DatePicker = forwardRef( rest?.onSelect?.(newSelected); }; + const DatePickerComponent = ( + { + return (disableWeekends && isWeekend(day)) || isMatch(day, disabled); + }} + weekStartsOn={1} + initialFocus={false} + labels={labels as any} + modifiers={{ + weekend: (day) => disableWeekends && isWeekend(day), + }} + modifiersClassNames={{ + weekend: "rdp-day__weekend", + }} + showWeekNumber={showWeekNumber} + onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined} + fixedWeeks + showOutsideDays + {...omit(rest, ["onSelect"])} + /> + ); + return ( ( onOpenToggle?.(); }, ariaId, + defined: true, }} >
{children} - {(_open ?? open) && ( - { - onClose?.() ?? setOpen(false); - }} - placement="bottom-start" - id={ariaId} - role="dialog" - ref={ref} - strategy={strategy} - className="navds-date__popover" - bubbleEscape={bubbleEscape} - flip={false} - > - { - return ( - (disableWeekends && isWeekend(day)) || - isMatch(day, disabled) - ); - }} - weekStartsOn={1} - initialFocus={false} - labels={labels as any} - modifiers={{ - weekend: (day) => disableWeekends && isWeekend(day), - }} - modifiersClassNames={{ - weekend: "rdp-day__weekend", - }} - showWeekNumber={showWeekNumber} - onWeekNumberClick={ - mode === "multiple" ? onWeekNumberClick : undefined - } - fixedWeeks - showOutsideDays - {...omit(rest, ["onSelect"])} - /> - - )} + onClose?.() ?? setOpen(false)} + locale={locale} + variant={mode} + popoverProps={{ + id: ariaId, + strategy, + }} + > + {DatePickerComponent} +
); diff --git a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx index 5f6dceb351..582d3663e2 100644 --- a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx +++ b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react"; import isSameDay from "date-fns/isSameDay"; import React, { useId, useState } from "react"; import { useDatepicker, useRangeDatepicker } from ".."; -import { BodyLong, Button, HGrid, HStack, Modal, VStack } from "../.."; +import { BodyLong, Button, HGrid, Modal, VStack } from "../.."; import DatePicker, { DatePickerProps } from "./DatePicker"; const disabledDays = [ @@ -18,7 +18,6 @@ export default { type DefaultStoryProps = DatePickerProps & { size: "medium" | "small"; - openOnFocus: boolean; inputfield: boolean; standalone: boolean; }; @@ -30,13 +29,11 @@ export const Default: StoryObj = { const rangeCtx = useRangeDatepicker({ fromDate: new Date("Aug 23 2020"), toDate: new Date("Aug 23 2023"), - openOnFocus: props.openOnFocus, }); const singleCtx = useDatepicker({ fromDate: new Date("Aug 23 2020"), toDate: new Date("Aug 23 2023"), - openOnFocus: props.openOnFocus, }); const newProps = { @@ -111,7 +108,6 @@ export const Default: StoryObj = { disableWeekends: false, showWeekNumber: false, mode: "single", - openOnFocus: true, inputfield: true, standalone: false, }, @@ -186,21 +182,6 @@ export const UseRangedDatepicker = () => { ); }; -export const OpenOnFocus = () => { - const { datepickerProps, inputProps } = useDatepicker({ - onDateChange: console.log, - openOnFocus: false, - }); - - return ( -
- - - -
- ); -}; - export const NB = () => ( ); @@ -333,15 +314,32 @@ export const Size = () => { fromDate: new Date("Aug 23 2019"), toDate: new Date("Feb 23 2024"), onDateChange: console.log, + defaultSelected: new Date("Feb 23 2023"), + }); + const { datepickerProps: d2, inputProps: i2 } = useDatepicker({ + fromDate: new Date("Aug 23 2019"), + toDate: new Date("Feb 23 2024"), + onDateChange: console.log, + defaultSelected: new Date("Feb 23 2023"), }); return (
- + - - + +
); @@ -441,33 +439,26 @@ export const ModalDemo = () => { }); return ( - + - - Lorem ipsum dolor sit, amet consectetur adipisicing elit. Maiores nisi - incidunt ipsum cupiditate nostrum nesciunt, corrupti nihil at atque - animi ab aut. Quam iusto harum eligendi magnam nulla repudiandae - molestias. + + Lorem ipsum dolor sit, amet consectetur adipisicing elit. - - - - - - - - - - - + + + + + + + + ); }; +ModalDemo.parameters = { chromatic: { pauseAnimationAtEnd: true } }; diff --git a/@navikt/core/react/src/date/datepicker/types.ts b/@navikt/core/react/src/date/datepicker/types.ts index d165bfc882..eac553f798 100644 --- a/@navikt/core/react/src/date/datepicker/types.ts +++ b/@navikt/core/react/src/date/datepicker/types.ts @@ -101,9 +101,4 @@ export interface DatePickerDefaultProps * @default See Popover */ strategy?: "absolute" | "fixed"; - /** - * Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape - * @default false - */ - bubbleEscape?: boolean; } diff --git a/@navikt/core/react/src/date/hooks/useDatepicker.tsx b/@navikt/core/react/src/date/hooks/useDatepicker.tsx index b2041499bc..9b4b938fac 100644 --- a/@navikt/core/react/src/date/hooks/useDatepicker.tsx +++ b/@navikt/core/react/src/date/hooks/useDatepicker.tsx @@ -1,17 +1,15 @@ import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; import isWeekend from "date-fns/isWeekend"; -import React, { useCallback, useRef, useState } from "react"; +import React, { useCallback, useState } from "react"; import { DayClickEventHandler, isMatch } from "react-day-picker"; -import { DateInputProps } from "../DateInput"; import { DatePickerProps } from "../datepicker/DatePicker"; +import { DateInputProps } from "../parts/DateInput"; import { formatDateForInput, getLocaleFromString, isValidDate, parseDate, } from "../utils"; -import { useEscape } from "./useEscape"; -import { useOutsideClickHandler } from "./useOutsideClickHandler"; export interface UseDatepickerOptions extends Pick< @@ -59,10 +57,10 @@ export interface UseDatepickerOptions */ allowTwoDigitYear?: boolean; /** - * Opens datepicker on input-focus - * @default true + * Will be removed in a future major-version + * @deprecated */ - openOnFocus?: boolean; + openOnFocus?: false; } interface UseDatepickerValue { @@ -76,7 +74,14 @@ interface UseDatepickerValue { inputProps: Pick< DateInputProps, "onChange" | "onFocus" | "onBlur" | "value" - > & { ref: React.RefObject }; + > & { + /** + * @private + */ + setAnchorRef: React.Dispatch< + React.SetStateAction + >; + }; /** * Resets all states (callback) */ @@ -143,14 +148,11 @@ export const useDatepicker = ( onValidate, defaultMonth, allowTwoDigitYear = true, - openOnFocus = true, } = opt; + const [anchorRef, setAnchorRef] = useState(null); const locale = getLocaleFromString(_locale); - const inputRef = useRef(null); - const [daypickerRef, setDaypickerRef] = useState(); - const [defaultSelected, setDefaultSelected] = useState(_defaultSelected); // Initialize states @@ -172,14 +174,6 @@ export const useDatepicker = ( [defaultMonth, defaultSelected, selectedDay, today] ); - useOutsideClickHandler(open, handleOpen, [ - daypickerRef, - inputRef.current, - inputRef.current?.nextSibling, - ]); - - useEscape(open, handleOpen, inputRef); - const updateDate = (date?: Date) => { onDateChange?.(date); setSelectedDay(date); @@ -207,7 +201,6 @@ export const useDatepicker = ( if (e.target.readOnly) { return; } - !open && openOnFocus && handleOpen(true); const day = parseDate( e.target.value, today, @@ -243,7 +236,7 @@ export const useDatepicker = ( const handleDayClick: DayClickEventHandler = (day, { selected }) => { if (day && !selected) { handleOpen(false); - inputRef.current && inputRef.current.focus(); + anchorRef?.focus(); } if (!required && selected) { @@ -319,11 +312,13 @@ export const useDatepicker = ( toDate, today, open, + onClose: () => { + handleOpen(false); + anchorRef?.focus(); + }, onOpenToggle: () => handleOpen(!open), disabled, disableWeekends, - bubbleEscape: true, - ref: setDaypickerRef, }; const inputProps = { @@ -331,7 +326,7 @@ export const useDatepicker = ( onFocus: handleFocus, onBlur: handleBlur, value: inputValue, - ref: inputRef, + setAnchorRef, }; return { datepickerProps, inputProps, reset, selectedDay, setSelected }; diff --git a/@navikt/core/react/src/date/hooks/useEscape.tsx b/@navikt/core/react/src/date/hooks/useEscape.tsx deleted file mode 100644 index f8dd0c20a3..0000000000 --- a/@navikt/core/react/src/date/hooks/useEscape.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useCallback, useEffect, RefObject } from "react"; - -export const useEscape = ( - open: boolean, - setOpen: (openState: boolean) => void, - focusRef: RefObject -) => { - const handleClose = useCallback(() => { - setOpen(false); - focusRef?.current && focusRef.current.focus(); - }, [focusRef, setOpen]); - - const escape = useCallback( - (event: KeyboardEvent) => { - if (open && event.key === "Escape") { - event.preventDefault(); // This prevents modal from closing when using datepicker inside modal - handleClose(); - } - }, - [handleClose, open] - ); - - useEffect(() => { - window.addEventListener("keydown", escape, false); - - return () => { - window.removeEventListener("keydown", escape, false); - }; - }, [escape]); -}; diff --git a/@navikt/core/react/src/date/hooks/useMonthPicker.tsx b/@navikt/core/react/src/date/hooks/useMonthPicker.tsx index 39b74badc5..fc6e07e18f 100644 --- a/@navikt/core/react/src/date/hooks/useMonthPicker.tsx +++ b/@navikt/core/react/src/date/hooks/useMonthPicker.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useMemo, useRef, useState } from "react"; -import { DateInputProps } from "../DateInput"; +import React, { useCallback, useMemo, useState } from "react"; import { MonthPickerProps } from "../monthpicker/types"; +import { DateInputProps } from "../parts/DateInput"; import { formatDateForInput, getLocaleFromString, @@ -8,8 +8,6 @@ import { isValidDate, parseDate, } from "../utils"; -import { useEscape } from "./useEscape"; -import { useOutsideClickHandler } from "./useOutsideClickHandler"; export interface UseMonthPickerOptions extends Pick< @@ -45,8 +43,8 @@ export interface UseMonthPickerOptions */ allowTwoDigitYear?: boolean; /** - * Opens datepicker on input-focus - * @default true + * Will be removed in a future major-version + * @deprecated */ openOnFocus?: boolean; } @@ -60,7 +58,12 @@ interface UseMonthPickerValue { * Use: */ inputProps: Pick & { - ref: React.RefObject; + /** + * @private + */ + setAnchorRef: React.Dispatch< + React.SetStateAction + >; }; /** * Currently selected Date @@ -138,17 +141,14 @@ export const useMonthpicker = ( onValidate, defaultYear, allowTwoDigitYear = true, - openOnFocus = true, } = opt; + const [anchorRef, setAnchorRef] = useState(null); const [defaultSelected, setDefaultSelected] = useState(_defaultSelected); const today = useMemo(() => new Date(), []); const locale = getLocaleFromString(_locale); - const inputRef = useRef(null); - const [monthpickerRef, setMonthpickerRef] = useState(); - // Initialize states const [year, setYear] = useState(defaultSelected ?? defaultYear ?? today); const [selectedMonth, setSelectedMonth] = useState(defaultSelected); @@ -169,14 +169,6 @@ export const useMonthpicker = ( [defaultSelected, defaultYear, selectedMonth, today] ); - useOutsideClickHandler(open, handleOpen, [ - monthpickerRef, - inputRef.current, - inputRef.current?.nextSibling, - ]); - - useEscape(open, handleOpen, inputRef); - const updateMonth = (date?: Date) => { onMonthChange?.(date); setSelectedMonth(date); @@ -204,7 +196,7 @@ export const useMonthpicker = ( if (e.target.readOnly) { return; } - !open && openOnFocus && handleOpen(true); + const day = parseDate( e.target.value, today, @@ -239,8 +231,8 @@ export const useMonthpicker = ( const handleMonthClick = (month?: Date) => { if (month) { handleOpen(false); - inputRef.current && inputRef.current.focus(); setYear(month); + anchorRef?.focus(); } if (!required && !month) { @@ -314,9 +306,11 @@ export const useMonthpicker = ( toDate, open, onOpenToggle: () => handleOpen(!open), + onClose: () => { + handleOpen(false); + anchorRef?.focus(); + }, disabled, - bubbleEscape: true, - ref: setMonthpickerRef, }; const inputProps = { @@ -324,7 +318,7 @@ export const useMonthpicker = ( onFocus: handleFocus, onBlur: handleBlur, value: inputValue, - ref: inputRef, + setAnchorRef, }; return { monthpickerProps, inputProps, reset, selectedMonth, setSelected }; diff --git a/@navikt/core/react/src/date/hooks/useOutsideClickHandler.tsx b/@navikt/core/react/src/date/hooks/useOutsideClickHandler.tsx deleted file mode 100644 index c5dd8cf3fd..0000000000 --- a/@navikt/core/react/src/date/hooks/useOutsideClickHandler.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useCallback, useEffect } from "react"; - -export const useOutsideClickHandler = ( - open: boolean, - setOpen: (openState: boolean) => void, - refs: Array -) => { - const handleFocusIn = useCallback( - (e) => { - const composed = e.composedPath?.()?.[0]; - if (!e?.target || !e?.target?.nodeType || !composed) { - return; - } - if ( - !refs.some( - (element) => - element?.contains(e.target) || element?.contains(composed) - ) - ) { - open && setOpen(false); - } - }, - [open, refs, setOpen] - ); - - useEffect(() => { - window.addEventListener("focusin", handleFocusIn); - window.addEventListener("pointerdown", handleFocusIn); - return () => { - window?.removeEventListener?.("focusin", handleFocusIn); - window?.removeEventListener?.("pointerdown", handleFocusIn); - }; - }, [handleFocusIn]); -}; diff --git a/@navikt/core/react/src/date/hooks/useRangeDatepicker.tsx b/@navikt/core/react/src/date/hooks/useRangeDatepicker.tsx index 394dcd2e43..b8ed600d73 100644 --- a/@navikt/core/react/src/date/hooks/useRangeDatepicker.tsx +++ b/@navikt/core/react/src/date/hooks/useRangeDatepicker.tsx @@ -1,10 +1,10 @@ import differenceInCalendarDays from "date-fns/differenceInCalendarDays"; import checkIsBefore from "date-fns/isBefore"; import isWeekend from "date-fns/isWeekend"; -import React, { useRef, useState } from "react"; +import React, { useState } from "react"; import { DateRange, isMatch } from "react-day-picker"; -import { DateInputProps } from "../DateInput"; import { DatePickerProps } from "../datepicker/DatePicker"; +import { DateInputProps } from "../parts/DateInput"; import { formatDateForInput, getLocaleFromString, @@ -12,8 +12,6 @@ import { parseDate, } from "../utils"; import { DateValidationT, UseDatepickerOptions } from "./useDatepicker"; -import { useEscape } from "./useEscape"; -import { useOutsideClickHandler } from "./useOutsideClickHandler"; export type RangeValidationT = { from: DateValidationT; @@ -74,14 +72,28 @@ interface UseRangeDatepickerValue { fromInputProps: Pick< DateInputProps, "onChange" | "onFocus" | "onBlur" | "value" - > & { ref: React.RefObject }; + > & { + /** + * @private + */ + setAnchorRef: React.Dispatch< + React.SetStateAction + >; + }; /** * Use: */ toInputProps: Pick< DateInputProps, "onChange" | "onFocus" | "onBlur" | "value" - > & { ref: React.RefObject }; + > & { + /** + * @private + */ + setAnchorRef?: React.Dispatch< + React.SetStateAction + >; + }; /** * Resets all states (callback) */ @@ -218,14 +230,11 @@ export const useRangeDatepicker = ( onValidate, defaultMonth, allowTwoDigitYear = true, - openOnFocus = true, } = opt; - const locale = getLocaleFromString(_locale); + const [anchorRef, setAnchorRef] = useState(null); - const inputRefTo = useRef(null); - const inputRefFrom = useRef(null); - const [daypickerRef, setDaypickerRef] = useState(); + const locale = getLocaleFromString(_locale); const [defaultSelected, setDefaultSelected] = useState(_defaultSelected); @@ -255,20 +264,6 @@ export const useRangeDatepicker = ( const [open, setOpen] = useState(false); - useOutsideClickHandler(open, setOpen, [ - daypickerRef, - inputRefTo.current, - inputRefFrom.current, - inputRefTo.current?.nextSibling, - inputRefFrom.current?.nextSibling, - ]); - - useEscape( - open, - setOpen, - selectedRange?.from && !selectedRange?.to ? inputRefTo : inputRefFrom - ); - const updateRange = (range?: DateRange) => { onRangeChange?.(range); setSelectedRange(range); @@ -324,7 +319,6 @@ export const useRangeDatepicker = ( if (e.target.readOnly) { return; } - !open && openOnFocus && setOpen(true); const day = parseDate( e.target.value, today, @@ -378,6 +372,7 @@ export const useRangeDatepicker = ( const handleSelect = (range) => { if (range?.from && range?.to) { setOpen(false); + anchorRef?.focus(); } const prevToRange = !selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to; @@ -545,10 +540,12 @@ export const useRangeDatepicker = ( mode: "range" as const, open, onOpenToggle: () => setOpen((x) => !x), + onClose: () => { + setOpen(false); + anchorRef?.focus(); + }, disabled, disableWeekends, - bubbleEscape: true, - ref: setDaypickerRef, }; const fromInputProps = { @@ -556,7 +553,7 @@ export const useRangeDatepicker = ( onFocus: (e) => handleFocus(e, RANGE.FROM), onBlur: (e) => handleBlur(e, RANGE.FROM), value: fromInputValue, - ref: inputRefFrom, + setAnchorRef, }; const toInputProps = { @@ -564,7 +561,7 @@ export const useRangeDatepicker = ( onFocus: (e) => handleFocus(e, RANGE.TO), onBlur: (e) => handleBlur(e, RANGE.TO), value: toInputValue, - ref: inputRefTo, + setAnchorRef, }; return { diff --git a/@navikt/core/react/src/date/index.ts b/@navikt/core/react/src/date/index.ts index cf82aca272..0333881410 100644 --- a/@navikt/core/react/src/date/index.ts +++ b/@navikt/core/react/src/date/index.ts @@ -1,4 +1,3 @@ -export { type DateInputProps } from "./DateInput"; export { default as DatePicker, type DatePickerProps, @@ -15,3 +14,4 @@ export { export { default as MonthPicker } from "./monthpicker/MonthPicker"; export { type MonthPickerStandaloneProps } from "./monthpicker/MonthPickerStandalone"; export { type MonthPickerProps } from "./monthpicker/types"; +export { type DateInputProps } from "./parts/DateInput"; diff --git a/@navikt/core/react/src/date/monthpicker/MonthPicker.tsx b/@navikt/core/react/src/date/monthpicker/MonthPicker.tsx index 664c7794db..0403a5d317 100644 --- a/@navikt/core/react/src/date/monthpicker/MonthPicker.tsx +++ b/@navikt/core/react/src/date/monthpicker/MonthPicker.tsx @@ -1,10 +1,10 @@ import cl from "clsx"; -import React, { forwardRef, useRef, useState } from "react"; +import React, { forwardRef, useMemo, useRef, useState } from "react"; import { RootProvider } from "react-day-picker"; -import { Popover } from "../../popover"; -import { useId } from "../../util"; -import { MonthPickerInput } from "../DateInput"; +import { mergeRefs, useId } from "../../util"; import { DateContext, SharedMonthProvider } from "../context"; +import { MonthPickerInput } from "../parts/DateInput"; +import { DateWrapper } from "../parts/DateWrapper"; import { getLocaleFromString } from "../utils"; import MonthCaption from "./MonthCaption"; import MonthPickerStandalone from "./MonthPickerStandalone"; @@ -74,7 +74,6 @@ export const MonthPicker = forwardRef( year, onYearChange, strategy = "absolute", - bubbleEscape = false, }, ref ) => { @@ -82,6 +81,7 @@ export const MonthPicker = forwardRef( const [open, setOpen] = useState(_open ?? false); const wrapperRef = useRef(null); + const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]); const [selectedMonth, setSelectedMonth] = useState( defaultSelected @@ -107,51 +107,47 @@ export const MonthPicker = forwardRef( onOpenToggle?.(); }, ariaId, + defined: true, }} >
{children} - {(_open ?? open) && ( - onClose?.() ?? setOpen(false)} - placement="bottom-start" - role="dialog" - ref={ref} - id={ariaId} - className="navds-date navds-date__popover" - strategy={strategy} - bubbleEscape={bubbleEscape} - flip={false} + onClose?.() ?? setOpen(false)} + locale={locale} + variant="month" + popoverProps={{ + id: ariaId, + strategy, + }} + > + - -
- - - - -
-
-
- )} +
+ + + + +
+ +
); diff --git a/@navikt/core/react/src/date/monthpicker/types.ts b/@navikt/core/react/src/date/monthpicker/types.ts index 9dab312f40..6446412d67 100644 --- a/@navikt/core/react/src/date/monthpicker/types.ts +++ b/@navikt/core/react/src/date/monthpicker/types.ts @@ -75,9 +75,4 @@ export interface MonthPickerProps extends React.HTMLAttributes { * @default "absolute" */ strategy?: "absolute" | "fixed"; - /** - * Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape - * @default false - */ - bubbleEscape?: boolean; } diff --git a/@navikt/core/react/src/date/DateInput.tsx b/@navikt/core/react/src/date/parts/DateInput.tsx similarity index 80% rename from @navikt/core/react/src/date/DateInput.tsx rename to @navikt/core/react/src/date/parts/DateInput.tsx index 5b219c1462..3960ea4d02 100644 --- a/@navikt/core/react/src/date/DateInput.tsx +++ b/@navikt/core/react/src/date/parts/DateInput.tsx @@ -1,11 +1,11 @@ import { CalendarIcon } from "@navikt/aksel-icons"; import cl from "clsx"; -import React, { forwardRef, InputHTMLAttributes } from "react"; -import { FormFieldProps, useFormField } from "../form/useFormField"; -import { useDateInputContext } from "./context"; -import { ReadOnlyIcon } from "../form/ReadOnlyIcon"; -import { BodyShort, ErrorMessage, Label } from "../typography"; -import { omit } from "../util"; +import React, { forwardRef, InputHTMLAttributes, useRef } from "react"; +import { ReadOnlyIcon } from "../../form/ReadOnlyIcon"; +import { FormFieldProps, useFormField } from "../../form/useFormField"; +import { BodyShort, ErrorMessage, Label } from "../../typography"; +import { omit } from "../../util"; +import { useDateInputContext } from "../context"; export interface DateInputProps extends FormFieldProps, @@ -28,6 +28,10 @@ export interface DateInputProps * @private */ variant?: "datepicker" | "monthpicker"; + /** + * @private + */ + setAnchorRef?: React.Dispatch>; } const DateInput = forwardRef((props, ref) => { @@ -37,9 +41,12 @@ const DateInput = forwardRef((props, ref) => { label, description, variant = "datepicker", + setAnchorRef, ...rest } = props; + const buttonRef = useRef(null); + const isDatepickerVariant = variant === "datepicker"; const conditionalVariables = { @@ -50,7 +57,7 @@ const DateInput = forwardRef((props, ref) => { }, }; - const { onOpen, ariaId, open } = useDateInputContext(); + const context = useDateInputContext(); const { inputProps, @@ -108,7 +115,7 @@ const DateInput = forwardRef((props, ref) => { {...omit(rest, ["error", "errorId", "size"])} {...inputProps} autoComplete="off" - aria-controls={open ? ariaId : undefined} + aria-controls={context?.open ? context.ariaId : undefined} readOnly={readOnly} className={cl( "navds-date__field-input", @@ -116,19 +123,23 @@ const DateInput = forwardRef((props, ref) => { "navds-body-short", `navds-body-short--${size}` )} - size={isDatepickerVariant ? 10 : 14} + size={isDatepickerVariant ? 11 : 14} /> + +
+ ); +}; diff --git a/@navikt/core/react/src/date/utils/labels.ts b/@navikt/core/react/src/date/utils/labels.ts index adf0e23c25..d137e55846 100644 --- a/@navikt/core/react/src/date/utils/labels.ts +++ b/@navikt/core/react/src/date/utils/labels.ts @@ -133,3 +133,86 @@ export const labelWeek = (localeCode?: string): string => { return `Uke:`; } }; + +const modalLabelSingle = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Velg dato`; + case "nn": + return `Vel dato`; + case "en-GB": + return `Pick a date`; + default: + return `Velg dato`; + } +}; + +const modalLabelMultiple = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Velg datoer`; + case "nn": + return `Vel datoar`; + case "en-GB": + return `Pick dates`; + default: + return `Velg datoer`; + } +}; + +const modalLabelRanged = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Velg start- og sluttdato`; + case "nn": + return `Vel start- og sluttdato`; + case "en-GB": + return `Pick a start and end date`; + default: + return `Velg start- og sluttdato`; + } +}; + +const modalLabelMonth = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Velg måned`; + case "nn": + return `Vel månad`; + case "en-GB": + return `Pick a month`; + default: + return `Velg måned`; + } +}; + +export const modalLabel = ( + localeCode: string, + variant: "single" | "multiple" | "range" | "month" +) => { + switch (variant) { + case "single": + return modalLabelSingle(localeCode); + case "multiple": + return modalLabelMultiple(localeCode); + case "range": + return modalLabelRanged(localeCode); + case "month": + return modalLabelMonth(localeCode); + default: + return modalLabelSingle(localeCode); + } +}; + +export const modalCloseButtonLabel = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Lukk`; + case "nn": + return `Lukk`; + case "en-GB": + return `Close`; + default: + return `Lukk`; + } +}; diff --git a/@navikt/core/react/src/guide-panel/guidepanel.stories.tsx b/@navikt/core/react/src/guide-panel/guidepanel.stories.tsx index 2d92fff8e4..ab1b47e7ee 100644 --- a/@navikt/core/react/src/guide-panel/guidepanel.stories.tsx +++ b/@navikt/core/react/src/guide-panel/guidepanel.stories.tsx @@ -1,7 +1,7 @@ +import { InformationIcon } from "@navikt/aksel-icons"; +import { Meta } from "@storybook/react"; import React from "react"; import { BodyLong, GuidePanel, VStack } from "../index"; -import { Meta } from "@storybook/react"; -import { InformationIcon } from "@navikt/aksel-icons"; export default { title: "ds-react/GuidePanel", diff --git a/@navikt/core/react/src/modal/Modal.tsx b/@navikt/core/react/src/modal/Modal.tsx index e9bf338f25..071e587f9b 100644 --- a/@navikt/core/react/src/modal/Modal.tsx +++ b/@navikt/core/react/src/modal/Modal.tsx @@ -8,6 +8,7 @@ import React, { useRef, } from "react"; import { createPortal } from "react-dom"; +import { DateContext } from "../date/context"; import { useProvider } from "../provider"; import { Detail, Heading } from "../typography"; import { mergeRefs, useId } from "../util"; @@ -99,7 +100,9 @@ export const Modal = forwardRef( const rootElement = useProvider()?.rootElement; const portalNode = useFloatingPortalNode({ root: rootElement }); - if (useContext(ModalContext)) { + const dateContext = useContext(DateContext); + const modalContext = useContext(ModalContext); + if (modalContext && !dateContext) { console.error("Modals should not be nested"); } diff --git a/@navikt/core/react/src/popover/Popover.tsx b/@navikt/core/react/src/popover/Popover.tsx index 599ce37c06..1b0e850252 100644 --- a/@navikt/core/react/src/popover/Popover.tsx +++ b/@navikt/core/react/src/popover/Popover.tsx @@ -18,6 +18,7 @@ import React, { useMemo, useRef, } from "react"; +import { DateContext } from "../date/context"; import { ModalContext } from "../modal/ModalContext"; import { mergeRefs, useClientLayoutEffect, useEventListener } from "../util"; import PopoverContent, { PopoverContentType } from "./PopoverContent"; @@ -73,11 +74,6 @@ export interface PopoverProps extends HTMLAttributes { * @default "absolute" */ strategy?: "absolute" | "fixed"; - /** - * Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape - * @default false - */ - bubbleEscape?: boolean; /** * Changes placement of the floating element in order to keep it in view. * @default true @@ -124,7 +120,6 @@ export const Popover = forwardRef( placement = "top", offset, strategy: userStrategy, - bubbleEscape = false, flip: _flip = true, ...rest }, @@ -132,8 +127,9 @@ export const Popover = forwardRef( ) => { const arrowRef = useRef(null); const isInModal = useContext(ModalContext) !== null; + const isInDatepicker = useContext(DateContext) !== null; const chosenStrategy = userStrategy ?? (isInModal ? "fixed" : "absolute"); - const chosenFlip = isInModal ? true : _flip; + const chosenFlip = isInDatepicker ? false : _flip; const { x, @@ -160,11 +156,7 @@ export const Popover = forwardRef( const { getFloatingProps } = useInteractions([ useClick(context), - useDismiss(context, { - bubbles: { - escapeKey: bubbleEscape, - }, - }), + useDismiss(context), ]); useClientLayoutEffect(() => { diff --git a/@navikt/core/react/src/util/__tests__/useMedia.test.tsx b/@navikt/core/react/src/util/__tests__/useMedia.test.tsx new file mode 100644 index 0000000000..8c908b1794 --- /dev/null +++ b/@navikt/core/react/src/util/__tests__/useMedia.test.tsx @@ -0,0 +1,19 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { useMedia } from "../useMedia"; + +function TestComponent({ fallback }: { fallback?: boolean }) { + const media = useMedia("screen and (min-width: 1024px)", fallback); + return
{`${media}`}
; +} + +describe("useMedia", () => { + test("Should return 'undefined' when no fallback is given", async () => { + render(); + expect(screen.getByTestId("media-id").innerHTML).toEqual("undefined"); + }); + test("Should return fallback", async () => { + render(); + expect(screen.getByTestId("media-id").innerHTML).toEqual("true"); + }); +}); diff --git a/@navikt/core/react/src/util/useMedia.ts b/@navikt/core/react/src/util/useMedia.ts new file mode 100644 index 0000000000..35dee321de --- /dev/null +++ b/@navikt/core/react/src/util/useMedia.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; + +export const noMatchMedia = + typeof window !== "undefined" && window.matchMedia === undefined; + +/** + * @example useMedia("screen and (min-width: 1024px)") + * @param media string + * @param fallback boolean + * @returns boolean | undefined + */ +export const useMedia = ( + media: string, + fallback?: boolean +): boolean | undefined => { + const [matches, setMatches] = useState(fallback); + + useEffect(() => { + if (noMatchMedia) { + return; + } + const mediaQueryList = window.matchMedia(media); + + setMatches(mediaQueryList.matches); + + const listener = (evt: MediaQueryListEvent) => { + setMatches(evt.matches); + }; + + mediaQueryList.addEventListener("change", listener); + + return () => { + mediaQueryList.removeEventListener("change", listener); + }; + }, [media]); + + return matches; +}; diff --git a/aksel.nav.no/website/pages/eksempler/datepicker/modal.tsx b/aksel.nav.no/website/pages/eksempler/datepicker/modal.tsx deleted file mode 100644 index 386ab31d05..0000000000 --- a/aksel.nav.no/website/pages/eksempler/datepicker/modal.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { DatePicker, Modal } from "@navikt/ds-react"; -import { withDsExample } from "components/website-modules/examples/withDsExample"; -import format from "date-fns/format"; -import nbLocale from "date-fns/locale/nb"; -import { useState } from "react"; - -const Example = () => { - const [date, setDate] = useState(null); - - return ( - - -
- -
- {date && format(date, "dd.MM.yyyy", { locale: nbLocale })} -
-
-
-
- ); -}; - -export default withDsExample(Example); - -/* Storybook story */ -export const Demo = { - render: Example, -}; - -export const args = { - index: 11, -};