@simplybusiness/mobius-datepicker 4.0.0-beta.7 → 4.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/cjs/components/NewDatePicker/DatePickerIcon.d.ts +1 -0
  2. package/dist/cjs/components/NewDatePicker/DatePickerIcon.js +7 -0
  3. package/dist/cjs/components/NewDatePicker/DatePickerIcon.js.map +1 -0
  4. package/dist/cjs/components/NewDatePicker/DatePickerModal.d.ts +7 -0
  5. package/dist/cjs/components/NewDatePicker/DatePickerModal.js +35 -0
  6. package/dist/cjs/components/NewDatePicker/DatePickerModal.js.map +1 -0
  7. package/dist/cjs/components/NewDatePicker/NewDatePicker.d.ts +7 -0
  8. package/dist/cjs/components/NewDatePicker/NewDatePicker.js +73 -0
  9. package/dist/cjs/components/NewDatePicker/NewDatePicker.js.map +1 -0
  10. package/dist/cjs/components/NewDatePicker/NewDatePicker.stories.d.ts +27 -0
  11. package/dist/cjs/components/NewDatePicker/NewDatePicker.stories.js +32 -0
  12. package/dist/cjs/components/NewDatePicker/NewDatePicker.stories.js.map +1 -0
  13. package/dist/cjs/components/NewDatePicker/NewDatePicker.test.d.ts +1 -0
  14. package/dist/cjs/components/NewDatePicker/NewDatePicker.test.js +80 -0
  15. package/dist/cjs/components/NewDatePicker/NewDatePicker.test.js.map +1 -0
  16. package/dist/cjs/components/NewDatePicker/constants.d.ts +2 -0
  17. package/dist/cjs/components/NewDatePicker/constants.js +7 -0
  18. package/dist/cjs/components/NewDatePicker/constants.js.map +1 -0
  19. package/dist/cjs/components/NewDatePicker/dateObjToString.d.ts +1 -0
  20. package/dist/cjs/components/NewDatePicker/dateObjToString.js +7 -0
  21. package/dist/cjs/components/NewDatePicker/dateObjToString.js.map +1 -0
  22. package/dist/cjs/components/NewDatePicker/dateObjToString.test.d.ts +1 -0
  23. package/dist/cjs/components/NewDatePicker/dateObjToString.test.js +15 -0
  24. package/dist/cjs/components/NewDatePicker/dateObjToString.test.js.map +1 -0
  25. package/dist/cjs/components/NewDatePicker/index.d.ts +1 -0
  26. package/dist/cjs/components/NewDatePicker/index.js +18 -0
  27. package/dist/cjs/components/NewDatePicker/index.js.map +1 -0
  28. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.d.ts +2 -0
  29. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.js +14 -0
  30. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.js.map +1 -0
  31. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.test.d.ts +1 -0
  32. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.test.js +23 -0
  33. package/dist/cjs/components/NewDatePicker/weekdayAsOneLetter.test.js.map +1 -0
  34. package/dist/cjs/hooks/useFocusTrap.d.ts +5 -0
  35. package/dist/cjs/hooks/useFocusTrap.js +41 -0
  36. package/dist/cjs/hooks/useFocusTrap.js.map +1 -0
  37. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  38. package/dist/cjs/utils/excludeControls.d.ts +1 -0
  39. package/dist/cjs/utils/excludeControls.js +29 -0
  40. package/dist/cjs/utils/excludeControls.js.map +1 -0
  41. package/dist/cjs/utils/excludeControls.test.d.ts +1 -0
  42. package/dist/cjs/utils/excludeControls.test.js +25 -0
  43. package/dist/cjs/utils/excludeControls.test.js.map +1 -0
  44. package/dist/cjs/utils/isTouchDevice.d.ts +1 -0
  45. package/dist/cjs/utils/isTouchDevice.js +11 -0
  46. package/dist/cjs/utils/isTouchDevice.js.map +1 -0
  47. package/dist/esm/components/NewDatePicker/DatePickerIcon.js +3 -0
  48. package/dist/esm/components/NewDatePicker/DatePickerIcon.js.map +1 -0
  49. package/dist/esm/components/NewDatePicker/DatePickerModal.js +28 -0
  50. package/dist/esm/components/NewDatePicker/DatePickerModal.js.map +1 -0
  51. package/dist/esm/components/NewDatePicker/NewDatePicker.js +66 -0
  52. package/dist/esm/components/NewDatePicker/NewDatePicker.js.map +1 -0
  53. package/dist/esm/components/NewDatePicker/constants.js +4 -0
  54. package/dist/esm/components/NewDatePicker/constants.js.map +1 -0
  55. package/dist/esm/components/NewDatePicker/dateObjToString.js +3 -0
  56. package/dist/esm/components/NewDatePicker/dateObjToString.js.map +1 -0
  57. package/dist/esm/components/NewDatePicker/dateObjToString.test.js +13 -0
  58. package/dist/esm/components/NewDatePicker/dateObjToString.test.js.map +1 -0
  59. package/dist/esm/components/NewDatePicker/index.js +2 -0
  60. package/dist/esm/components/NewDatePicker/index.js.map +1 -0
  61. package/dist/esm/components/NewDatePicker/weekdayAsOneLetter.js +10 -0
  62. package/dist/esm/components/NewDatePicker/weekdayAsOneLetter.js.map +1 -0
  63. package/dist/esm/components/NewDatePicker/weekdayAsOneLetter.test.js +21 -0
  64. package/dist/esm/components/NewDatePicker/weekdayAsOneLetter.test.js.map +1 -0
  65. package/dist/esm/hooks/useFocusTrap.js +38 -0
  66. package/dist/esm/hooks/useFocusTrap.js.map +1 -0
  67. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  68. package/dist/esm/utils/excludeControls.js +25 -0
  69. package/dist/esm/utils/excludeControls.js.map +1 -0
  70. package/dist/esm/utils/excludeControls.test.js +23 -0
  71. package/dist/esm/utils/excludeControls.test.js.map +1 -0
  72. package/dist/esm/utils/isTouchDevice.js +7 -0
  73. package/dist/esm/utils/isTouchDevice.js.map +1 -0
  74. package/package.json +6 -4
  75. package/src/components/NewDatePicker/DatePickerIcon.tsx +1 -0
  76. package/src/components/NewDatePicker/DatePickerModal.tsx +69 -0
  77. package/src/components/NewDatePicker/NewDatePicker.mdx +521 -0
  78. package/src/components/NewDatePicker/NewDatePicker.stories.tsx +46 -0
  79. package/src/components/NewDatePicker/NewDatePicker.test.tsx +121 -0
  80. package/src/components/NewDatePicker/NewDatePicker.tsx +119 -0
  81. package/src/components/NewDatePicker/constants.ts +3 -0
  82. package/src/components/NewDatePicker/dateObjToString.test.ts +17 -0
  83. package/src/components/NewDatePicker/dateObjToString.ts +3 -0
  84. package/src/components/NewDatePicker/index.tsx +1 -0
  85. package/src/components/NewDatePicker/weekdayAsOneLetter.test.ts +28 -0
  86. package/src/components/NewDatePicker/weekdayAsOneLetter.ts +11 -0
  87. package/src/hooks/useFocusTrap.tsx +55 -0
  88. package/src/utils/excludeControls.test.ts +26 -0
  89. package/src/utils/excludeControls.ts +28 -0
  90. package/src/utils/isTouchDevice.ts +7 -0
