diff --git a/.changeset/serious-bears-cough.md b/.changeset/serious-bears-cough.md new file mode 100644 index 00000000000..7937ed30c58 --- /dev/null +++ b/.changeset/serious-bears-cough.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-react": minor +--- + +Datepicker: Tilbyr nå muligheten til å bruke `onWeekNumberClick`. diff --git a/.changeset/serious-bears-cough2.md b/.changeset/serious-bears-cough2.md new file mode 100644 index 00000000000..a9d0a8f7317 --- /dev/null +++ b/.changeset/serious-bears-cough2.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-css": patch +--- + +Datepicker: Tilpasset padding og sizing på mobil. diff --git a/.changeset/serious-bears-cough3.md b/.changeset/serious-bears-cough3.md new file mode 100644 index 00000000000..a4a7923d79c --- /dev/null +++ b/.changeset/serious-bears-cough3.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-css": patch +--- + +MonthPicker: Tilpasset padding og sizing på mobil. diff --git a/@navikt/core/css/config/_mappings.js b/@navikt/core/css/config/_mappings.js index 09b16b169eb..20a2dea9c37 100644 --- a/@navikt/core/css/config/_mappings.js +++ b/@navikt/core/css/config/_mappings.js @@ -183,7 +183,7 @@ const StyleMappings = { { component: "DatePicker", main: "date.css", - dependencies: [typoCss, "button.css", "popover.css"], + dependencies: [typoCss, primitivesCss, "button.css", "popover.css"], }, { component: "MonthPicker", diff --git a/@navikt/core/css/date.css b/@navikt/core/css/date.css index 4436d7ad4e4..6280171d0d0 100644 --- a/@navikt/core/css/date.css +++ b/@navikt/core/css/date.css @@ -1,5 +1,5 @@ .navds-date { - padding: var(--a-spacing-3); + padding: var(--a-spacing-4) var(--a-spacing-3); } .navds-date .rdp-day_range_middle.rdp-day_disabled { @@ -23,28 +23,29 @@ } .navds-date .rdp-weeknumber { - color: var(--ac-date-week-text, var(--a-text-on-neutral)); - background: var(--ac-date-week-bg, var(--a-surface-neutral)); font-size: var(--a-font-size-small); display: flex; - justify-content: center; align-items: center; - padding: var(--a-spacing-05) var(--a-spacing-1); - border-radius: var(--a-border-radius-small); + 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__caption-dropdown { - display: flex; - justify-content: space-between; - gap: var(--a-spacing-2); - align-items: center; +.navds-date .rdp-weeknumber.rdp-button { + width: 2rem; + height: 2rem; + box-shadow: 0 0 0 1px var(--a-border-default); + color: var(--a-text-subtle); + font-size: var(--a-font-size-small); } -.navds-date__caption__month-wrapper { - display: flex; - justify-content: center; - gap: var(--a-spacing-2); - align-items: center; +.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 { @@ -53,6 +54,7 @@ .navds-date .rdp-button { all: unset; + display: block; width: 2.5rem; height: 2.5rem; text-align: center; @@ -60,13 +62,13 @@ } .navds-date .rdp-day_range_start { - border-radius: var(--a-border-radius-xlarge) var(--a-border-radius-small) var(--a-border-radius-small) + border-radius: var(--a-border-radius-xlarge) var(--a-border-radius-medium) var(--a-border-radius-medium) var(--a-border-radius-xlarge); } .navds-date .rdp-day_range_end:not(.rdp-day_range_start) { - border-radius: var(--a-border-radius-small) var(--a-border-radius-xlarge) var(--a-border-radius-xlarge) - var(--a-border-radius-small); + border-radius: var(--a-border-radius-medium) var(--a-border-radius-xlarge) var(--a-border-radius-xlarge) + var(--a-border-radius-medium); } .navds-date .rdp-day_range_start.rdp-day_range_end { @@ -102,7 +104,7 @@ all: unset; text-align: center; width: 3rem; - height: 2.75rem; + height: 3rem; text-transform: capitalize; border-radius: var(--a-border-radius-medium); cursor: pointer; @@ -188,7 +190,8 @@ display: flex; justify-content: space-between; align-items: center; - gap: var(--a-spacing-2); + gap: var(--a-spacing-1); + padding-inline: var(--a-spacing-1); } .navds-date__caption-button, @@ -287,3 +290,38 @@ .navds-date__field--readonly .navds-date__field-button { cursor: default; } + +.navds-date__caption-button { + width: 2rem; + height: 2rem; +} + +.navds-date__week-row { + display: flex; + align-items: center; + gap: var(--a-spacing-05); +} + +.navds-date__week-wrapper { + width: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + margin: 0; +} + +@media (min-width: 480px) { + .navds-date { + padding: var(--a-spacing-5) var(--a-spacing-4); + } + + .navds-date__caption { + gap: var(--a-spacing-2); + } + + .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 e10da734a20..0f29d9abd39 100644 --- a/@navikt/core/react/src/date/datepicker/DatePicker.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePicker.tsx @@ -5,46 +5,56 @@ import { DateRange, DayPicker, DayPickerBase, - isMatch, Matcher, - SelectMultipleEventHandler, - SelectRangeEventHandler, - SelectSingleEventHandler, + isMatch, } from "react-day-picker"; -import { omit, Popover, useId } from "../.."; +import { Popover, omit, useId } from "../.."; import { DateInputProps, DatePickerInput } from "../DateInput"; import { DateContext } from "../context"; import { getLocaleFromString, labels } from "../utils"; -import { Caption, DropdownCaption } from "./caption"; import DatePickerStandalone, { DatePickerStandaloneType, } from "./DatePickerStandalone"; -import { DayButton } from "./DayButton"; -import { TableHead } from "./TableHead"; +import Caption from "./parts/Caption"; +import DayButton from "./parts/DayButton"; +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"; -export type ConditionalModeProps = - | { - mode?: "single"; - onSelect?: (val?: Date) => void; - selected?: Date; - defaultSelected?: Date; - } - | { - mode?: "multiple"; - onSelect?: (val?: Date[]) => void; - selected?: Date[]; - defaultSelected?: Date[]; - min?: number; - max?: number; - } - | { - mode?: "range"; - onSelect?: (val?: DateRange) => void; - selected?: DateRange; - defaultSelected?: DateRange; - min?: number; - max?: number; - }; +export type SingleMode = { + mode?: "single"; + onSelect?: (val?: Date) => void; + selected?: Date; + defaultSelected?: Date; + onWeekNumberClick?: never; +}; + +export type MultipleMode = { + mode: "multiple"; + onSelect?: (val?: Date[]) => void; + selected?: Date[]; + defaultSelected?: Date[]; + min?: number; + max?: number; + /** + * Allows selecting a week at a time. Only used with mode="multiple". + */ + onWeekNumberClick?: DayPickerBase["onWeekNumberClick"]; +}; + +export type RangeMode = { + mode: "range"; + onSelect?: (val?: DateRange) => void; + selected?: DateRange; + defaultSelected?: DateRange; + min?: number; + max?: number; + onWeekNumberClick?: never; +}; + +type ConditionalModeProps = SingleMode | MultipleMode | RangeMode; //github.com/gpbl/react-day-picker/blob/50b6dba/packages/react-day-picker/src/types/DayPickerBase.ts#L139 export interface DatePickerDefaultProps @@ -186,6 +196,7 @@ export const DatePicker = forwardRef( onOpenToggle, strategy, bubbleEscape = false, + onWeekNumberClick, ...rest }, ref @@ -199,33 +210,20 @@ export const DatePicker = forwardRef( Date | Date[] | DateRange | undefined >(defaultSelected); - const handleSingleSelect: SelectSingleEventHandler = (selectedDay) => { - setSelectedDates(selectedDay); - selectedDay && (onClose?.() ?? setOpen(false)); - rest?.onSelect && (rest?.onSelect as (val?: Date) => void)(selectedDay); - }; + const mode = rest.mode ?? ("single" as any); - const handleMultipleSelect: SelectMultipleEventHandler = (selectedDays) => { - setSelectedDates(selectedDays); - rest?.onSelect && - (rest?.onSelect as (val?: Date[]) => void)(selectedDays); - }; + /** + * @param selected Date | Date[] | DateRange | undefined + */ + const handleSelect = (selected) => { + setSelectedDates(selected); - const handleRangeSelect: SelectRangeEventHandler = (selectedDays) => { - setSelectedDates(selectedDays); - selectedDays?.from && selectedDays?.to && (onClose?.() ?? setOpen(false)); - rest?.onSelect && - (rest?.onSelect as (val?: DateRange) => void)(selectedDays); - }; - - const overrideProps = { - mode: rest.mode ?? ("single" as any), - onSelect: - rest?.mode === "single" - ? handleSingleSelect - : rest?.mode === "multiple" - ? handleMultipleSelect - : handleRangeSelect, + if (rest.mode === "single") { + selected && (onClose?.() ?? setOpen(false)); + } else if (rest.mode === "range") { + selected?.from && selected?.to && (onClose?.() ?? setOpen(false)); + } + rest?.onSelect?.(selected); }; return ( @@ -262,12 +260,16 @@ export const DatePicker = forwardRef( > ( weekend: "rdp-day__weekend", }} showWeekNumber={showWeekNumber} + onWeekNumberClick={ + mode === "multiple" ? onWeekNumberClick : undefined + } fixedWeeks showOutsideDays {...omit(rest, ["onSelect"])} diff --git a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx index 2622c1532bb..2591e21cce6 100644 --- a/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx +++ b/@navikt/core/react/src/date/datepicker/DatePickerStandalone.tsx @@ -1,19 +1,22 @@ import cl from "clsx"; import isWeekend from "date-fns/isWeekend"; import React, { forwardRef } from "react"; -import { - DateRange, - DayPicker, - isMatch, - SelectMultipleEventHandler, - SelectRangeEventHandler, - SelectSingleEventHandler, -} from "react-day-picker"; +import { DateRange, DayPicker, isMatch } from "react-day-picker"; import { omit } from "../.."; import { getLocaleFromString, labels } from "../utils"; -import { Caption, DropdownCaption } from "./caption"; -import { ConditionalModeProps, DatePickerDefaultProps } from "./DatePicker"; -import { TableHead } from "./TableHead"; +import { + DatePickerDefaultProps, + MultipleMode, + RangeMode, + SingleMode, +} from "./DatePicker"; +import TableHead from "./parts/TableHead"; +import WeekNumber from "./parts/WeekNumber"; +import Caption from "./parts/Caption"; +import DropdownCaption from "./parts/DropdownCaption"; +import Row from "./parts/Row"; +import { HeadRow } from "./parts/HeadRow"; +import DayButton from "./parts/DayButton"; interface DatePickerStandaloneDefaultProps extends Omit< @@ -31,8 +34,10 @@ interface DatePickerStandaloneDefaultProps fixedWeeks?: boolean; } +type StandaloneConditionalModeProps = SingleMode | MultipleMode | RangeMode; + export type DatePickerStandaloneProps = DatePickerStandaloneDefaultProps & - ConditionalModeProps; + StandaloneConditionalModeProps; export type DatePickerStandaloneType = React.ForwardRefExoticComponent< DatePickerStandaloneProps & React.RefAttributes @@ -53,7 +58,8 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< selected, defaultSelected, onSelect, - fixedWeeks = true, + fixedWeeks = false, + onWeekNumberClick, ...rest }, ref @@ -62,29 +68,14 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< Date | Date[] | DateRange | undefined >(defaultSelected); - const handleSingleSelect: SelectSingleEventHandler = (selectedDay) => { - setSelectedDates(selectedDay); - onSelect && (onSelect as (val?: Date) => void)(selectedDay); - }; - - const handleMultipleSelect: SelectMultipleEventHandler = (selectedDays) => { - setSelectedDates(selectedDays); - onSelect && (onSelect as (val?: Date[]) => void)(selectedDays); - }; - - const handleRangeSelect: SelectRangeEventHandler = (selectedDays) => { - setSelectedDates(selectedDays); - onSelect && (onSelect as (val?: DateRange) => void)(selectedDays); - }; + const mode = rest.mode ?? ("single" as any); - const overrideProps = { - mode: rest.mode ?? ("single" as any), - onSelect: - rest?.mode === "single" - ? handleSingleSelect - : rest?.mode === "multiple" - ? handleMultipleSelect - : handleRangeSelect, + /** + * @param selected Date | Date[] | DateRange | undefined + */ + const handleSelect = (selected) => { + setSelectedDates(selected); + onSelect?.(selected); }; return ( @@ -94,11 +85,16 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef< > ); diff --git a/@navikt/core/react/src/date/datepicker/caption/DropdownCaption.tsx b/@navikt/core/react/src/date/datepicker/caption/DropdownCaption.tsx deleted file mode 100644 index b4e192d635b..00000000000 --- a/@navikt/core/react/src/date/datepicker/caption/DropdownCaption.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; -import setMonth from "date-fns/setMonth"; -import setYear from "date-fns/setYear"; -import startOfMonth from "date-fns/startOfMonth"; -import React from "react"; -import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker"; -import { Button, Select } from "../../.."; -import { getMonths, getYears } from "../../utils/get-dates"; -import { labelMonthDropdown, labelYearDropdown } from "../../utils/labels"; -import { max, min } from "date-fns"; - -export const DropdownCaption = ({ displayMonth, id }: CaptionProps) => { - const { goToMonth, nextMonth, previousMonth } = useNavigation(); - const { - fromDate, - toDate, - formatters: { formatYearCaption, formatMonthCaption, formatCaption }, - labels: { labelPrevious, labelNext }, - locale, - } = useDayPicker(); - - if (!fromDate || !toDate) { - console.warn("Using dropdownCaption required fromDate and toDate"); - return null; - } - - const handleYearChange: React.ChangeEventHandler = (e) => { - const newMonth = setYear( - startOfMonth(displayMonth), - Number(e.target.value) - ); - goToMonth(startOfMonth(min([max([newMonth, fromDate]), toDate]))); - }; - - const handleMonthChange: React.ChangeEventHandler = (e) => - goToMonth(setMonth(startOfMonth(displayMonth), Number(e.target.value))); - - const years = getYears(fromDate, toDate, displayMonth.getFullYear()); - const months = getMonths(fromDate, toDate, displayMonth); - - const previousLabel = labelPrevious(previousMonth, { locale }); - const nextLabel = labelNext(nextMonth, { locale }); - const yearDropdownLabel = labelYearDropdown(locale); - const MonthDropdownLabel = labelMonthDropdown(locale); - - return ( -
- - {formatCaption(displayMonth, { locale })} - -
- ); -}; - -export default DropdownCaption; diff --git a/@navikt/core/react/src/date/datepicker/caption/index.ts b/@navikt/core/react/src/date/datepicker/caption/index.ts deleted file mode 100644 index 97c41b69d03..00000000000 --- a/@navikt/core/react/src/date/datepicker/caption/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Caption } from "./Caption"; -export { default as DropdownCaption } from "./DropdownCaption"; diff --git a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx index 073662e98b5..707ce2490da 100644 --- a/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx +++ b/@navikt/core/react/src/date/datepicker/datepicker.stories.tsx @@ -2,8 +2,9 @@ import { Meta, StoryObj } from "@storybook/react"; import React, { useId, useState } from "react"; import { useDatepicker, useRangeDatepicker } from ".."; -import { Button } from "../.."; +import { Button, HGrid, VStack } from "../.."; import DatePicker, { DatePickerProps } from "./DatePicker"; +import isSameDay from "date-fns/isSameDay"; const disabledDays = [ new Date("Oct 10 2022"), @@ -367,3 +368,67 @@ export const Readonly = () => { ); }; + +export const StandaloneOptions = () => { + return ( + + + + + + + + ); +}; + +export const WeekDayClick = () => { + const [days, setDays] = useState([]); + + 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) { + setDays(cleanup); + } else { + setDays([...dates, ...cleanup]); + } + }; + + return ( + + handleWeekClick(dates)} + onSelect={(dates) => dates && setDays(dates)} + selected={days} + today={new Date("Nov 23 2022")} + /> + handleWeekClick(dates)} + onSelect={(dates) => dates && setDays(dates)} + selected={days} + today={new Date("Nov 23 2022")} + disableWeekends + /> + + ); +}; diff --git a/@navikt/core/react/src/date/datepicker/caption/Caption.tsx b/@navikt/core/react/src/date/datepicker/parts/Caption.tsx similarity index 66% rename from @navikt/core/react/src/date/datepicker/caption/Caption.tsx rename to @navikt/core/react/src/date/datepicker/parts/Caption.tsx index 152d3eb5b11..71a673fcd6c 100644 --- a/@navikt/core/react/src/date/datepicker/caption/Caption.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/Caption.tsx @@ -2,7 +2,11 @@ import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; import React from "react"; import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker"; import { Button, Label } from "../../.."; +import WeekRow from "./WeekRow"; +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/Caption + */ export const DatePickerCaption = ({ displayMonth, id }: CaptionProps) => { const { goToMonth, nextMonth, previousMonth } = useNavigation(); const { @@ -15,30 +19,27 @@ export const DatePickerCaption = ({ displayMonth, id }: CaptionProps) => { const nextLabel = labelNext(nextMonth, { locale }); return ( -
- - -
+ <> +
-
+ + ); }; diff --git a/@navikt/core/react/src/date/datepicker/DayButton.tsx b/@navikt/core/react/src/date/datepicker/parts/DayButton.tsx similarity index 91% rename from @navikt/core/react/src/date/datepicker/DayButton.tsx rename to @navikt/core/react/src/date/datepicker/parts/DayButton.tsx index 9d8c9c2c8f9..98bc0c23ef7 100644 --- a/@navikt/core/react/src/date/datepicker/DayButton.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/DayButton.tsx @@ -2,7 +2,7 @@ import format from "date-fns/format"; import React, { useRef } from "react"; import { Button, DayProps, useDayPicker, useDayRender } from "react-day-picker"; -export const DayButton = (props: DayProps) => { +const DayButton = (props: DayProps) => { const buttonRef = useRef(null); const dayRender = useDayRender(props.date, props.displayMonth, buttonRef); const { locale } = useDayPicker(); @@ -25,3 +25,5 @@ export const DayButton = (props: DayProps) => { /> ); }; + +export default DayButton; diff --git a/@navikt/core/react/src/date/datepicker/parts/DropdownCaption.tsx b/@navikt/core/react/src/date/datepicker/parts/DropdownCaption.tsx new file mode 100644 index 00000000000..5df6135db6e --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/DropdownCaption.tsx @@ -0,0 +1,113 @@ +import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons"; +import setMonth from "date-fns/setMonth"; +import setYear from "date-fns/setYear"; +import startOfMonth from "date-fns/startOfMonth"; +import React from "react"; +import { CaptionProps, useDayPicker, useNavigation } from "react-day-picker"; +import { Button, Select } from "../../.."; +import { getMonths, getYears } from "../../utils/get-dates"; +import { labelMonthDropdown, labelYearDropdown } from "../../utils/labels"; +import { max, min } from "date-fns"; +import WeekRow from "./WeekRow"; + +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/CaptionDropdowns + */ +export const DropdownCaption = ({ displayMonth, id }: CaptionProps) => { + const { goToMonth, nextMonth, previousMonth } = useNavigation(); + const { + fromDate, + toDate, + formatters: { formatYearCaption, formatMonthCaption, formatCaption }, + labels: { labelPrevious, labelNext }, + locale, + } = useDayPicker(); + + if (!fromDate || !toDate) { + console.warn("Using dropdownCaption required fromDate and toDate"); + return null; + } + + const handleYearChange: React.ChangeEventHandler = (e) => { + const newMonth = setYear( + startOfMonth(displayMonth), + Number(e.target.value) + ); + goToMonth(startOfMonth(min([max([newMonth, fromDate]), toDate]))); + }; + + const handleMonthChange: React.ChangeEventHandler = (e) => + goToMonth(setMonth(startOfMonth(displayMonth), Number(e.target.value))); + + const years = getYears(fromDate, toDate, displayMonth.getFullYear()); + const months = getMonths(fromDate, toDate, displayMonth); + + const previousLabel = labelPrevious(previousMonth, { locale }); + const nextLabel = labelNext(nextMonth, { locale }); + const yearDropdownLabel = labelYearDropdown(locale); + const MonthDropdownLabel = labelMonthDropdown(locale); + + return ( + <> +
+ + {formatCaption(displayMonth, { locale })} + +
+ + + ); +}; + +export default DropdownCaption; diff --git a/@navikt/core/react/src/date/datepicker/parts/HeadRow.tsx b/@navikt/core/react/src/date/datepicker/parts/HeadRow.tsx new file mode 100644 index 00000000000..fa18ba1f934 --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/HeadRow.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { addDays, Locale, startOfWeek } from "date-fns"; +import { useDayPicker } from "react-day-picker"; +import { Hide } from "../../../layout/responsive"; + +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/HeadRow + */ +export function HeadRow(): JSX.Element { + const { + classNames, + styles, + showWeekNumber, + locale, + formatters: { formatWeekdayName }, + labels: { labelWeekday }, + } = useDayPicker(); + + const weekdays = getWeekdays(locale); + + return ( + + {showWeekNumber && ( + + + + )} + {weekdays.map((weekday, i) => ( + + {formatWeekdayName(weekday, { locale })} + + ))} + + ); +} + +/** + * Generate a series of 7 days, starting from the week, to use for formatting + * the weekday names (Monday, Tuesday, etc.). + */ +export function getWeekdays(locale?: Locale): Date[] { + const start = startOfWeek(new Date(), { locale, weekStartsOn: 1 }); + + const days: Date[] = []; + for (let i = 0; i < 7; i++) { + const day = addDays(start, i); + days.push(day); + } + return days; +} diff --git a/@navikt/core/react/src/date/datepicker/parts/Row.tsx b/@navikt/core/react/src/date/datepicker/parts/Row.tsx new file mode 100644 index 00000000000..5c9f3101aaf --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/Row.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { getUnixTime } from "date-fns"; +import { useDayPicker, Day } from "react-day-picker"; +import WeekNumber from "./WeekNumber"; +import { Hide } from "../../../layout/responsive"; + +/** + * The props for the {@link Row} component. + */ +export interface RowProps { + /** The month where the row is displayed. */ + displayMonth: Date; + /** The number of the week to render. */ + weekNumber: number; + /** The days contained in the week. */ + dates: Date[]; +} + +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/Row + */ +function Row(props: RowProps): JSX.Element { + const { styles, classNames, showWeekNumber } = useDayPicker(); + + return ( + + {showWeekNumber && ( + + + + + + )} + {props.dates.map((date) => ( + + + + ))} + + ); +} + +export default Row; diff --git a/@navikt/core/react/src/date/datepicker/TableHead.tsx b/@navikt/core/react/src/date/datepicker/parts/TableHead.tsx similarity index 69% rename from @navikt/core/react/src/date/datepicker/TableHead.tsx rename to @navikt/core/react/src/date/datepicker/parts/TableHead.tsx index f59690647fd..cac2aed7457 100644 --- a/@navikt/core/react/src/date/datepicker/TableHead.tsx +++ b/@navikt/core/react/src/date/datepicker/parts/TableHead.tsx @@ -1,8 +1,10 @@ import React from "react"; import { HeadRow, useDayPicker } from "react-day-picker"; -/** Render the table head. */ -export function TableHead(): JSX.Element { +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/Head + */ +function TableHead(): JSX.Element { const { classNames, styles, components } = useDayPicker(); const HeadRowComponent = components?.HeadRow ?? HeadRow; return ( @@ -11,3 +13,5 @@ export function TableHead(): JSX.Element { ); } + +export default TableHead; diff --git a/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx new file mode 100644 index 00000000000..7b69e29d8ca --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/WeekNumber.tsx @@ -0,0 +1,79 @@ +/* https://github.com/gpbl/react-day-picker/blob/7f78cd5/src/components/WeekNumber/WeekNumber.tsx#L21 */ +import React from "react"; +import { Button, useDayPicker } from "react-day-picker"; +import { labelWeekNumber, labelWeekNumberButton } from "../../utils/labels"; + +export interface WeekNumberProps { + /** The number of the week. */ + number: number; + /** The dates in the week. */ + dates: Date[]; + headerVersion?: boolean; +} + +/** + * https://github.com/gpbl/react-day-picker/tree/main/src/components/WeekNumber + */ +function WeekNumber(props: WeekNumberProps): JSX.Element { + const { number: weekNumber, dates } = props; + const { + onWeekNumberClick, + styles, + classNames, + locale: { code }, + } = useDayPicker(); + + const weekLabel = labelWeekNumber({ + week: Number(weekNumber), + localeCode: code, + }); + + if (!onWeekNumberClick) { + return ( + + {weekNumber} + + ); + } + + const label = labelWeekNumberButton({ + week: Number(weekNumber), + localeCode: code, + }); + + const handleClick: React.MouseEventHandler = function (e) { + onWeekNumberClick(weekNumber, dates, e); + }; + + if (props?.headerVersion) { + return ( + + ); + } + + return ( + + ); +} + +export default WeekNumber; diff --git a/@navikt/core/react/src/date/datepicker/parts/WeekRow.tsx b/@navikt/core/react/src/date/datepicker/parts/WeekRow.tsx new file mode 100644 index 00000000000..2fb12a45b1c --- /dev/null +++ b/@navikt/core/react/src/date/datepicker/parts/WeekRow.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { useDayPicker } from "react-day-picker"; +import { Show } from "../../../layout/responsive"; +import { Detail } from "../../../typography"; +import { getMonthWeeks } from "../../utils/get-month-weeks"; +import { labelWeek } from "../../utils/labels"; +import WeekNumber from "./WeekNumber"; +import { useId } from "../../../util"; + +const WeekRow = ({ displayMonth }: { displayMonth: Date }) => { + const { locale, fixedWeeks, onWeekNumberClick } = useDayPicker(); + + const labelId = useId(); + + if (!onWeekNumberClick) { + return null; + } + + const weeks = getMonthWeeks(displayMonth, { + useFixedWeeks: Boolean(fixedWeeks), + locale, + }); + + return ( + + + + + + + {labelWeek(locale?.code)} + + + + {weeks.map((week) => ( + + ))} + + +
+ + + +
+
+ ); +}; + +export default WeekRow; diff --git a/@navikt/core/react/src/date/utils/__tests__/get-month-weeks.test.ts b/@navikt/core/react/src/date/utils/__tests__/get-month-weeks.test.ts new file mode 100644 index 00000000000..1a8332819e9 --- /dev/null +++ b/@navikt/core/react/src/date/utils/__tests__/get-month-weeks.test.ts @@ -0,0 +1,113 @@ +/* https://github.com/gpbl/react-day-picker/blob/main/src/components/Table/utils/getMonthWeeks.test.ts */ +import { nb, enGB } from "date-fns/locale"; + +import { getMonthWeeks } from "../get-month-weeks"; + +describe('when using the "nB" locale', () => { + const locale = nb; + describe("when getting the weeks for January 2022", () => { + const date = new Date(2022, 0); + const weeks = getMonthWeeks(date, { locale }); + test("the first week should be the last of the previous year", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + expect(weekNumbers[0]).toEqual(52); + }); + test("the first week should contain days from previous year", () => { + expect(weeks[0].dates.map((date) => date.getDate())).toEqual([ + 27, 28, 29, 30, 31, 1, 2, + ]); + }); + test("the last week should be the last of January", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + expect(weekNumbers[weekNumbers.length - 1]).toEqual(5); + }); + }); +}); + +describe('when using the "enGB" locale', () => { + const locale = enGB; + describe("when using fixed weeks", () => { + const useFixedWeeks = true; + describe("when getting the weeks for December 2022", () => { + const date = new Date(2022, 11); + const weeks = getMonthWeeks(date, { useFixedWeeks, locale }); + test("should return 48 - 1 week numbers", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + const expectedResult = [48, 49, 50, 51, 52, 1]; + expect(weekNumbers).toEqual(expectedResult); + }); + test("the last week should be the one in the next year", () => { + const lastWeek = weeks[weeks.length - 1]; + const lastWeekDates = lastWeek.dates.map((date) => date.getDate()); + const expectedResult = [2, 3, 4, 5, 6, 7, 8]; + expect(lastWeekDates).toEqual(expectedResult); + }); + }); + describe("when getting the weeks for December 2021", () => { + const weeks = getMonthWeeks(new Date(2021, 11), { + useFixedWeeks, + locale, + }); + test("should return 48 - 1 week numbers", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + const expectedResult = [48, 49, 50, 51, 52, 1]; + expect(weekNumbers).toEqual(expectedResult); + }); + test("week 1 contains the first day of the new year", () => { + expect(weeks[4].dates.map((date) => date.getDate())).toEqual([ + 27, 28, 29, 30, 31, 1, 2, + ]); + }); + }); + }); +}); + +describe('when using the "nb" locale', () => { + const locale = nb; + describe("when using fixed weeks", () => { + const useFixedWeeks = true; + describe("when getting the weeks for December 2022", () => { + const date = new Date(2022, 11); + const weeks = getMonthWeeks(date, { useFixedWeeks, locale }); + test("should return 48 - 1 week numbers", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + const expectedResult = [48, 49, 50, 51, 52, 1]; + expect(weekNumbers).toEqual(expectedResult); + }); + test("the last week should be the one in the next year", () => { + const lastWeek = weeks[weeks.length - 1]; + const lastWeekDates = lastWeek.dates.map((date) => date.getDate()); + const expectedResult = [2, 3, 4, 5, 6, 7, 8]; + expect(lastWeekDates).toEqual(expectedResult); + }); + }); + describe("when getting the weeks for December 2021", () => { + const weeks = getMonthWeeks(new Date(2021, 11), { + useFixedWeeks, + locale, + }); + test("should return 48 - 1 week numbers", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + const expectedResult = [48, 49, 50, 51, 52, 1]; + expect(weekNumbers).toEqual(expectedResult); + }); + test("week 1 contains the first day of the new year", () => { + expect(weeks[4].dates.map((date) => date.getDate())).toEqual([ + 27, 28, 29, 30, 31, 1, 2, + ]); + }); + }); + }); +}); + +describe("getMonthWeeks should calculate week-number corectly", () => { + const locale = nb; + describe("when getting the weeks for September 2022", () => { + const date = new Date(2022, 8); + const weeks = getMonthWeeks(date, { locale }); + test("the last week should have number 39", () => { + const weekNumbers = weeks.map((week) => week.weekNumber); + expect(weekNumbers[weekNumbers.length - 1]).toEqual(39); + }); + }); +}); diff --git a/@navikt/core/react/src/date/utils/get-month-weeks.ts b/@navikt/core/react/src/date/utils/get-month-weeks.ts new file mode 100644 index 00000000000..8a597dd8aaa --- /dev/null +++ b/@navikt/core/react/src/date/utils/get-month-weeks.ts @@ -0,0 +1,93 @@ +import { + addDays, + addWeeks, + differenceInCalendarDays, + endOfMonth, + endOfWeek, + getWeek, + getWeeksInMonth, + Locale, + startOfMonth, + startOfWeek, +} from "date-fns"; + +export type MonthWeek = { + /** The week number from the start of the year. */ + weekNumber: number; + /** The dates in the week. */ + dates: Date[]; +}; + +export function getMonthWeeks( + month: Date, + options: { + locale: Locale; + useFixedWeeks?: boolean; + } +): MonthWeek[] { + const _options = { + ...options, + weekStartsOn: 1 as const, + }; + const weeksInMonth: MonthWeek[] = daysToMonthWeeks( + startOfMonth(month), + endOfMonth(month), + _options + ); + + if (_options?.useFixedWeeks) { + // Add extra weeks to the month, up to 6 weeks + const nrOfMonthWeeks = getWeeksInMonth(month, _options); + if (nrOfMonthWeeks < 6) { + const lastWeek = weeksInMonth[weeksInMonth.length - 1]; + const lastDate = lastWeek.dates[lastWeek.dates.length - 1]; + const toDate = addWeeks(lastDate, 6 - nrOfMonthWeeks); + const extraWeeks = daysToMonthWeeks( + addWeeks(lastDate, 1), + toDate, + _options + ); + weeksInMonth.push(...extraWeeks); + } + } + return weeksInMonth; +} + +/** Return the weeks between two dates. */ +export function daysToMonthWeeks( + fromDate: Date, + toDate: Date, + options?: { + locale?: Locale; + weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; + } +): MonthWeek[] { + const toWeek = endOfWeek(toDate, options); + const fromWeek = startOfWeek(fromDate, options); + + const nOfDays = differenceInCalendarDays(toWeek, fromWeek); + const days: Date[] = []; + + for (let i = 0; i <= nOfDays; i++) { + days.push(addDays(fromWeek, i)); + } + + const weeksInMonth = days.reduce((result: MonthWeek[], date) => { + const weekNumber = getWeek(date, options); + + const existingWeek = result.find( + (value) => value.weekNumber === weekNumber + ); + if (existingWeek) { + existingWeek.dates.push(date); + return result; + } + result.push({ + weekNumber, + dates: [date], + }); + return result; + }, []); + + return weeksInMonth; +} diff --git a/@navikt/core/react/src/date/utils/labels.ts b/@navikt/core/react/src/date/utils/labels.ts index 6323cac0b5c..adf0e23c25d 100644 --- a/@navikt/core/react/src/date/utils/labels.ts +++ b/@navikt/core/react/src/date/utils/labels.ts @@ -82,3 +82,54 @@ export const labels: Partial = { labelNext, labelPrevious, }; + +export const labelWeekNumber = ({ + localeCode, + week, +}: { + localeCode?: string; + week: number; +}): string => { + switch (localeCode) { + case "nb": + return `Uke ${week}`; + case "nn": + return `Veke ${week}`; + case "en-GB": + return `Week ${week}`; + default: + return `Uke ${week}`; + } +}; + +export const labelWeekNumberButton = ({ + localeCode, + week, +}: { + localeCode?: string; + week: number; +}): string => { + switch (localeCode) { + case "nb": + return `Velg uke ${week}`; + case "nn": + return `Vel veke ${week}`; + case "en-GB": + return `Pick week ${week}`; + default: + return `Velg uke ${week}`; + } +}; + +export const labelWeek = (localeCode?: string): string => { + switch (localeCode) { + case "nb": + return `Uke:`; + case "nn": + return `Veke:`; + case "en-GB": + return `Week:`; + default: + return `Uke:`; + } +};