From 0f04a6a1aa33ca2de88c8c54d358ea651c9a7df9 Mon Sep 17 00:00:00 2001 From: Damien Robson Date: Tue, 12 Nov 2024 08:08:49 +0000 Subject: [PATCH] feat(date-input, date-range): upgrade react-day-picker to v9 Upgrades react-day-picker to remove the dependency on React 18 --- __mocks__/jest/styleMock.js | 1 + jest.config.ts | 4 + package-lock.json | 37 +- package.json | 2 +- playwright/components/date-input/index.ts | 3 +- playwright/components/date-input/locators.ts | 4 +- .../date-picker/date-picker.component.tsx | 233 +++++---- .../date-picker/date-picker.test.tsx | 57 +-- .../date-picker/day-picker.style.ts | 479 ++++++++++++------ .../__internal__/navbar/navbar.component.tsx | 10 +- .../date/__internal__/navbar/navbar.style.ts | 2 +- src/components/date/__internal__/utils.ts | 5 +- .../__internal__/weekday/weekday.style.ts | 4 +- .../__internal__/weekday/weekday.test.tsx | 18 +- src/components/date/date.component.tsx | 21 +- src/components/date/date.mdx | 6 + src/components/date/date.pw.tsx | 244 ++++----- src/components/date/date.stories.tsx | 20 + src/components/date/date.test.tsx | 18 +- .../__internal__/tab-title/tab-title.style.ts | 12 +- 20 files changed, 733 insertions(+), 447 deletions(-) create mode 100644 __mocks__/jest/styleMock.js diff --git a/__mocks__/jest/styleMock.js b/__mocks__/jest/styleMock.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/__mocks__/jest/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/jest.config.ts b/jest.config.ts index be8667404a..919d382488 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,4 +1,5 @@ import { Config } from "jest"; + import coverageThresholds from "./coverage-thresholds.json"; const isCI = process.env.CI === "true"; @@ -30,6 +31,9 @@ const config: Config = { "^.+\\.(js|mjs|jsx|ts|tsx)$": "babel-jest", "^.+\\.svg$": "/svgTransform.mjs", }, + moduleNameMapper: { + "\\.(css|less)$": "/__mocks__/jest/styleMock.js", + }, }; export default config; diff --git a/package-lock.json b/package-lock.json index 788a6d2914..a2d9f99dfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "lodash": "^4.17.21", "polished": "^4.2.2", "prop-types": "^15.8.1", - "react-day-picker": "~7.4.10", + "react-day-picker": "9.3.0", "react-dnd": "^15.1.2", "react-dnd-html5-backend": "^15.1.3", "react-is": "^17.0.2", @@ -2696,6 +2696,12 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@date-fns/tz": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz", + "integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==", + "license": "MIT" + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -24534,14 +24540,33 @@ } }, "node_modules/react-day-picker": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-7.4.10.tgz", - "integrity": "sha512-/QkK75qLKdyLmv0kcVzhL7HoJPazoZXS8a6HixbVoK6vWey1Od1WRLcxfyEiUsRfccAlIlf6oKHShqY2SM82rA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.3.0.tgz", + "integrity": "sha512-xXgZISTXlwQ1Igt4cBttXF+aK1Xvd00azcGVY74PNCAe8PxtULFVWGT1UfdavFiVScF04dyV8QcybKZAw570QQ==", + "license": "MIT", "dependencies": { - "prop-types": "^15.6.2" + "@date-fns/tz": "^1.1.2", + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" }, "peerDependencies": { - "react": "~0.13.x || ~0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0" + "react": ">=16.8.0" + } + }, + "node_modules/react-day-picker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/react-dnd": { diff --git a/package.json b/package.json index 9b6ce76ae3..974cb2cfdf 100644 --- a/package.json +++ b/package.json @@ -195,7 +195,7 @@ "lodash": "^4.17.21", "polished": "^4.2.2", "prop-types": "^15.8.1", - "react-day-picker": "~7.4.10", + "react-day-picker": "9.3.0", "react-dnd": "^15.1.2", "react-dnd-html5-backend": "^15.1.3", "react-is": "^17.0.2", diff --git a/playwright/components/date-input/index.ts b/playwright/components/date-input/index.ts index 187fd6024b..b4cf96625d 100644 --- a/playwright/components/date-input/index.ts +++ b/playwright/components/date-input/index.ts @@ -1,4 +1,5 @@ import type { Page } from "@playwright/test"; + import { DAY_PICKER_WRAPPER, DAY_PICKER_HEADING } from "./locators"; // component preview locators @@ -6,4 +7,4 @@ import { DAY_PICKER_WRAPPER, DAY_PICKER_HEADING } from "./locators"; export const dayPickerWrapper = (page: Page) => page.locator(DAY_PICKER_WRAPPER); export const dayPickerHeading = (page: Page) => - page.locator(DAY_PICKER_HEADING).locator("div"); + page.locator(DAY_PICKER_HEADING); diff --git a/playwright/components/date-input/locators.ts b/playwright/components/date-input/locators.ts index 7e4ea8af56..7dd736eea1 100644 --- a/playwright/components/date-input/locators.ts +++ b/playwright/components/date-input/locators.ts @@ -1,3 +1,3 @@ // component preview locators -export const DAY_PICKER_WRAPPER = 'div[class="DayPicker-wrapper"]'; -export const DAY_PICKER_HEADING = ".DayPicker-Caption"; +export const DAY_PICKER_WRAPPER = 'div[class="rdp-root"]'; +export const DAY_PICKER_HEADING = 'span[class="rdp-caption_label"]'; diff --git a/src/components/date/__internal__/date-picker/date-picker.component.tsx b/src/components/date/__internal__/date-picker/date-picker.component.tsx index d975644df4..63a14f37f1 100644 --- a/src/components/date/__internal__/date-picker/date-picker.component.tsx +++ b/src/components/date/__internal__/date-picker/date-picker.component.tsx @@ -1,37 +1,44 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; -import DayPicker, { +import { flip, offset } from "@floating-ui/dom"; +import React, { + useCallback, + KeyboardEvent, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + DayPicker, DayPickerProps, - DayModifiers, - Modifier, - LocaleUtils, + defaultLocale, + Modifiers, } from "react-day-picker"; -import { flip, offset } from "@floating-ui/dom"; -import { getDisabledDays } from "../utils"; -import Popover from "../../../../__internal__/popover"; import useLocale from "../../../../hooks/__internal__/useLocale"; +import Popover from "../../../../__internal__/popover"; import Navbar from "../navbar"; import Weekday from "../weekday"; -import StyledDayPicker from "./day-picker.style"; -import Events from "../../../../__internal__/utils/helpers/events"; +import { getDisabledDays } from "../utils"; import { defaultFocusableSelectors } from "../../../../__internal__/focus-trap/focus-trap-utils"; +import Events from "../../../../__internal__/utils/helpers/events"; + +import StyledDayPicker from "./day-picker.style"; + +import "react-day-picker/style.css"; type CustomRefObject = { current?: T | null; }; -/** there is an issue with typescript-to-proptypes package that means we need to override these types */ -interface Modifiers { - today: NonNullable | NonNullable[]; - outside: NonNullable | NonNullable[]; - [other: string]: NonNullable | NonNullable[]; -} - export interface PickerProps - extends Omit { - disabledDays?: NonNullable | NonNullable[] | undefined[]; + extends Omit< + DayPickerProps, + "mode" | "disabledDays" | "modifiers" | "selectedDays" + > { + mode: "single"; + disabledDays?: NonNullable | NonNullable[] | undefined[]; modifiers?: Partial; - selectedDays?: NonNullable | NonNullable[] | undefined[]; + selectedDays?: NonNullable | NonNullable[] | undefined[]; } export interface DatePickerProps { @@ -46,7 +53,7 @@ export interface DatePickerProps { /** Element that the DatePicker will be displayed under */ inputElement: CustomRefObject; /** Currently selected date */ - selectedDays?: Date; + selectedDays?: Date | undefined; /** Callback to handle mousedown event on picker container */ pickerMouseDown?: () => void; /** Sets whether the picker should be displayed */ @@ -82,6 +89,15 @@ export const DatePicker = ({ pickerTabGuardId, onPickerClose, }: DatePickerProps) => { + if ((minDate && !maxDate) || (!minDate && maxDate)) { + console.warn( + "Both minDate and maxDate must be provided to DatePicker component in order to set disabled dates.", + ); + } + + const [focusedMonth, setFocusedMonth] = useState( + selectedDays || new Date(), + ); const locale = useLocale(); const { localize, options } = locale.date.dateFnsLocale(); const { weekStartsOn } = options || /* istanbul ignore next */ {}; @@ -93,13 +109,6 @@ export const DatePicker = ({ }), [localize], ); - const monthsShort = useMemo( - () => - Array.from({ length: 12 }).map((_, i) => - localize?.month(i, { width: "abbreviated" }).substring(0, 3), - ), - [localize], - ); const weekdaysLong = useMemo( () => Array.from({ length: 7 }).map((_, i) => localize?.day(i)), [localize], @@ -122,7 +131,7 @@ export const DatePicker = ({ useEffect(() => { if (open) { // this is a temporary fix for some axe issues that are baked into the library we use for the picker - const captionElement = ref.current?.querySelector(".DayPicker-Caption"); + const captionElement = ref.current?.querySelector(".rdp-month_caption"); /* istanbul ignore else */ if (captionElement) { captionElement.removeAttribute("role"); @@ -130,41 +139,27 @@ export const DatePicker = ({ } // focus the selected or today's date first + /* istanbul ignore next */ const selectedDay = - ref.current?.querySelector(".DayPicker-Day--selected") || - ref.current?.querySelector(".DayPicker-Day--today"); - const firstDay = ref.current?.querySelector( - ".DayPicker-Day[tabindex='0']", - ); + ref.current?.querySelector(".rdp-selected") || + ref.current?.querySelector(".rdp-today"); + const firstDay = ref.current?.querySelector(".rdp-day[tabindex='0']"); /* istanbul ignore else */ if (selectedDay && firstDay !== selectedDay) { - selectedDay?.setAttribute("tabindex", "0"); firstDay?.setAttribute("tabindex", "-1"); } } }, [open]); - const handleDayClick = ( - date: Date, - modifiers: DayModifiers, - ev: React.MouseEvent, - ) => { - if (!modifiers.disabled) { - const { id, name } = inputElement?.current - ?.firstChild as HTMLInputElement; - ev.target = { - ...ev.target, - id, - name, - } as HTMLInputElement; - onDayClick?.(date, ev); - onPickerClose?.(); - } + const handleDayClick = (date?: Date) => { + /* istanbul ignore else */ + if (date) onDayClick?.(date, {} as React.MouseEvent); + onPickerClose?.(); }; const handleKeyUp = useCallback( - (ev) => { + (ev: KeyboardEvent) => { /* istanbul ignore else */ if (open && Events.isEscKey(ev)) { inputElement.current?.querySelector("input")?.focus(); @@ -179,7 +174,7 @@ export const DatePicker = ({ const handleOnKeyDown = (ev: React.KeyboardEvent) => { /* istanbul ignore else */ if ( - ref.current?.querySelector(".DayPicker-NavBar button") === + ref.current?.querySelector(".rdp-nav button") === document.activeElement && Events.isTabKey(ev) && Events.isShiftKey(ev) @@ -193,7 +188,7 @@ export const DatePicker = ({ const handleOnDayKeyDown = ( _day: Date, - _modifiers: DayModifiers, + _modifiers: Modifiers, ev: React.KeyboardEvent, ) => { // we need to manually handle this as the picker may be in a Portal @@ -223,66 +218,100 @@ export const DatePicker = ({ } }; - const formatDay = (date: Date) => - `${weekdaysShort[date.getDay()]} ${date.getDate()} ${ - monthsShort[date.getMonth()] - } ${date.getFullYear()}`; + useEffect(() => { + if (selectedDays) { + setFocusedMonth(selectedDays); + } + }, [selectedDays]); if (!open) { return null; } - const localeUtils = { formatDay } as LocaleUtils; - const handleTabGuardFocus = () => { ref.current?.querySelector("button")?.focus(); }; return ( - - + -
- { - const { className, weekday } = weekdayElementProps; + +
+ { + const date = e as Date; + handleDayClick(date); + }} + components={{ + Nav: (props) => { + return ; + }, + Weekday: (props) => { + const fixedDays = { + Sunday: 0, + Monday: 1, + Tuesday: 2, + Wednesday: 3, + Thursday: 4, + Friday: 5, + Saturday: 6, + }; + const { className, "aria-label": ariaLabel } = props; + const dayIndex = fixedDays[ariaLabel as keyof typeof fixedDays]; - return ( - - {weekdaysShort[weekday]} - - ); - }} - navbarElement={} - fixedWeeks - initialMonth={selectedDays || undefined} - disabledDays={getDisabledDays(minDate, maxDate)} - locale={locale.locale()} - localeUtils={localeUtils} - onDayKeyDown={handleOnDayKeyDown} - {...pickerProps} - /> - - + return ( + + {weekdaysShort[dayIndex]} + + ); + }, + }} + fixedWeeks + defaultMonth={selectedDays || undefined} + onDayKeyDown={(date, modifiers, e) => { + handleOnDayKeyDown( + date, + modifiers, + e as React.KeyboardEvent, + ); + }} + {...pickerProps} + showOutsideDays + /> + + + ); }; diff --git a/src/components/date/__internal__/date-picker/date-picker.test.tsx b/src/components/date/__internal__/date-picker/date-picker.test.tsx index fbb0f2a94b..85a8d196a8 100644 --- a/src/components/date/__internal__/date-picker/date-picker.test.tsx +++ b/src/components/date/__internal__/date-picker/date-picker.test.tsx @@ -1,7 +1,6 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; - import enGBLocale from "date-fns/locale/en-GB"; import deLocale from "date-fns/locale/de"; import esLocale from "date-fns/locale/es"; @@ -22,7 +21,7 @@ const DatePickerWithInput = (props: MockProps) => { const ref = React.useRef(null); const Input = () => (
- +
); return ( @@ -60,9 +59,15 @@ test("should render the day element that matches the `selectedDate` when prop is open />, ); - const selectedDay = screen.getByRole("gridcell", { name: "Thu 4 Apr 2019" }); - expect(selectedDay).toHaveAttribute("aria-selected", "true"); + const selectedDay = screen.getByLabelText("Thursday, April 4th, 2019", { + exact: false, + }); + + expect(selectedDay).toHaveAttribute( + "aria-label", + "Thursday, April 4th, 2019, selected", + ); }); test("should render the expected weekday with `aria-disabled=true` attribute when `minDate` is '2019-04-02'", () => { @@ -74,11 +79,11 @@ test("should render the expected weekday with `aria-disabled=true` attribute whe open />, ); - const disabledDay = screen.getByRole("gridcell", { name: "Mon 1 Apr 2019" }); - const activeDay = screen.getByRole("gridcell", { name: "Tue 2 Apr 2019" }); + const disabledDay = screen.getByLabelText("Monday, April 1st, 2019"); + const activeDay = screen.getByLabelText("Tuesday, April 2nd, 2019"); - expect(disabledDay).toHaveAttribute("aria-disabled", "true"); - expect(activeDay).toHaveAttribute("aria-disabled", "false"); + expect(disabledDay).toBeDisabled(); + expect(activeDay).toBeEnabled(); }); test("should not render any of the current month's weekdays with `aria-disabled=true` attribute when `minDate` is invalid", () => { @@ -91,12 +96,10 @@ test("should not render any of the current month's weekdays with `aria-disabled= />, ); // need to filter out the weekdays that are not in the current month - const currentMonthDays = screen.getAllByRole("gridcell", { - name: new RegExp("Apr", "i"), - }); + const currentMonthDays = screen.getAllByLabelText("April", { exact: false }); currentMonthDays.forEach((day) => { - expect(day).toHaveAttribute("aria-disabled", "false"); + expect(day).toBeEnabled(); }); }); @@ -110,12 +113,10 @@ test("should not render any of the current month's weekdays with `aria-disabled= />, ); // need to filter out the weekdays that are not in the current month - const currentMonthDays = screen.getAllByRole("gridcell", { - name: new RegExp("Apr", "i"), - }); + const currentMonthDays = screen.getAllByLabelText("April", { exact: false }); currentMonthDays.forEach((day) => { - expect(day).toHaveAttribute("aria-disabled", "false"); + expect(day).toBeEnabled(); }); }); @@ -128,11 +129,11 @@ test("should render the expected weekday with `aria-disabled=true` attribute whe open />, ); - const disabledDay = screen.getByRole("gridcell", { name: "Sat 6 Apr 2019" }); - const activeDay = screen.getByRole("gridcell", { name: "Fri 5 Apr 2019" }); + const disabledDay = screen.getByLabelText("Saturday, April 6th, 2019"); + const activeDay = screen.getByLabelText("Friday, April 5th, 2019"); - expect(disabledDay).toHaveAttribute("aria-disabled", "true"); - expect(activeDay).toHaveAttribute("aria-disabled", "false"); + expect(disabledDay).toBeDisabled(); + expect(activeDay).toBeEnabled(); }); test("should not render any of the current month's weekdays with `aria-disabled=true` attribute when `maxDate` is invalid", () => { @@ -145,12 +146,10 @@ test("should not render any of the current month's weekdays with `aria-disabled= />, ); // need to filter out the weekdays that are not in the current month - const currentMonthDays = screen.getAllByRole("gridcell", { - name: new RegExp("Apr", "i"), - }); + const currentMonthDays = screen.getAllByLabelText("April", { exact: false }); currentMonthDays.forEach((day) => { - expect(day).toHaveAttribute("aria-disabled", "false"); + expect(day).toBeEnabled(); }); }); @@ -164,12 +163,10 @@ test("should not render any of the current month's weekdays with `aria-disabled= />, ); // need to filter out the weekdays that are not in the current month - const currentMonthDays = screen.getAllByRole("gridcell", { - name: new RegExp("Apr", "i"), - }); + const currentMonthDays = screen.getAllByLabelText("April", { exact: false }); currentMonthDays.forEach((day) => { - expect(day).toHaveAttribute("aria-disabled", "false"); + expect(day).toBeEnabled(); }); }); @@ -185,7 +182,7 @@ test("should not call `onDayClick` callback when a user clicks a disabled day", onDayClick={onDayClick} />, ); - const disabledDay = screen.getByRole("gridcell", { name: "Mon 1 Apr 2019" }); + const disabledDay = screen.getByLabelText("Monday, April 1st, 2019"); await user.click(disabledDay); expect(onDayClick).not.toHaveBeenCalled(); @@ -202,7 +199,7 @@ test("should call `onDayClick` callback when a user clicks a day that is not dis onDayClick={onDayClick} />, ); - const activeDay = screen.getByRole("gridcell", { name: "Tue 2 Apr 2019" }); + const activeDay = screen.getByLabelText("Tuesday, April 2nd, 2019"); await user.click(activeDay); expect(onDayClick).toHaveBeenCalled(); diff --git a/src/components/date/__internal__/date-picker/day-picker.style.ts b/src/components/date/__internal__/date-picker/day-picker.style.ts index 3fff700e4b..5405497fe0 100644 --- a/src/components/date/__internal__/date-picker/day-picker.style.ts +++ b/src/components/date/__internal__/date-picker/day-picker.style.ts @@ -1,4 +1,5 @@ import styled, { css } from "styled-components"; + import baseTheme from "../../../../style/themes/base"; import addFocusStyling from "../../../../style/utils/add-focus-styling"; @@ -6,185 +7,316 @@ const oldFocusStyling = ` outline: solid 3px var(--colorsSemanticFocus500); `; -// Styles copied from https://github.com/gpbl/react-day-picker/blob/v6.1.1/src/style.css -const addReactDayPickerStyles = () => ` - .DayPicker { - display: inline-block; +const officalReactDayPickerStyling = () => css` + /* Variables declaration */ + /* prettier-ignore */ + .rdp-root { + --rdp-accent-color: blue; /* The accent color used for selected days and UI elements. */ + --rdp-accent-background-color: #f0f0ff; /* The accent background color used for selected days and UI elements. */ + + --rdp-day-height: 2.75rem; /* The height of the day cells. */ + --rdp-day-width: 2.75rem; /* The width of the day cells. */ + + --rdp-day_button-border-radius: 100%; /* The border radius of the day cells. */ + --rdp-day_button-border: 2px solid transparent; /* The border of the day cells. */ + --rdp-day_button-height: var(--rdp-day-height); /* The height of the day cells. */ + --rdp-day_button-width: var(--rdp-day-width); /* The width of the day cells. */ + + --rdp-selected-border: 2px solid var(--rdp-accent-color); /* The border of the selected days. */ + --rdp-disabled-opacity: 0.5; /* The opacity of the disabled days. */ + --rdp-outside-opacity: 0.75; /* The opacity of the days outside the current month. */ + --rdp-today-color: var(--rdp-accent-color); /* The color of the today's date. */ + + --rdp-dropdown-gap: 0.5rem;/* The gap between the dropdowns used in the month captons. */ + + --rdp-months-gap: 2rem; /* The gap between the months in the multi-month view. */ + + --rdp-nav_button-disabled-opacity: 0.5; /* The opacity of the disabled navigation buttons. */ + --rdp-nav_button-height: 2.25rem; /* The height of the navigation buttons. */ + --rdp-nav_button-width: 2.25rem; /* The width of the navigation buttons. */ + --rdp-nav-height: 2.75rem; /* The height of the navigation bar. */ + + --rdp-range_middle-background-color: var(--rdp-accent-background-color); /* The color of the background for days in the middle of a range. */ + --rdp-range_middle-foreground-color: white; /* The foregraound color for days in the middle of a range. */ + --rdp-range_middle-color: inherit;/* The color of the range text. */ + + --rdp-range_start-color: white; /* The color of the range text. */ + --rdp-range_start-background: linear-gradient(var(--rdp-gradient-direction), transparent 50%, var(--rdp-range_middle-background-color) 50%); /* Used for the background of the start of the selected range. */ + --rdp-range_start-date-background-color: var(--rdp-accent-color); /* The background color of the date when at the start of the selected range. */ + + --rdp-range_end-background: linear-gradient(var(--rdp-gradient-direction), var(--rdp-range_middle-background-color) 50%, transparent 50%); /* Used for the background of the end of the selected range. */ + --rdp-range_end-color: white;/* The color of the range text. */ + --rdp-range_end-date-background-color: var(--rdp-accent-color); /* The background color of the date when at the end of the selected range. */ + + --rdp-week_number-border-radius: 100%; /* The border radius of the week number. */ + --rdp-week_number-border: 2px solid transparent; /* The border of the week number. */ + + --rdp-week_number-height: var(--rdp-day-height); /* The height of the week number cells. */ + --rdp-week_number-opacity: 0.75; /* The opacity of the week number. */ + --rdp-week_number-width: var(--rdp-day-width); /* The width of the week number cells. */ + --rdp-weeknumber-text-align: center; /* The text alignment of the weekday cells. */ + + --rdp-weekday-opacity: 0.75; /* The opacity of the weekday. */ + --rdp-weekday-padding: 0.5rem 0rem; /* The padding of the weekday. */ + --rdp-weekday-text-align: center; /* The text alignment of the weekday cells. */ + + --rdp-gradient-direction: 90deg; } - .DayPicker-wrapper { - display: flex; - flex-wrap: wrap; - justify-content: center; - position: relative; - user-select: none; - flex-direction: row; - padding: 1rem 0; + .rdp-root[dir="rtl"] { + --rdp-gradient-direction: -90deg; } - .DayPicker-Month { - display: table; - border-collapse: collapse; - border-spacing: 0; - user-select: none; - margin: 0 1rem; + /* Root of the component. */ + .rdp-root { + position: relative; /* Required to position the navigation toolbar. */ + box-sizing: border-box; } - .DayPicker-NavBar { - position: absolute; - left: 0; - right: 0; + .rdp-root * { + box-sizing: border-box; } - .DayPicker-NavButton { - position: absolute; - width: 1.5rem; - height: 1.5rem; - background-repeat: no-repeat; - background-position: center; - background-size: contain; + .rdp-day { + width: var(--rdp-day-width); + height: var(--rdp-day-height); + text-align: center; + } + + .rdp-day_button { + background: none; + padding: 0; + margin: 0; cursor: pointer; + font: inherit; + color: inherit; + justify-content: center; + align-items: center; + display: flex; + + width: var(--rdp-day_button-width); + height: var(--rdp-day_button-height); + border: var(--rdp-day_button-border); + border-radius: var(--rdp-day_button-border-radius); } - .DayPicker-NavButton--prev { - left: 1rem; - background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2cHgiIGhlaWdodD0iNTBweCIgdmlld0JveD0iMCAwIDI2IDUwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5wcmV2PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9InByZXYiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEzLjM5MzE5MywgMjUuMDAwMDAwKSBzY2FsZSgtMSwgMSkgdHJhbnNsYXRlKC0xMy4zOTMxOTMsIC0yNS4wMDAwMDApIHRyYW5zbGF0ZSgwLjg5MzE5MywgMC4wMDAwMDApIiBmaWxsPSIjNTY1QTVDIj4KICAgICAgICAgICAgPHBhdGggZD0iTTAsNDkuMTIzNzMzMSBMMCw0NS4zNjc0MzQ1IEwyMC4xMzE4NDU5LDI0LjcyMzA2MTIgTDAsNC4yMzEzODMxNCBMMCwwLjQ3NTA4NDQ1OSBMMjUsMjQuNzIzMDYxMiBMMCw0OS4xMjM3MzMxIEwwLDQ5LjEyMzczMzEgWiIgaWQ9InJpZ2h0IiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+CiAgICAgICAgPC9nPgogICAgPC9nPgo8L3N2Zz4K"); + .rdp-day_button:disabled { + cursor: revert; } - .DayPicker-NavButton--next { - right: 1rem; - background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjI2cHgiIGhlaWdodD0iNTBweCIgdmlld0JveD0iMCAwIDI2IDUwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbG5zOnNrZXRjaD0iaHR0cDovL3d3dy5ib2hlbWlhbmNvZGluZy5jb20vc2tldGNoL25zIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggMy4zLjIgKDEyMDQzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5uZXh0PC90aXRsZT4KICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPgogICAgPGRlZnM+PC9kZWZzPgogICAgPGcgaWQ9IlBhZ2UtMSIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSI+CiAgICAgICAgPGcgaWQ9Im5leHQiIHNrZXRjaDp0eXBlPSJNU0xheWVyR3JvdXAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuOTUxNDUxLCAwLjAwMDAwMCkiIGZpbGw9IiM1NjVBNUMiPgogICAgICAgICAgICA8cGF0aCBkPSJNMCw0OS4xMjM3MzMxIEwwLDQ1LjM2NzQzNDUgTDIwLjEzMTg0NTksMjQuNzIzMDYxMiBMMCw0LjIzMTM4MzE0IEwwLDAuNDc1MDg0NDU5IEwyNSwyNC43MjMwNjEyIEwwLDQ5LjEyMzczMzEgTDAsNDkuMTIzNzMzMSBaIiBpZD0icmlnaHQiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPgo="); + .rdp-day_button { + outline: none; } - .DayPicker-NavButton--interactionDisabled { - display: none; + .rdp-caption_label { + z-index: 1; + + position: relative; + display: inline-flex; + align-items: center; + + white-space: nowrap; + border: 0; } - .DayPicker-Caption { - display: table-caption; - height: 1.5rem; - text-align: center; + .rdp-button_next, + .rdp-button_previous { + border: none; + background: none; + padding: 0; + margin: 0; + cursor: pointer; + font: inherit; + color: inherit; + -moz-appearance: none; + -webkit-appearance: none; + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + appearance: none; + + width: var(--rdp-nav_button-width); + height: var(--rdp-nav_button-height); } - .DayPicker-Weekdays { - display: table-header-group; + .rdp-button_next:disabled, + .rdp-button_previous:disabled { + cursor: revert; + + opacity: var(--rdp-nav_button-disabled-opacity); } - .DayPicker-WeekdaysRow { - display: table-row; + .rdp-chevron { + display: inline-block; + fill: var(--rdp-accent-color); } - .DayPicker-Weekday { - display: table-cell; + .rdp-root[dir="rtl"] .rdp-nav .rdp-chevron { + transform: rotate(180deg); + } - abbr { - text-decoration: none; - } + .rdp-root[dir="rtl"] .rdp-nav .rdp-chevron { + transform: rotate(180deg); + transform-origin: 50%; } - .DayPicker-Body { - display: table-row-group; + .rdp-dropdowns { + position: relative; + display: inline-flex; + align-items: center; + gap: var(--rdp-dropdown-gap); } + .rdp-dropdown { + z-index: 2; - .DayPicker-Week { - display: table-row; + /* Reset */ + opacity: 0; + appearance: none; + position: absolute; + inset-block-start: 0; + inset-block-end: 0; + inset-inline-start: 0; + width: 100%; + margin: 0; + padding: 0; + cursor: inherit; + border: none; + line-height: inherit; } - .DayPicker-Day { - display: table-cell; - padding: 0.5rem; - border: 1px solid #eaecec; - text-align: center; - cursor: pointer; - vertical-align: middle; + .rdp-dropdown_root { + position: relative; + display: inline-flex; + align-items: center; } - .DayPicker-WeekNumber { - display: table-cell; - padding: 0.5rem; - text-align: right; - vertical-align: middle; - min-width: 1rem; - font-size: 0.75em; - cursor: pointer; - color: #8b9898; + .rdp-dropdown_root[data-disabled="true"] .rdp-chevron { + opacity: var(--rdp-disabled-opacity); } - .DayPicker--interactionDisabled .DayPicker-Day { - cursor: default; + .rdp-month_caption { + display: flex; + align-content: center; + height: var(--rdp-nav-height); + font-weight: bold; + font-size: large; } - .DayPicker-Footer { - display: table-caption; - caption-side: bottom; - padding-top: 0.5rem; + .rdp-months { + position: relative; + display: flex; + flex-wrap: wrap; + gap: var(--rdp-months-gap); + max-width: fit-content; } - .DayPicker-TodayButton { - border: none; - background-image: none; - background-color: transparent; - box-shadow: none; - cursor: pointer; - color: #4a90e2; - font-size: 0.875em; + .rdp-month_grid { + border-collapse: collapse; } - /* Default modifiers */ + .rdp-nav { + position: absolute; + inset-block-start: 0; + inset-inline-end: 0; + + display: flex; + align-items: center; + + height: var(--rdp-nav-height); + width: 100%; + } - .DayPicker-Day--today { - color: #d0021b; + .rdp-weekday { + opacity: var(--rdp-weekday-opacity); + padding: var(--rdp-weekday-padding); font-weight: 500; + font-size: smaller; + text-align: var(--rdp-weekday-text-align); + text-transform: var(--rdp-weekday-text-transform); } - .DayPicker-Day--disabled { - color: #dce0e0; - cursor: default; - background-color: #eff1f1; + .rdp-week_number { + opacity: var(--rdp-week_number-opacity); + font-weight: 400; + font-size: small; + height: var(--rdp-week_number-height); + width: var(--rdp-week_number-width); + border: var(--rdp-week_number-border); + border-radius: var(--rdp-week_number-border-radius); + text-align: var(--rdp-weeknumber-text-align); } - .DayPicker-Day--outside { - cursor: default; - color: #dce0e0; + /* DAY MODIFIERS */ + .rdp-today:not(.rdp-outside) { + color: var(--rdp-today-color); } - /* Example modifiers */ + .rdp-selected { + font-weight: bold; + font-size: large; + } - .DayPicker-Day--sunday { - background-color: #f7f8f8; + .rdp-selected .rdp-day_button { + border: var(--rdp-selected-border); } - .DayPicker-Day--sunday:not(.DayPicker-Day--today) { - color: #dce0e0; + .rdp-outside { + opacity: var(--rdp-outside-opacity); } - .DayPicker-Day--selected:not(.DayPicker-Day--disabled):not(.DayPicker-Day--outside) { - color: #fff; - background-color: #4a90e2; + .rdp-disabled { + opacity: var(--rdp-disabled-opacity); } - /* DayPickerInput */ + .rdp-hidden { + visibility: hidden; + color: var(--rdp-range_start-color); + } - .DayPickerInput { - display: inline-block; + .rdp-range_start { + background: var(--rdp-range_start-background); } - .DayPickerInput-OverlayWrapper { - position: relative; + .rdp-range_start .rdp-day_button { + background-color: var(--rdp-range_start-date-background-color); + color: var(--rdp-range_start-color); } - .DayPickerInput-Overlay { - left: 0; - position: absolute; - background: white; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + .rdp-range_middle { + background-color: var(--rdp-range_middle-background-color); + } + + .rdp-range_middle .rdp-day_button { + border-color: transparent; + border: unset; + border-radius: unset; + color: var(--rdp-range_middle-color); + } + + .rdp-range_end { + background: var(--rdp-range_end-background); + color: var(--rdp-range_end-color); + } + + .rdp-range_end .rdp-day_button { + color: var(--rdp-range_start-color); + background-color: var(--rdp-range_end-date-background-color); + } + + .rdp-range_start.rdp-range_end { + background: revert; + } + + .rdp-focusable { + cursor: pointer; } `; const StyledDayPicker = styled.div` - ${addReactDayPickerStyles} + ${officalReactDayPickerStyling} - position: absolute; + /* position: absolute; height: 346px; width: 352px; ${({ theme }) => css` @@ -193,9 +325,9 @@ const StyledDayPicker = styled.div` ` margin-top: var(--spacing050); `} - `} + `} */ - .DayPicker { + .rdp-root { z-index: 1000; top: calc(100% + 1px); left: 0; @@ -209,46 +341,41 @@ const StyledDayPicker = styled.div` border-radius: var(--borderRadius050); } - .DayPicker * { + .rdp-root * { box-sizing: border-box; } - .DayPicker:focus { + .rdp-root:focus { outline: none; } - .DayPicker abbr[title] { + .rdp-root abbr[title] { border: none; cursor: initial; } - .DayPicker-wrapper { + .rdp-months { padding: 0; - &:focus { - ${({ theme }) => - !theme.focusRedesignOptOut - ? addFocusStyling() - : /* istanbul ignore next */ oldFocusStyling} - border-radius: var(--borderRadius050); - } } - .DayPicker-Month { + .rdp-month { margin: 0 0 2px; } - .DayPicker-Body, - .DayPicker-Week { + .rdp-month_grid, + .rdp_weeks { width: 100%; + margin-top: 8px; } - .DayPicker-Caption { + .rdp-month_caption { color: var(--colorsActionMajorYin090); line-height: var(--sizing500); height: var(--sizing500); //font: var(--typographyDatePickerCalendarMonthM); font assets to be updated part of FE-4975 font-size: 16px; font-weight: 800; + display: block; > div { margin: 0 auto; @@ -256,15 +383,25 @@ const StyledDayPicker = styled.div` } } - .DayPicker-Day { + .rdp-weekday { + border: medium; + height: var(--sizing500); + min-width: var(--sizing500); + font-weight: 800; + color: var(--colorsActionMinor500); + text-transform: uppercase; + font-size: 12px; + text-align: center; + padding: 20px 0px 5px; + } + + .rdp-day { min-width: var(--sizing500); height: var(--sizing450); padding: 0; - background-color: var(--colorsUtilityYang100); + background-color: transparent; cursor: pointer; border: none; - //font-family: var(--fontFamiliesDefault); font assets to be updated part of FE-4975 - //font: var(--typographyDatePickerCalendarDateM); font assets to be updated part of FE-4975 font-weight: var(--fontWeights500); font-size: var(--fontSizes100); line-height: var(--lineHeights500); @@ -275,17 +412,6 @@ const StyledDayPicker = styled.div` color: var(--colorsActionMajorYin090); } - ${({ theme }) => - ` - &:focus { - ${ - !theme.focusRedesignOptOut - ? addFocusStyling(true) - : /* istanbul ignore next */ oldFocusStyling - } - } - `} - + * { border-left: 1px; } @@ -295,42 +421,77 @@ const StyledDayPicker = styled.div` } } - .DayPicker-Day--today, - .DayPicker-Day--today.DayPicker-Day--outside { + .rdp-today, + .rdp-today.rdp-outside { color: var(--colorsActionMajorYin090); background-color: var(--colorsActionMinor200); } - .DayPicker-Day--outside { + .rdp-outside { color: var(--colorsActionMajorYin055); - background-color: var(--colorsUtilityYang100); + background-color: transparent; + /* background-color: var(--colorsUtilityYang100); */ + } + + .rdp-today:not(.rdp-outside) { + font-weight: var(--fontWeights500); + border-radius: var(--borderRadius400); + color: inherit; } - .DayPicker-Day--disabled, - .DayPicker-Day--disabled:hover { + .rdp-disabled, + .rdp-disabled:hover { color: var(--colorsActionMajorYin030); background-color: var(--colorsUtilityYang100); cursor: default; - &.DayPicker-Day--today { + &.rdp-today { background-color: var(--colorsActionMinor200); } } - .DayPicker-Day--selected:not(.DayPicker-Day--disabled):not( - .DayPicker-Day--outside - ) { + .rdp-selected:not(.rdp-disabled):not(.rdp-outside) { background-color: var(--colorsActionMajor500); color: var(--colorsUtilityYang100); border-radius: var(--borderRadius400); } - .DayPicker-Day--selected.DayPicker-Day--disabled:not( - .DayPicker-Day--outside - ) { + .rdp-selected.rdp-disabled:not(.rdp-outside) { background-color: var(--colorsActionMajor500); color: var(--colorsUtilityYang100); } + + .rdp-selected { + &:focus-visible { + outline: none; + } + } + + .rdp-selected .rdp-day_button { + border: none; + &:focus-visible { + outline: none; + } + } + + .rdp-focused:not(.rdp-disabled):not(.rdp-outside) { + ${({ theme }) => css` + ${!theme.focusRedesignOptOut + ? addFocusStyling(true) + : /* istanbul ignore next */ oldFocusStyling} + `} + border-radius: var(--borderRadius400); + } + + .rdp-day.rdp-selected { + ${({ theme }) => css` + &:focus { + ${!theme.focusRedesignOptOut + ? addFocusStyling(true) + : /* istanbul ignore next */ oldFocusStyling} + } + `} + } `; StyledDayPicker.defaultProps = { diff --git a/src/components/date/__internal__/navbar/navbar.component.tsx b/src/components/date/__internal__/navbar/navbar.component.tsx index 7d1b1c98db..9a7561a928 100644 --- a/src/components/date/__internal__/navbar/navbar.component.tsx +++ b/src/components/date/__internal__/navbar/navbar.component.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { NavProps } from "react-day-picker"; + import StyledButton from "./button.style"; import StyledNavbar from "./navbar.style"; import Icon from "../../../icon"; @@ -15,7 +17,7 @@ export const Navbar = ({ onPreviousClick, onNextClick, className, -}: NavbarProps) => { +}: NavProps) => { const locale = useLocale(); const { previousMonthButton, nextMonthButton } = locale.date.ariaLabels; @@ -35,14 +37,16 @@ export const Navbar = ({ onPreviousClick?.()} + onClick={(e) => { + onPreviousClick?.(e); + }} onKeyDown={handleKeyDown} > onNextClick?.()} + onClick={(e) => onNextClick?.(e)} onKeyDown={handleKeyDown} > diff --git a/src/components/date/__internal__/navbar/navbar.style.ts b/src/components/date/__internal__/navbar/navbar.style.ts index fe756c029e..e6bed9d55c 100644 --- a/src/components/date/__internal__/navbar/navbar.style.ts +++ b/src/components/date/__internal__/navbar/navbar.style.ts @@ -1,7 +1,7 @@ import styled from "styled-components"; const StyledNavbar = styled.div` - &.DayPicker-NavBar { + &.rdp-nav { display: flex; justify-content: space-between; padding: 0; diff --git a/src/components/date/__internal__/utils.ts b/src/components/date/__internal__/utils.ts index e7afeb121d..16b2e9fa4e 100644 --- a/src/components/date/__internal__/utils.ts +++ b/src/components/date/__internal__/utils.ts @@ -1,4 +1,5 @@ -import { Modifier } from "react-day-picker"; +import { Matcher } from "react-day-picker"; + import { format, formatISO, isMatch, parse, parseISO } from "./date-fns-fp"; const DATE_STRING_LENGTH = 10; @@ -220,7 +221,7 @@ export function checkISOFormatAndLength(value: string) { export function getDisabledDays( minDate = "", maxDate = "", -): Modifier | Modifier[] { +): NonNullable | NonNullable | undefined { const days = []; if (!minDate && !maxDate) { diff --git a/src/components/date/__internal__/weekday/weekday.style.ts b/src/components/date/__internal__/weekday/weekday.style.ts index c65857ab2b..0eeda8f8d2 100644 --- a/src/components/date/__internal__/weekday/weekday.style.ts +++ b/src/components/date/__internal__/weekday/weekday.style.ts @@ -1,8 +1,8 @@ import styled from "styled-components"; -const StyledWeekday = styled.div` +const StyledWeekday = styled.th` &, - &.DayPicker-Weekday { + & abbr { border: none; height: var(--sizing500); min-width: var(--sizing500); diff --git a/src/components/date/__internal__/weekday/weekday.test.tsx b/src/components/date/__internal__/weekday/weekday.test.tsx index c6a546a294..e31d9bae13 100644 --- a/src/components/date/__internal__/weekday/weekday.test.tsx +++ b/src/components/date/__internal__/weekday/weekday.test.tsx @@ -3,8 +3,22 @@ import { screen, render } from "@testing-library/react"; import Weekday from "./weekday.component"; +const Component = (props: { + children: React.ReactNode; + className?: string; + title?: string; +}) => ( + + + + Foo + + +
+); + test("should render the passed `title` as the attribute on the `abbr` element", () => { - render(Foo); + render(Foo); const abbr = screen.getByTitle("title"); expect(abbr).toBeInTheDocument(); @@ -12,7 +26,7 @@ test("should render the passed `title` as the attribute on the `abbr` element", }); test("should render the passed `className` on the `div` element", () => { - render(Foo); + render(Foo); const weekday = screen.getByRole("columnheader", { name: "Foo" }); expect(weekday).toHaveClass("custom-class"); diff --git a/src/components/date/date.component.tsx b/src/components/date/date.component.tsx index 7205cfaaa3..de9f5a3fbb 100644 --- a/src/components/date/date.component.tsx +++ b/src/components/date/date.component.tsx @@ -6,6 +6,7 @@ import React, { useState, useCallback, } from "react"; +import { parse, isValid } from "date-fns"; import { additionalYears, @@ -125,7 +126,7 @@ export const DateInput = React.forwardRef( onClick, onFocus, onKeyDown, - pickerProps = {}, + pickerProps = { mode: "single" }, readOnly, size = "medium", tooltipPosition, @@ -155,11 +156,19 @@ export const DateInput = React.forwardRef( ); const { inputRefMap, setInputRefMap } = useContext(DateRangeContext); const [open, setOpen] = useState(false); - const [selectedDays, setSelectedDays] = useState( - checkISOFormatAndLength(value) + const [selectedDays, setSelectedDays] = useState(() => { + const parsedDate = parse(value, "P", new Date(), { + locale: dateFnsLocale(), + }); + const isValidDate = isValid(parsedDate); + if (!isValidDate) { + return undefined; + } + + return checkISOFormatAndLength(value) ? parseISODate(value) - : parseDate(format, value), - ); + : parseDate(format, value); + }); const isInitialValue = useRef(true); const pickerTabGuardId = useRef(guid()); @@ -220,7 +229,7 @@ export const DateInput = React.forwardRef( return function cleanup() { document.removeEventListener("mousedown", handleClick); }; - }, [open]); + }, [open, onPickerClose]); const handleChange = (ev: React.ChangeEvent) => { isInitialValue.current = false; diff --git a/src/components/date/date.mdx b/src/components/date/date.mdx index cb36dd38b8..3c7952c1b7 100644 --- a/src/components/date/date.mdx +++ b/src/components/date/date.mdx @@ -64,6 +64,12 @@ be used to set the input value like in the example below, although the component +### With disabled dates + +You can configure the available dates by passing a `minDate` and `maxDate` prop to the component. + + + ### With labelInline **Note:** The `labelInline` prop is not supported if the `validationRedesignOptIn` flag on the `CarbonProvider` is true. diff --git a/src/components/date/date.pw.tsx b/src/components/date/date.pw.tsx index df29c409af..5ed086e500 100644 --- a/src/components/date/date.pw.tsx +++ b/src/components/date/date.pw.tsx @@ -1,6 +1,7 @@ import React from "react"; import { expect, test } from "@playwright/experimental-ct-react17"; import dayjs from "dayjs"; +import advancedFormat from "dayjs/plugin/advancedFormat"; import { DateInputCustom, DateInputValidationNewDesign, @@ -33,9 +34,11 @@ import { import { HooksConfig } from "../../../playwright"; import { alertDialogPreview } from "../../../playwright/components/dialog"; +dayjs.extend(advancedFormat); + const testData = [CHARACTERS.DIACRITICS, CHARACTERS.SPECIALCHARACTERS]; -const DAY_PICKER_PREFIX = "DayPicker-Day--"; -const TODAY = dayjs().format("ddd D MMM YYYY"); +const DAY_PICKER_PREFIX = "rdp-"; +const TODAY = dayjs().format("dddd, MMMM Do, YYYY"); const DATE_INPUT = dayjs("2022-05-01").format("DD/MM/YYYY"); const TODAY_DATE_INPUT = dayjs().format("DD/MM/YYYY"); const NEXT_MONTH = dayjs("2022-05-01").add(1, "months").format("MMMM YYYY"); @@ -44,8 +47,8 @@ const PREVIOUS_MONTH = dayjs("2022-05-01") .subtract(1, "months") .format("MMMM YYYY"); const MIN_DATE = "04/04/2030"; -const DAY_BEFORE_MIN_DATE = "Wed 3 Apr 2030"; -const DAY_AFTER_MAX_DATE = "Fri 5 Apr 2030"; +const DAY_BEFORE_MIN_DATE = "Wednesday, April 3rd, 2030"; +const DAY_AFTER_MAX_DATE = "Friday, April 5th, 2030"; const DDMMYYY_DATE_TO_ENTER = "27,05,2022"; const MMDDYYYY_DATE_TO_ENTER = "05,27,2022"; const YYYYMMDD_DATE_TO_ENTER = "2022,05,27"; @@ -104,11 +107,10 @@ test.describe("Functionality tests", () => { const input = getDataElementByValue(page, "input"); await input.fill(MIN_DATE); - const dayPicker = page - .getByRole("row") - .locator(`div[aria-label="${DAY_BEFORE_MIN_DATE}"]`); - await expect(dayPicker).toHaveAttribute("aria-disabled", "true"); - await expect(dayPicker).toHaveAttribute("aria-selected", "false"); + const dayPicker = page.locator( + `button[aria-label="${DAY_BEFORE_MIN_DATE}"]`, + ); + await expect(dayPicker).toHaveAttribute("disabled", ""); }); test(`should check the maxDate prop`, async ({ mount, page }) => { @@ -117,11 +119,10 @@ test.describe("Functionality tests", () => { const input = getDataElementByValue(page, "input"); await input.fill(MIN_DATE); - const dayPicker = page - .getByRole("row") - .locator(`div[aria-label="${DAY_AFTER_MAX_DATE}"]`); - await expect(dayPicker).toHaveAttribute("aria-disabled", "true"); - await expect(dayPicker).toHaveAttribute("aria-selected", "false"); + const dayPicker = page.locator( + `button[aria-label="${DAY_AFTER_MAX_DATE}"]`, + ); + await expect(dayPicker).toHaveAttribute("disabled", ""); }); test(`should check the date is set to today's day`, async ({ @@ -130,15 +131,14 @@ test.describe("Functionality tests", () => { }) => { await mount(); - const dayClass = `DayPicker-Day ${DAY_PICKER_PREFIX}selected ${DAY_PICKER_PREFIX}today`; + const dayClass = `rdp-day rdp-today`; const input = getDataElementByValue(page, "input"); await input.fill(TODAY_DATE_INPUT); - const dayPicker = page - .getByRole("row") - .locator(`div[aria-label="${TODAY}"]`); - await expect(dayPicker).toHaveAttribute("aria-label", TODAY); - await expect(dayPicker).toHaveClass(dayClass); + const dayPicker = page.locator(`td[data-today="true"]`).getByRole("button"); + await expect(dayPicker).toHaveAttribute("aria-label", `Today, ${TODAY}`); + const inputParent = page.locator(`td[data-today="true"]`); + await expect(inputParent).toHaveClass(dayClass); }); test(`should open dayPicker after click on input`, async ({ @@ -202,7 +202,7 @@ test.describe("Functionality tests", () => { const inputParent = getDataElementByValue(page, "input").locator(".."); await inputParent.click(); - const wrapperParent = dayPickerWrapper(page).locator("..").locator(".."); + const wrapperParent = dayPickerWrapper(page).locator(".."); await expect(wrapperParent).toHaveAttribute( "data-floating-placement", `${position}-start`, @@ -293,7 +293,7 @@ test.describe("Functionality tests", () => { ); await expect(arrowRight).toBeFocused(); await page.keyboard.press("Tab"); - const dayPicker = page.locator(`.${DAY_PICKER_PREFIX}selected`); + const dayPicker = page.locator(`.rdp-selected`).locator("button"); await expect(dayPicker).toBeFocused(); }); @@ -316,7 +316,9 @@ test.describe("Functionality tests", () => { ); await expect(arrowRight).toBeFocused(); await page.keyboard.press("Tab"); - const dayPicker = page.locator(`.${DAY_PICKER_PREFIX}selected`); + const dayPicker = page + .locator(`.${DAY_PICKER_PREFIX}selected`) + .locator("button"); await expect(dayPicker).toBeFocused(); await page.keyboard.press("Tab"); const wrapper = dayPickerWrapper(page); @@ -336,7 +338,9 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); await page.keyboard.press("Tab"); - const dayPicker = page.locator(`.${DAY_PICKER_PREFIX}today`); + const dayPicker = page + .locator(`.${DAY_PICKER_PREFIX}today`) + .locator("button"); await expect(dayPicker).toBeFocused(); await page.keyboard.press("Tab"); const wrapper = dayPickerWrapper(page); @@ -347,7 +351,7 @@ test.describe("Functionality tests", () => { mount, page, }) => { - await mount(); + await mount(); await page.focus("body"); await page.keyboard.press("Tab"); @@ -356,62 +360,54 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); await page.keyboard.press(arrowKeys[3]); const focusedElement1 = page - .getByRole("row") - .nth(2) - .locator("div") - .filter({ hasText: "8" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "21" }); await expect(focusedElement1).toBeFocused(); await page.keyboard.press(arrowKeys[3]); const focusedElement2 = page - .getByRole("row") - .nth(3) - .locator("div") - .filter({ hasText: "15" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "28" }); await expect(focusedElement2).toBeFocused(); await page.keyboard.press(arrowKeys[1]); const focusedElement3 = page - .getByRole("row") - .nth(3) - .locator("div") - .filter({ hasText: "14" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "27" }); await expect(focusedElement3).toBeFocused(); await page.keyboard.press(arrowKeys[1]); const focusedElement4 = page - .getByRole("row") - .nth(3) - .locator("div") - .filter({ hasText: "13" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "26" }); await expect(focusedElement4).toBeFocused(); await page.keyboard.press(arrowKeys[0]); const focusedElement5 = page - .getByRole("row") - .nth(3) - .locator("div") - .filter({ hasText: "14" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "27" }); await expect(focusedElement5).toBeFocused(); await page.keyboard.press(arrowKeys[0]); const focusedElement6 = page - .getByRole("row") - .nth(3) - .locator("div") - .filter({ hasText: "15" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "28" }); await expect(focusedElement6).toBeFocused(); await page.keyboard.press(arrowKeys[2]); const focusedElement7 = page - .getByRole("row") - .nth(2) - .locator("div") - .filter({ hasText: "8" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "21" }); await expect(focusedElement7).toBeFocused(); await page.keyboard.press(arrowKeys[2]); const focusedElement8 = page - .getByRole("row") - .nth(1) - .locator("div") - .filter({ hasText: "1" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "14" }); await expect(focusedElement8).toBeFocused(); }); @@ -419,7 +415,7 @@ test.describe("Functionality tests", () => { mount, page, }) => { - await mount(); + await mount(); await page.focus("body"); await page.keyboard.press("Tab"); @@ -428,10 +424,9 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); await page.keyboard.press(arrowKeys[1]); const focusedElement = page - .getByRole("row") - .nth(5) - .locator("div") - .filter({ hasText: "30" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "13" }); await expect(focusedElement).toBeFocused(); const pickerHeading = dayPickerHeading(page); await expect(pickerHeading).toHaveText(PREVIOUS_MONTH); @@ -460,16 +455,14 @@ test.describe("Functionality tests", () => { await page.keyboard.press(arrowKeys[2]); if (day === "1") { const focusedElement = page - .getByRole("row") - .nth(4) - .locator("div") + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") .filter({ hasText: result }); await expect(focusedElement).toBeFocused(); } else { const focusedElement = page - .getByRole("row") - .nth(5) - .locator("div") + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") .filter({ hasText: result }); await expect(focusedElement).toBeFocused(); } @@ -501,16 +494,14 @@ test.describe("Functionality tests", () => { await page.keyboard.press(arrowKeys[3]); if (day === "30" || day === "31") { const focusedElement = page - .getByRole("row") - .nth(2) - .locator("div") + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") .filter({ hasText: result }); await expect(focusedElement).toBeFocused(); } else { const focusedElement = page - .getByRole("row") - .nth(1) - .locator("div") + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") .filter({ hasText: result }) .filter({ hasNotText: "30" }) .filter({ hasNotText: "31" }); @@ -526,7 +517,7 @@ test.describe("Functionality tests", () => { mount, page, }) => { - await mount(); + await mount(); await page.focus("body"); await page.keyboard.press("Tab"); @@ -535,14 +526,13 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); await page.keyboard.press(arrowKeys[1]); const focusedElement = page - .getByRole("row") - .nth(5) - .locator("div") - .filter({ hasText: "30" }); + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") + .filter({ hasText: "13" }); await expect(focusedElement).toBeFocused(); await page.keyboard.press(key); await expect(getDataElementByValue(page, "input")).toHaveValue( - "30/04/2022", + "13/04/2022", ); }); }); @@ -606,9 +596,8 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); await page.keyboard.press(arrowKeys[0]); const focusedElement = page - .getByRole("row") - .nth(1) - .locator("div") + .locator(`.${DAY_PICKER_PREFIX}focused`) + .locator("button") .filter({ hasText: "1" }) .filter({ hasNotText: "31" }); await expect(focusedElement).toBeFocused(); @@ -748,21 +737,23 @@ test.describe("Functionality tests", () => { }); test(`should check the pickerProps prop`, async ({ mount, page }) => { - await mount(); + await mount( + , + ); const input = getDataElementByValue(page, "input"); await input.click(); - const months = page.locator("div[class=DayPicker-Month]"); + const months = page.locator("div[class=rdp-month]"); await expect(months).toHaveCount(2); const pickerHeading1 = page - .locator(".DayPicker-Caption") - .locator("div") + .locator(".rdp-month_caption") + .locator("span") .nth(0); await expect(pickerHeading1).toBeVisible(); await expect(pickerHeading1).toHaveText(ACTUAL_MONTH); const pickerHeading2 = page - .locator(".DayPicker-Caption") - .locator("div") + .locator(".rdp-month_caption") + .locator("span") .nth(1); await expect(pickerHeading2).toBeVisible(); await expect(pickerHeading2).toHaveText(NEXT_MONTH); @@ -836,9 +827,11 @@ test.describe("Functionality tests", () => { const input = getDataElementByValue(page, "input"); await input.click(); await expect(input).toHaveCSS("border-radius", "4px"); - const dayPicker1 = page.getByLabel("Sun 1 May 2022"); + const dayPicker1 = page + .getByLabel("Sunday, May 1st, 2022, selected") + .locator(".."); await expect(dayPicker1).toHaveCSS("border-radius", "32px"); - const dayPicker2 = page.getByLabel("Mon 2 May 2022"); + const dayPicker2 = page.getByLabel("Monday, May 2nd, 2022").locator(".."); await expect(dayPicker2).toHaveCSS("border-radius", "32px"); const dayPickerNavButton1 = page.getByLabel("Previous month"); await expect(dayPickerNavButton1).toHaveCSS("border-radius", "4px"); @@ -858,18 +851,33 @@ test.describe("Functionality tests", () => { await page.keyboard.press("Tab"); const inputParent = getDataElementByValue(page, "input").locator(".."); await checkGoldenOutline(inputParent); - const dayPicker1 = page.getByLabel("Sun 1 May 2022"); - await dayPicker1.focus(); - await checkGoldenOutline(dayPicker1); - const dayPicker2 = page.getByLabel("Mon 2 May 2022"); - await dayPicker2.focus(); - await checkGoldenOutline(dayPicker2); + await page.keyboard.press("Tab"); const dayPickerNavButton1 = page.getByLabel("Previous month"); await dayPickerNavButton1.focus(); - await checkGoldenOutline(dayPickerNavButton1); + await expect(dayPickerNavButton1).toHaveCSS( + "outline", + "rgb(255, 188, 25) solid 3px", + ); + await page.keyboard.press("Tab"); const dayPickerNavButton2 = page.getByLabel("Next month"); await dayPickerNavButton2.focus(); - await checkGoldenOutline(dayPickerNavButton2); + await expect(dayPickerNavButton2).toHaveCSS( + "outline", + "rgb(255, 188, 25) solid 3px", + ); + await page.keyboard.press("Tab"); + const dayPicker1 = page + .getByLabel("Sunday, May 1st, 2022, selected") + .locator(".."); + await dayPicker1.focus(); + await expect(dayPicker1).toHaveCSS( + "outline", + "rgb(255, 188, 25) solid 3px", + ); + + await page.keyboard.press("ArrowRight"); + const dayPicker2 = page.getByLabel("Monday, May 2nd, 2022").locator(".."); + await dayPicker2.focus(); }); test(`should have the expected styling when opt out flag is false`, async ({ @@ -880,8 +888,6 @@ test.describe("Functionality tests", () => { await page.focus("body"); await page.keyboard.press("Tab"); - const wrapperParent = dayPickerWrapper(page).locator("..").locator(".."); - await expect(wrapperParent).toHaveCSS("margin-top", "4px"); const inputParent = getDataElementByValue(page, "input").locator(".."); await expect(inputParent).toHaveCSS( "box-shadow", @@ -891,20 +897,8 @@ test.describe("Functionality tests", () => { "outline", "rgba(0, 0, 0, 0) solid 3px", ); - const dayPicker1 = page.getByLabel("Sun 1 May 2022"); - await dayPicker1.focus(); - await expect(dayPicker1).toHaveCSS( - "box-shadow", - "rgba(0, 0, 0, 0.9) 0px 0px 0px 3px inset, rgb(255, 188, 25) 0px 0px 0px 6px inset", - ); - await expect(dayPicker1).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); - const dayPicker2 = page.getByLabel("Mon 2 May 2022"); - await dayPicker2.focus(); - await expect(dayPicker2).toHaveCSS( - "box-shadow", - "rgba(0, 0, 0, 0.9) 0px 0px 0px 3px inset, rgb(255, 188, 25) 0px 0px 0px 6px inset", - ); - await expect(dayPicker2).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); + + await page.keyboard.press("Tab"); const dayPickerNavButton1 = page.getByLabel("Previous month"); await dayPickerNavButton1.focus(); await expect(dayPickerNavButton1).toHaveCSS( @@ -915,6 +909,8 @@ test.describe("Functionality tests", () => { "outline", "rgba(0, 0, 0, 0) solid 3px", ); + + await page.keyboard.press("Tab"); const dayPickerNavButton2 = page.getByLabel("Next month"); await dayPickerNavButton2.focus(); await expect(dayPickerNavButton2).toHaveCSS( @@ -925,6 +921,24 @@ test.describe("Functionality tests", () => { "outline", "rgba(0, 0, 0, 0) solid 3px", ); + + await page.keyboard.press("Tab"); + const dayPicker1 = page.getByLabel("Sunday, May 1st, 2022").locator(".."); + await dayPicker1.focus(); + await expect(dayPicker1).toHaveCSS( + "box-shadow", + "rgba(0, 0, 0, 0.9) 0px 0px 0px 3px inset, rgb(255, 188, 25) 0px 0px 0px 6px inset", + ); + await expect(dayPicker1).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); + + await page.keyboard.press("ArrowRight"); + const dayPicker2 = page.getByLabel("Monday, May 2nd, 2022").locator(".."); + await dayPicker2.focus(); + await expect(dayPicker2).toHaveCSS( + "box-shadow", + "rgba(0, 0, 0, 0.9) 0px 0px 0px 3px inset, rgb(255, 188, 25) 0px 0px 0px 6px inset", + ); + await expect(dayPicker2).toHaveCSS("outline", "rgba(0, 0, 0, 0) solid 3px"); }); (["top", "bottom", "left", "right"] as const).forEach((position) => { diff --git a/src/components/date/date.stories.tsx b/src/components/date/date.stories.tsx index 9df69d32cc..e0e5bf6217 100644 --- a/src/components/date/date.stories.tsx +++ b/src/components/date/date.stories.tsx @@ -124,6 +124,26 @@ export const Empty: Story = () => { }; Empty.storyName = "Empty"; +export const DisabledDates: Story = () => { + const [state, setState] = useState("04/04/2019"); + const setValue = (ev: DateChangeEvent) => { + setState(ev.target.value.formattedValue); + }; + return ( + console.log("blur")} + /> + ); +}; +DisabledDates.storyName = "Disabled Dates"; +DisabledDates.parameters = { chromatic: { disableSnapshot: true } }; + export const WithLabelInline: Story = () => { const [state, setState] = useState("01/10/2016"); const setValue = (ev: DateChangeEvent) => { diff --git a/src/components/date/date.test.tsx b/src/components/date/date.test.tsx index 2ee4aa1db3..dbb9592072 100644 --- a/src/components/date/date.test.tsx +++ b/src/components/date/date.test.tsx @@ -68,7 +68,7 @@ const MockComponent = ({ ); }; -// temporatrily running timers on every spec as we have issues +// temporarily running timers on every spec as we have issues // around how slow the tests that open the calendar are. // FE-6724 raised to investigate and implement a better solution beforeAll(() => { @@ -479,23 +479,21 @@ test("should not close the picker or call the `onChange` and `onBlur` callbacks test("should not close the picker or call the `onChange` and `onBlur` callbacks when the user clicks on a disabled day", async () => { const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); const onChange = jest.fn(); - const onBlur = jest.fn(); render( , ); const input = screen.getByRole("textbox"); await user.click(input); jest.advanceTimersByTime(10); - await user.click(screen.getByRole("gridcell", { name: "Wed 3 Apr 2019" })); + await user.click(screen.getByLabelText("Wednesday, April 3rd, 2019")); expect(screen.queryByRole("grid")).toBeVisible(); expect(onChange).not.toHaveBeenCalled(); - expect(onBlur).not.toHaveBeenCalled(); }); test("should close the open picker when a user presses the 'Escape' key", async () => { @@ -609,7 +607,7 @@ test("should focus the next button and then the selected day element when the us expect(screen.getByRole("button", { name: "Next month" })).toHaveFocus(); await user.tab(); expect( - screen.getByRole("gridcell", { name: "Thu 4 Apr 2019" }), + screen.getByLabelText("Thursday, April 4th, 2019", { exact: false }), ).toHaveFocus(); await user.tab(); expect(screen.queryByRole("grid")).not.toBeInTheDocument(); @@ -621,10 +619,12 @@ test("should close the picker, update the value and refocus the input element wh const input = screen.getByRole("textbox"); await user.click(input); jest.advanceTimersByTime(10); - await user.click(screen.getByRole("gridcell", { name: "Thu 11 Apr 2019" })); + await user.click(screen.getByLabelText("Thursday, April 11th, 2019")); expect(screen.queryByRole("grid")).not.toBeInTheDocument(); - expect(input).toHaveFocus(); + await waitFor(() => { + expect(input).toHaveFocus(); + }); expect(input).toHaveValue("11/04/2019"); }); @@ -1668,6 +1668,6 @@ test("should select the correct date when the locale is overridden and a date is await user.type(input, "05/04"); jest.advanceTimersByTime(10); - const grid = screen.getByRole("grid").childNodes[0].textContent; + const grid = screen.getByRole("status").textContent; expect(grid).toEqual("April 2019"); }); diff --git a/src/components/tabs/__internal__/tab-title/tab-title.style.ts b/src/components/tabs/__internal__/tab-title/tab-title.style.ts index 6036afc074..0f6c73fc3b 100644 --- a/src/components/tabs/__internal__/tab-title/tab-title.style.ts +++ b/src/components/tabs/__internal__/tab-title/tab-title.style.ts @@ -2,9 +2,10 @@ import styled, { css } from "styled-components"; import StyledIcon from "../../../icon/icon.style"; import StyledValidationIcon from "../../../../__internal__/validations/validation-icon.style"; -import { TabTitleProps } from "."; import addFocusStyling from "../../../../style/utils/add-focus-styling"; +import { TabTitleProps } from "."; + interface StyledTitleContentProps extends Pick< TabTitleProps, @@ -151,7 +152,7 @@ const StyledTitleContent = styled.span` padding: 10px 16px; ${borders && `padding-bottom: 9px;`} - `} + `} ${(warning || info) && css` @@ -180,7 +181,7 @@ const StyledTitleContent = styled.span` border-right-color: transparent; padding-right: ${size === "large" ? "26px" : "18px"}; `} - + &:hover { outline: 1px solid; outline-offset: -1px; @@ -230,7 +231,7 @@ const StyledTitleContent = styled.span` border-right-color: transparent; padding-right: ${size === "large" ? "26px" : "18px"}; `} - + &:hover { outline: 2px solid var(--colorsSemanticNegative500); outline-offset: -2px; @@ -488,7 +489,7 @@ const tabTitleStyles = css< border-right: none; } `} - + background-color: var(--colorsActionMajorYang100); &:hover { @@ -546,7 +547,6 @@ const StyledLayoutWrapper = styled.div` hasCustomLayout, titlePosition = "before", hasCustomSibling, - position, validationRedesignOptIn, }) => css` ${hasCustomLayout &&