@@ -0,0 +1,121 @@
1
+ import { render, screen, fireEvent } from "@testing-library/react";
2
+ import { NewDatePicker } from ".";
3
+
4
+ const mockMatchMedia = (matches: boolean) => {
5
+ Object.defineProperty(window, "matchMedia", {
6
+ writable: true,
7
+ configurable: true,
8
+ value: jest.fn().mockImplementation(query => ({
9
+ matches,
10
+ media: query,
11
+ onchange: null,
12
+ addListener: jest.fn(),
13
+ removeListener: jest.fn(),
14
+ addEventListener: jest.fn(),
15
+ removeEventListener: jest.fn(),
16
+ dispatchEvent: jest.fn(),
17
+ })),
18
+ });
19
+ };
20
+
21
+ describe("NewDatePicker", () => {
22
+ describe("given it is a touch device", () => {
23
+ beforeEach(() => {
24
+ mockMatchMedia(true);
25
+ });
26
+
27
+ it("should render without errors", () => {
28
+ const exampleDate = "2023-11-13";
29
+ const labelText = "Start date";
30
+ const testId = "date-picker";
31
+
32
+ render(
33
+ <NewDatePicker
34
+ defaultValue={exampleDate}
35
+ label={labelText}
36
+ data-testid={testId}
37
+ />,
38
+ );
39
+
40
+ const inputField = screen.getByTestId(testId);
41
+ const label = screen.getByLabelText(labelText);
42
+
43
+ expect(inputField).toHaveValue(exampleDate);
44
+ expect(label).toBeInTheDocument();
45
+ });
46
+
47
+ describe("when value changes", () => {
48
+ it("triggers onChange", () => {
49
+ const labelText = "Start date";
50
+ const testId = "date-picker";
51
+ const onChange = jest.fn();
52
+
53
+ render(
54
+ <NewDatePicker
55
+ label={labelText}
56
+ data-testid={testId}
57
+ onChange={onChange}
58
+ />,
59
+ );
60
+
61
+ const inputField = screen.getByTestId(testId);
62
+ const newDate = "2015-06-01";
63
+
64
+ fireEvent.change(inputField, { target: { value: newDate } });
65
+
66
+ expect(inputField).toHaveValue(newDate);
67
+ expect(onChange).toHaveBeenCalledWith(newDate);
68
+ });
69
+ });
70
+ });
71
+
72
+ describe("given it is a non-touch device", () => {
73
+ beforeEach(() => {
74
+ mockMatchMedia(false);
75
+ });
76
+
77
+ it("should render without errors", () => {
78
+ const exampleDate = "2023-11-13";
79
+ const labelText = "Start date";
80
+ const testId = "date-picker";
81
+
82
+ render(
83
+ <NewDatePicker
84
+ defaultValue={exampleDate}
85
+ label={labelText}
86
+ data-testid={testId}
87
+ />,
88
+ );
89
+
90
+ const inputField = screen.getByTestId(testId);
91
+ const label = screen.getByLabelText(labelText);
92
+
93
+ expect(inputField).toHaveValue(exampleDate);
94
+ expect(label).toBeInTheDocument();
95
+ });
96
+
97
+ describe("when value changes", () => {
98
+ it("triggers onChange", () => {
99
+ const labelText = "Start date";
100
+ const testId = "date-picker";
101
+ const onChange = jest.fn();
102
+
103
+ render(
104
+ <NewDatePicker
105
+ label={labelText}
106
+ data-testid={testId}
107
+ onChange={onChange}
108
+ />,
109
+ );
110
+
111
+ const inputField = screen.getByTestId(testId);
112
+ const newDate = "2015-06-01";
113
+
114
+ fireEvent.change(inputField, { target: { value: newDate } });
115
+
116
+ expect(inputField).toHaveValue(newDate);
117
+ expect(onChange).toHaveBeenCalledWith(newDate);
118
+ });
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import {
4
+ Button,
5
+ TextField,
6
+ TextFieldElementType,
7
+ TextFieldProps,
8
+ VisuallyHidden,
9
+ } from "@simplybusiness/mobius";
10
+ import classNames from "classnames/dedupe";
11
+ import { ChangeEvent, useEffect, useRef, useState } from "react";
12
+ import { isTouchDevice } from "../../utils/isTouchDevice";
13
+ import { DatePickerModal } from "./DatePickerModal";
14
+
15
+ export interface NewDatePickerProps
16
+ extends Omit<TextFieldProps, "defaultValue" | "onChange"> {
17
+ onChange?: (date: string) => void;
18
+ defaultValue?: string;
19
+ id?: string;
20
+ }
21
+
22
+ export const NewDatePicker = (props: NewDatePickerProps) => {
23
+ const {
24
+ onChange,
25
+ defaultValue = "",
26
+ isDisabled,
27
+ validationState,
28
+ ...otherProps
29
+ } = props;
30
+ const ref = useRef<TextFieldElementType | null>(null);
31
+ const [top, setTop] = useState<number>(0);
32
+ const [isOpen, setIsOpen] = useState<boolean>(false);
33
+ const [textFieldVal, setTextFieldVal] = useState<string>(defaultValue); // yyyy-MM-dd
34
+ const touchDevice = isTouchDevice();
35
+
36
+ const containerClasses = classNames("mobius/DatePickerContainer", {
37
+ "--is-disabled": isDisabled,
38
+ "--is-valid": validationState === "valid",
39
+ "--is-invalid": validationState === "invalid",
40
+ });
41
+
42
+ const popoverToggleClasses = classNames("mobius/DateFieldButton", {
43
+ "--is-valid": validationState === "valid",
44
+ "--is-invalid": validationState === "invalid",
45
+ });
46
+
47
+ const setPopoverPosition = () => {
48
+ if (!ref.current || isOpen) return;
49
+
50
+ const refHeight = ref.current.getBoundingClientRect().height;
51
+
52
+ setTop(refHeight);
53
+ };
54
+
55
+ const togglePopoverVisibility = () => {
56
+ setIsOpen(!isOpen);
57
+ setPopoverPosition();
58
+ };
59
+
60
+ const handleTextFieldChange = (event: ChangeEvent<TextFieldElementType>) => {
61
+ setTextFieldVal(event.target.value);
62
+ };
63
+
64
+ const onDateSelected = (selectedDate: string) => {
65
+ setTextFieldVal(selectedDate);
66
+ setIsOpen(false);
67
+ };
68
+
69
+ // When user manually types in date,
70
+ // select date when popover becomes visible
71
+ useEffect(() => {
72
+ if (!textFieldVal) return;
73
+ if (onChange) {
74
+ onChange(textFieldVal);
75
+ }
76
+ }, [textFieldVal, onChange]);
77
+
78
+ if (touchDevice) {
79
+ return (
80
+ <TextField
81
+ type="date"
82
+ onChange={handleTextFieldChange}
83
+ value={textFieldVal}
84
+ isDisabled={isDisabled}
85
+ validationState={validationState}
86
+ {...otherProps}
87
+ />
88
+ );
89
+ }
90
+
91
+ return (
92
+ <div className={containerClasses} ref={ref}>
93
+ <TextField
94
+ type="date"
95
+ className="mobius/DatePicker"
96
+ onChange={handleTextFieldChange}
97
+ value={textFieldVal}
98
+ isDisabled={isDisabled}
99
+ validationState={validationState}
100
+ {...otherProps}
101
+ />
102
+ <Button
103
+ className={popoverToggleClasses}
104
+ onClick={togglePopoverVisibility}
105
+ isDisabled={isDisabled}
106
+ >
107
+ <VisuallyHidden>Pick date</VisuallyHidden>
108
+ </Button>
109
+ {isOpen && (
110
+ <DatePickerModal
111
+ date={textFieldVal}
112
+ isOpen={isOpen}
113
+ top={top}
114
+ onSelected={onDateSelected}
115
+ />
116
+ )}
117
+ </div>
118
+ );
119
+ };
@@ -0,0 +1,3 @@
1
+ export const MONDAY_AS_NUMBER = 1;
2
+
3
+ export const DEFAULT_LOCALE = window?.navigator?.language || "en-GB";
@@ -0,0 +1,17 @@
1
+ import { dateObjToString } from "./dateObjToString";
2
+
3
+ describe("dateObjToString", () => {
4
+ describe("given a Date object", () => {
5
+ it("returns a formatted string as yyyy-mm-dd", () => {
6
+ const exampleDate = "2023-11-13";
7
+
8
+ const input = new Date(exampleDate);
9
+
10
+ const actual = dateObjToString(input);
11
+
12
+ const expected = exampleDate;
13
+
14
+ expect(actual).toEqual(expected);
15
+ });
16
+ });
17
+ });
@@ -0,0 +1,3 @@
1
+ import { format } from "date-fns";
2
+
3
+ export const dateObjToString = (date: Date) => format(date, "yyyy-MM-dd");
@@ -0,0 +1 @@
1
+ export * from "./NewDatePicker";
@@ -0,0 +1,28 @@
1
+ import { weekdayAsOneLetter } from "./weekdayAsOneLetter";
2
+
3
+ describe("weekdayAsOneLetter", () => {
4
+ describe("given a Date object and a locale", () => {
5
+ it("returns weekday as a single letter", () => {
6
+ const randomMonday = new Date("2023-11-13");
7
+ const locale = { locale: { code: "en-GB" } };
8
+
9
+ const actual = weekdayAsOneLetter(randomMonday, locale);
10
+
11
+ const expected = "M";
12
+
13
+ expect(actual).toEqual(expected);
14
+ });
15
+ });
16
+
17
+ describe("given a Date object and no locale", () => {
18
+ it("returns weekday as a single letter, using en-GB as default locale", () => {
19
+ const randomMonday = new Date("2023-11-13");
20
+
21
+ const actual = weekdayAsOneLetter(randomMonday);
22
+
23
+ const expected = "M";
24
+
25
+ expect(actual).toEqual(expected);
26
+ });
27
+ });
28
+ });
@@ -0,0 +1,11 @@
1
+ import { DateFormatter } from "react-day-picker";
2
+ import { DEFAULT_LOCALE } from "./constants";
3
+
4
+ export const weekdayAsOneLetter: DateFormatter = (weekday, options) => {
5
+ const locale = options?.locale?.code || DEFAULT_LOCALE;
6
+ const oneLetter = weekday.toLocaleString(locale, {
7
+ weekday: "narrow",
8
+ });
9
+
10
+ return oneLetter;
11
+ };
@@ -0,0 +1,55 @@
1
+ import { ReactNode, useEffect, useRef } from "react";
2
+
3
+ const useFocusTrap = () => {
4
+ const containerRef = useRef<HTMLDivElement | null>(null);
5
+
6
+ // eslint-disable-next-line consistent-return
7
+ useEffect(() => {
8
+ if (containerRef.current) {
9
+ const element = containerRef.current;
10
+ const focusableElements =
11
+ containerRef.current.querySelectorAll<HTMLElement>(
12
+ '.rdp-nav_button, [tabindex]:not([tabindex="-1"])',
13
+ );
14
+
15
+ const firstElement = focusableElements[0];
16
+ const lastElement = focusableElements[focusableElements.length - 1];
17
+
18
+ const handleTabKeyPress = (event: KeyboardEvent) => {
19
+ if (event.key === "Tab") {
20
+ if (event.shiftKey && document.activeElement === firstElement) {
21
+ event.preventDefault();
22
+ lastElement.focus();
23
+ } else if (
24
+ !event.shiftKey &&
25
+ document.activeElement === lastElement // TODO: handle for any day
26
+ ) {
27
+ event.preventDefault();
28
+ firstElement.focus();
29
+ }
30
+ }
31
+ };
32
+
33
+ element.addEventListener("keydown", handleTabKeyPress);
34
+ return () => {
35
+ element.removeEventListener("keydown", handleTabKeyPress);
36
+ };
37
+ }
38
+ }, []);
39
+
40
+ return containerRef;
41
+ };
42
+
43
+ export type FocusTrapProps = {
44
+ children: ReactNode;
45
+ };
46
+
47
+ export default function FocusTrap({ children }: FocusTrapProps) {
48
+ const focusRef = useFocusTrap();
49
+
50
+ return (
51
+ <div className="trap" ref={focusRef}>
52
+ {children}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,26 @@
1
+ import { excludeControls } from "./excludeControls";
2
+
3
+ describe("excludeControls", () => {
4
+ describe("given a list of props", () => {
5
+ it("returns an object where each prop has an entry with disabled params", () => {
6
+ const props = ["className", "elementType"];
7
+
8
+ const actual = excludeControls(...props);
9
+
10
+ const expected = {
11
+ elementType: {
12
+ table: {
13
+ disable: true,
14
+ },
15
+ },
16
+ className: {
17
+ table: {
18
+ disable: true,
19
+ },
20
+ },
21
+ };
22
+
23
+ expect(actual).toEqual(expected);
24
+ });
25
+ });
26
+ });
@@ -0,0 +1,28 @@
1
+ // This function generates data structure required to hide specified
2
+ // prop from individual stories. Key 'table' is where specified key
3
+ // is disabled.
4
+ //
5
+ // The above is documented here:
6
+ // https://storybook.js.org/docs/react/essentials/controls#disable-controls-for-specific-properties
7
+ //
8
+ // The expected usage looks as follows:
9
+ //
10
+ // <Meta
11
+ // title="Components/MyComponent"
12
+ // component={MyComponent}
13
+ // argTypes={{
14
+ // label: {
15
+ // control: {
16
+ // type: "text",
17
+ // },
18
+ // table: {
19
+ // disable: true,
20
+ // },
21
+ // },
22
+ // }}
23
+ // />
24
+ export const excludeControls = (...args: string[]) =>
25
+ args.reduce(
26
+ (prev, acc) => ({ ...prev, [acc]: { table: { disable: true } } }),
27
+ {},
28
+ );
@@ -0,0 +1,7 @@
1
+ export const isTouchDevice = (): boolean | undefined => {
2
+ if (typeof window !== "undefined") {
3
+ return window.matchMedia("(hover: none), (pointer: coarse)").matches;
4
+ }
5
+
6
+ return undefined;
7
+ };