From 753ba1647ef67fcefd63d79b11ce3ee52ee2a5d3 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 12:49:00 -0400 Subject: [PATCH 001/351] Extends `useControlledValue` to accept any type --- .changeset/soft-berries-obey.md | 5 +++++ .../src/useControlledValue/useControlledValue.ts | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 .changeset/soft-berries-obey.md diff --git a/.changeset/soft-berries-obey.md b/.changeset/soft-berries-obey.md new file mode 100644 index 0000000000..f169b748be --- /dev/null +++ b/.changeset/soft-berries-obey.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/hooks': minor +--- + +Extends `useControlledValue` to accept any type diff --git a/packages/hooks/src/useControlledValue/useControlledValue.ts b/packages/hooks/src/useControlledValue/useControlledValue.ts index 008235ee54..c1eacd848d 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.ts +++ b/packages/hooks/src/useControlledValue/useControlledValue.ts @@ -1,7 +1,7 @@ import { ChangeEventHandler, useEffect, useState } from 'react'; import isUndefined from 'lodash/isUndefined'; -interface ControlledValueReturnObject { +interface ControlledValueReturnObject { /** Whether the value is controlled */ isControlled: boolean; @@ -24,14 +24,17 @@ interface ControlledValueReturnObject { * Returns a {@link ControlledValueReturnObject} with the controlled or uncontrolled `value`, * `onChange` & `onClear` handlers, a `setInternalValue` setter, and a boolean `isControlled` */ -export const useControlledValue = ( +export const useControlledValue = ( controlledValue?: T, changeHandler?: ChangeEventHandler, -): ControlledValueReturnObject => { + defaultValue?: T, +): ControlledValueReturnObject => { const isControlled = !isUndefined(controlledValue); // Keep track of state internally, initializing it to the controlled value - const [value, setInternalValue] = useState(controlledValue ?? ('' as T)); + const [value, setInternalValue] = useState( + controlledValue ?? defaultValue, + ); // If the controlled value changes, update the internal state variable useEffect(() => { From c565f2638902fabe592895c0dcb9a5c1bb61eed6 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 15:16:06 -0400 Subject: [PATCH 002/351] Adds more explicit tests --- .../useControlledValue.spec.ts | 112 +++++++++++++----- 1 file changed, 83 insertions(+), 29 deletions(-) diff --git a/packages/hooks/src/useControlledValue/useControlledValue.spec.ts b/packages/hooks/src/useControlledValue/useControlledValue.spec.ts index 8ebc6f789a..035cee5cb5 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.spec.ts +++ b/packages/hooks/src/useControlledValue/useControlledValue.spec.ts @@ -1,42 +1,96 @@ -import { ChangeEvent } from 'react'; +import { ChangeEvent, ChangeEventHandler } from 'react'; import { act } from 'react-test-renderer'; import { renderHook } from '@testing-library/react-hooks'; import { useControlledValue } from './useControlledValue'; +const changeEventMock = { + target: { value: 'banana' }, +} as ChangeEvent; + describe('packages/lib/useControlledValue', () => { - test('with controlled component', async () => { - const value = 'apple'; - const handler = jest.fn(); - const { - result: { current }, - } = renderHook(() => useControlledValue(value as string, handler)); - expect(current.isControlled).toBe(true); - expect(current.value).toBe(value); - - act(() => { - current.handleChange({ target: { value: 'banana' } } as ChangeEvent); - current.setUncontrolledValue('banana'); + describe('with controlled component', () => { + test('calling with a value sets value and isControlled', () => { + const handler = jest.fn(); + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: 'apple', + }); + expect(result.current.isControlled).toBe(true); + expect(result.current.value).toBe('apple'); + }); + + test('calling with a new value changes the value', () => { + const handler = jest.fn(); + const { result, rerender } = renderHook( + v => useControlledValue(v, handler), + { + initialProps: 'apple', + }, + ); + + expect(result.current.value).toBe('apple'); + + act(() => { + rerender('banana'); + }); + + expect(result.current.value).toBe('banana'); + }); + + test('provided handler should be called', () => { + const handler = jest.fn(); + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: 'apple', + }); + + // simulate responding to an event + act(() => { + result.current.handleChange(changeEventMock); + }); + expect(handler).toHaveBeenCalledWith( + expect.objectContaining(changeEventMock), + ); + // value doesn't change unless we explicitly change it + expect(result.current.value).toBe('apple'); }); - expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ target: { value: 'banana' } }), - ); - expect(current.value).toBe('apple'); }); - test('with uncontrolled component', async () => { - const value = undefined; - const handler = jest.fn(); - const { - result: { current }, - } = renderHook(() => useControlledValue(value, handler)); - expect(current.isControlled).toBe(false); - expect(current.value).toBe(''); - - act(() => { - current.handleChange({ target: { value: 'apple' } } as ChangeEvent); + describe('with uncontrolled component', () => { + test('calling without a value sets value and isControlled', () => { + const handler = jest.fn(); + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: undefined, + }); + expect(result.current.isControlled).toBe(false); + expect(result.current.value).toBe(undefined); + }); + + test('calling setter updates value', () => { + const handler = jest.fn(); + const { result } = renderHook( + v => useControlledValue(v, handler), + { + initialProps: undefined, + }, + ); + + act(() => { + result.current.setUncontrolledValue('apple'); + }); + expect(result.current.value).toBe('apple'); + }); + + test('provided handler should be called', () => { + const handler = jest.fn(); + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: undefined, + }); + + act(() => { + result.current.handleChange(changeEventMock); + }); expect(handler).toHaveBeenCalledWith( - expect.objectContaining({ target: { value: 'apple' } }), + expect.objectContaining(changeEventMock), ); }); }); From b61283585a257987408ccfa6b54b6ce23dd27d45 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 15:28:30 -0400 Subject: [PATCH 003/351] adds test component tests --- ...ue.spec.ts => useControlledValue.spec.tsx} | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) rename packages/hooks/src/useControlledValue/{useControlledValue.spec.ts => useControlledValue.spec.tsx} (51%) diff --git a/packages/hooks/src/useControlledValue/useControlledValue.spec.ts b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx similarity index 51% rename from packages/hooks/src/useControlledValue/useControlledValue.spec.ts rename to packages/hooks/src/useControlledValue/useControlledValue.spec.tsx index 035cee5cb5..bfff17fbb2 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.spec.ts +++ b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx @@ -1,6 +1,9 @@ +import React from 'react'; import { ChangeEvent, ChangeEventHandler } from 'react'; import { act } from 'react-test-renderer'; +import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import userEvent from '@testing-library/user-event'; import { useControlledValue } from './useControlledValue'; @@ -94,4 +97,81 @@ describe('packages/lib/useControlledValue', () => { ); }); }); + + describe('with test component', () => { + const TestComponent = ({ + valueProp, + handlerProp, + }: { + valueProp?: string; + handlerProp?: ChangeEventHandler; + }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { value, handleChange } = useControlledValue( + valueProp, + handlerProp, + ); + + return ; + }; + + describe('Controlled test component', () => { + test('initially renders with a value', () => { + const result = render(); + const input = result.getByTestId('test'); + expect(input).toHaveValue('apple'); + }); + + test('responds to value changes', () => { + const result = render(); + const input = result.getByTestId('test'); + result.rerender(); + expect(input).toHaveValue('banana'); + }); + + test('user interaction triggers handler', () => { + const handler = jest.fn(); + const result = render( + , + ); + const input = result.getByTestId('test'); + userEvent.type(input, 'b'); + expect(handler).toHaveBeenCalled(); + }); + + test('user interaction does not change the element value', () => { + const handler = jest.fn(); + const result = render( + , + ); + const input = result.getByTestId('test'); + userEvent.type(input, 'b'); + expect(input).toHaveValue('apple'); + }); + }); + + describe('Uncontrolled test component', () => { + test('initially renders without a value', () => { + const result = render(); + const input = result.getByTestId('test'); + expect(input).toHaveValue(''); + }); + + test('user interaction triggers handler', () => { + const handler = jest.fn(); + const result = render(); + const input = result.getByTestId('test'); + userEvent.type(input, 'b'); + expect(handler).toHaveBeenCalled(); + }); + + test('user interaction does not change the element value', () => { + const handler = jest.fn(); + const result = render(); + const input = result.getByTestId('test'); + userEvent.type(input, 'banana'); + expect(input).toHaveValue('banana'); + }); + }); + }); }); From f4e1c613075c85b707533d8831840073fbb7cf87 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 15:32:55 -0400 Subject: [PATCH 004/351] adds value tests --- .../useControlledValue.spec.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx index bfff17fbb2..8b2e85a018 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx +++ b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx @@ -56,6 +56,46 @@ describe('packages/lib/useControlledValue', () => { // value doesn't change unless we explicitly change it expect(result.current.value).toBe('apple'); }); + + describe('value types', () => { + test('accepts number values', () => { + const { result } = renderHook(v => useControlledValue(v), { + initialProps: 5, + }); + expect(result.current.value).toBe(5); + }); + + test('accepts boolean values', () => { + const { result } = renderHook(v => useControlledValue(v), { + initialProps: false, + }); + expect(result.current.value).toBe(false); + }); + + test('accepts array values', () => { + const arr = ['foo', 'bar']; + const { result } = renderHook(v => useControlledValue(v), { + initialProps: arr, + }); + expect(result.current.value).toBe(arr); + }); + + test('accepts object values', () => { + const obj = { foo: 'foo', bar: 'bar' }; + const { result } = renderHook(v => useControlledValue(v), { + initialProps: obj, + }); + expect(result.current.value).toBe(obj); + }); + + test('accepts date values', () => { + const date = new Date('2023-08-23'); + const { result } = renderHook(v => useControlledValue(v), { + initialProps: date, + }); + expect(result.current.value).toBe(date); + }); + }); }); describe('with uncontrolled component', () => { From bbb1832e65f3661f6f2dbc98706cf26844e2e53a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 15:51:20 -0400 Subject: [PATCH 005/351] isControlled never changes from initial render --- .changeset/soft-berries-obey.md | 3 +- .../useControlledValue.spec.tsx | 63 +++++++++++++------ .../useControlledValue/useControlledValue.ts | 6 +- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/.changeset/soft-berries-obey.md b/.changeset/soft-berries-obey.md index f169b748be..8d0236cdfe 100644 --- a/.changeset/soft-berries-obey.md +++ b/.changeset/soft-berries-obey.md @@ -2,4 +2,5 @@ '@leafygreen-ui/hooks': minor --- -Extends `useControlledValue` to accept any type +- Extends `useControlledValue` to accept any type. +- The value of `isControlled` is now immutable after the first render diff --git a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx index 8b2e85a018..faf8fc1b6f 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx +++ b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx @@ -14,8 +14,7 @@ const changeEventMock = { describe('packages/lib/useControlledValue', () => { describe('with controlled component', () => { test('calling with a value sets value and isControlled', () => { - const handler = jest.fn(); - const { result } = renderHook(v => useControlledValue(v, handler), { + const { result } = renderHook(v => useControlledValue(v), { initialProps: 'apple', }); expect(result.current.isControlled).toBe(true); @@ -23,13 +22,9 @@ describe('packages/lib/useControlledValue', () => { }); test('calling with a new value changes the value', () => { - const handler = jest.fn(); - const { result, rerender } = renderHook( - v => useControlledValue(v, handler), - { - initialProps: 'apple', - }, - ); + const { result, rerender } = renderHook(v => useControlledValue(v), { + initialProps: 'apple', + }); expect(result.current.value).toBe('apple'); @@ -40,7 +35,7 @@ describe('packages/lib/useControlledValue', () => { expect(result.current.value).toBe('banana'); }); - test('provided handler should be called', () => { + test('provided handler is called within returned hook handler', () => { const handler = jest.fn(); const { result } = renderHook(v => useControlledValue(v, handler), { initialProps: 'apple', @@ -57,6 +52,15 @@ describe('packages/lib/useControlledValue', () => { expect(result.current.value).toBe('apple'); }); + test('setting value to undefined should keep the component controlled', () => { + const { result, rerender } = renderHook(v => useControlledValue(v), { + initialProps: 'apple', + }); + expect(result.current.isControlled).toBe(true); + act(() => rerender(undefined)); + expect(result.current.isControlled).toBe(true); + }); + describe('value types', () => { test('accepts number values', () => { const { result } = renderHook(v => useControlledValue(v), { @@ -100,8 +104,7 @@ describe('packages/lib/useControlledValue', () => { describe('with uncontrolled component', () => { test('calling without a value sets value and isControlled', () => { - const handler = jest.fn(); - const { result } = renderHook(v => useControlledValue(v, handler), { + const { result } = renderHook(v => useControlledValue(v), { initialProps: undefined, }); expect(result.current.isControlled).toBe(false); @@ -109,13 +112,9 @@ describe('packages/lib/useControlledValue', () => { }); test('calling setter updates value', () => { - const handler = jest.fn(); - const { result } = renderHook( - v => useControlledValue(v, handler), - { - initialProps: undefined, - }, - ); + const { result } = renderHook(v => useControlledValue(v), { + initialProps: undefined, + }); act(() => { result.current.setUncontrolledValue('apple'); @@ -136,6 +135,32 @@ describe('packages/lib/useControlledValue', () => { expect.objectContaining(changeEventMock), ); }); + + test('calling the returned handler sets the value', () => { + const handler = jest.fn(); + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: undefined, + }); + + act(() => { + result.current.handleChange(changeEventMock); + }); + expect(result.current.value).toBe('banana'); + }); + + test('changing value prop from initial undefined is ignored', () => { + const { result, rerender } = renderHook( + v => useControlledValue(v), + { + initialProps: undefined, + }, + ); + expect(result.current.isControlled).toBe(false); + // @ts-ignore - picking up renderHook.options types, not actual hook types + act(() => rerender('apple')); + expect(result.current.isControlled).toBe(false); + expect(result.current.value).toBe(undefined); + }); }); describe('with test component', () => { diff --git a/packages/hooks/src/useControlledValue/useControlledValue.ts b/packages/hooks/src/useControlledValue/useControlledValue.ts index 536879319f..e843f2f551 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.ts +++ b/packages/hooks/src/useControlledValue/useControlledValue.ts @@ -1,4 +1,4 @@ -import { ChangeEventHandler, useState } from 'react'; +import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import isUndefined from 'lodash/isUndefined'; interface ControlledValueReturnObject { @@ -28,7 +28,9 @@ export const useControlledValue = ( controlledValue?: T, changeHandler?: ChangeEventHandler, ): ControlledValueReturnObject => { - const isControlled = !isUndefined(controlledValue); + // isControlled should only be computed once + // eslint-disable-next-line react-hooks/exhaustive-deps + const isControlled = useMemo(() => !isUndefined(controlledValue), []); // Keep track of the uncontrolled value state internally const [uncontrolledValue, setUncontrolledValue] = useState(); From af6cc4867690aeac0cf8f57618e4fb0819a4441c Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 24 Aug 2023 16:31:17 -0400 Subject: [PATCH 006/351] adds synthetic events --- .changeset/soft-berries-obey.md | 1 + packages/hooks/package.json | 1 + .../useControlledValue.spec.tsx | 108 +++++++++++++++--- .../useControlledValue/useControlledValue.ts | 30 ++++- packages/hooks/tsconfig.json | 6 +- 5 files changed, 130 insertions(+), 16 deletions(-) diff --git a/.changeset/soft-berries-obey.md b/.changeset/soft-berries-obey.md index 8d0236cdfe..b428fe5f29 100644 --- a/.changeset/soft-berries-obey.md +++ b/.changeset/soft-berries-obey.md @@ -3,4 +3,5 @@ --- - Extends `useControlledValue` to accept any type. +- Adds `updateValue` function in return value. This method triggers a synthetic event to update the value of a controlled or uncontrolled component. - The value of `isControlled` is now immutable after the first render diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 27ca1b683d..e3f73c5d70 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,6 +22,7 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/lib": "^11.0.0", "lodash": "^4.17.21" }, "gitHead": "dd71a2d404218ccec2e657df9c0263dc1c15b9e0", diff --git a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx index faf8fc1b6f..837c94aaa3 100644 --- a/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx +++ b/packages/hooks/src/useControlledValue/useControlledValue.spec.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { ChangeEvent, ChangeEventHandler } from 'react'; import { act } from 'react-test-renderer'; import { render } from '@testing-library/react'; @@ -11,6 +11,10 @@ const changeEventMock = { target: { value: 'banana' }, } as ChangeEvent; +const mutableRefMock = { + current: document.createElement('div'), +}; + describe('packages/lib/useControlledValue', () => { describe('with controlled component', () => { test('calling with a value sets value and isControlled', () => { @@ -61,6 +65,30 @@ describe('packages/lib/useControlledValue', () => { expect(result.current.isControlled).toBe(true); }); + test('setUncontrolledValue does nothing for controlled components', () => { + const { result } = renderHook(v => useControlledValue(v), { + initialProps: 'apple', + }); + act(() => { + result.current.setUncontrolledValue('banana'); + }); + expect(result.current.value).toBe('apple'); + }); + + test('updateValue triggers the provided handler', async () => { + const handler = jest.fn(); + + const { result } = renderHook(v => useControlledValue(v, handler), { + initialProps: 'apple', + }); + + await act(() => { + result.current.updateValue('banana', mutableRefMock); + }); + + expect(handler).toHaveBeenCalled(); + }); + describe('value types', () => { test('accepts number values', () => { const { result } = renderHook(v => useControlledValue(v), { @@ -111,15 +139,20 @@ describe('packages/lib/useControlledValue', () => { expect(result.current.value).toBe(undefined); }); - test('calling setter updates value', () => { - const { result } = renderHook(v => useControlledValue(v), { - initialProps: undefined, - }); + test('setUncontrolledValue updates value', () => { + const handler = jest.fn(); + const { result } = renderHook( + v => useControlledValue(v, handler), + { + initialProps: undefined, + }, + ); act(() => { result.current.setUncontrolledValue('apple'); }); expect(result.current.value).toBe('apple'); + expect(handler).not.toHaveBeenCalled(); }); test('provided handler should be called', () => { @@ -136,6 +169,23 @@ describe('packages/lib/useControlledValue', () => { ); }); + test('updateValue updates value & calls handler', () => { + const handler = jest.fn(); + const { result } = renderHook( + v => useControlledValue(v, handler), + { + initialProps: undefined, + }, + ); + + act(() => { + result.current.updateValue('banana', mutableRefMock); + }); + + expect(handler).toHaveBeenCalled(); + expect(result.current.value).toBe('banana'); + }); + test('calling the returned handler sets the value', () => { const handler = jest.fn(); const { result } = renderHook(v => useControlledValue(v, handler), { @@ -171,25 +221,39 @@ describe('packages/lib/useControlledValue', () => { valueProp?: string; handlerProp?: ChangeEventHandler; }) => { + const inputRef = useRef(null); // eslint-disable-next-line react-hooks/rules-of-hooks - const { value, handleChange } = useControlledValue( + const { value, handleChange, updateValue } = useControlledValue( valueProp, handlerProp, ); - return ; + return ( + <> + + + +); + export const Generated = () => <>; diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 85c70115ca..6fc62d3c3f 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -14,6 +14,14 @@ import { formFieldFontStyles, textContainerStyle } from './FormField.styles'; import { type FormFieldProps, FormFieldState } from './FormField.types'; import { useFormFieldProps } from './useFormFieldProps'; +/** + * Creates a form field element with the appropriate styles and attributes for each element. + * + * Use the {@link FormFieldInput} element to apply the appropriate + * interaction styles to the inner container element. + * + * See .stories file for examples + * */ export const FormField = forwardRef( ( { diff --git a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx b/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx index d35d0d19a6..1ef1584ca3 100644 --- a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx +++ b/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx @@ -23,6 +23,7 @@ import { } from './FormFieldInput.styles'; import { FormFieldInputProps } from './FormFieldInput.types'; +/** Applies styling around the input of a FormField element */ export const FormFieldInput = forwardRef( ({ icon, className, children, ...rest }: FormFieldInputProps, fwdRef) => { const { theme } = useDarkMode(); diff --git a/packages/form-field/tsconfig.json b/packages/form-field/tsconfig.json index 3b211e674b..510537789c 100644 --- a/packages/form-field/tsconfig.json +++ b/packages/form-field/tsconfig.json @@ -15,6 +15,9 @@ ], "exclude": ["**/*.spec.*", "**/*.story.*"], "references": [ + { + "path": "../button" + }, { "path": "../emotion" }, From d1541f627db19f7aa842f482940362d50818497b Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 15:06:14 -0400 Subject: [PATCH 165/351] improves button demo --- packages/form-field/src/FormField.stories.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 87240834cd..b493566eb6 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -10,6 +10,7 @@ import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; import { FormFieldProps, FormFieldState } from './FormField/FormField.types'; +import { useFormFieldContext } from './FormField/FormFieldContext/FormFieldContext'; import { FormFieldInput } from './FormField/FormFieldInput'; import { FormField } from '.'; @@ -121,11 +122,20 @@ export const WithIconButton: StoryFn = ({ ); +const DemoFormFieldButton = (props: FormFieldStoryProps) => { + const { inputProps } = useFormFieldContext(); + return ( + + ); +}; + export const WithButtonInput: StoryFn = ( props: FormFieldStoryProps, ) => ( - + ); From fd2881555d2ad647962ecc166084c0af3d8655e9 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 16:37:07 -0400 Subject: [PATCH 166/351] mv sub components --- packages/form-field/src/FormField.stories.tsx | 4 ++-- .../src/FormField/FormField.spec.tsx | 19 ------------------- .../form-field/src/FormField/FormField.tsx | 2 +- .../FormFieldInput/FormFieldInput.spec.tsx | 8 -------- .../FormFieldContext/FormFieldContext.tsx | 4 ++-- .../FormFieldInput/FormFieldInput.styles.ts | 2 +- .../FormFieldInput/FormFieldInput.tsx | 2 +- .../FormFieldInput/FormFieldInput.types.ts | 2 +- .../{FormField => }/FormFieldInput/index.ts | 0 9 files changed, 8 insertions(+), 35 deletions(-) delete mode 100644 packages/form-field/src/FormField/FormFieldInput/FormFieldInput.spec.tsx rename packages/form-field/src/{FormField => }/FormFieldContext/FormFieldContext.tsx (84%) rename packages/form-field/src/{FormField => }/FormFieldInput/FormFieldInput.styles.ts (99%) rename packages/form-field/src/{FormField => }/FormFieldInput/FormFieldInput.tsx (97%) rename packages/form-field/src/{FormField => }/FormFieldInput/FormFieldInput.types.ts (74%) rename packages/form-field/src/{FormField => }/FormFieldInput/index.ts (100%) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index b493566eb6..3c2138423b 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -10,8 +10,8 @@ import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; import { FormFieldProps, FormFieldState } from './FormField/FormField.types'; -import { useFormFieldContext } from './FormField/FormFieldContext/FormFieldContext'; -import { FormFieldInput } from './FormField/FormFieldInput'; +import { useFormFieldContext } from './FormFieldContext/FormFieldContext'; +import { FormFieldInput } from './FormFieldInput'; import { FormField } from '.'; const meta: StoryMetaType = { diff --git a/packages/form-field/src/FormField/FormField.spec.tsx b/packages/form-field/src/FormField/FormField.spec.tsx index ce7ba50c62..db5f75722a 100644 --- a/packages/form-field/src/FormField/FormField.spec.tsx +++ b/packages/form-field/src/FormField/FormField.spec.tsx @@ -277,25 +277,6 @@ describe('packages/form-field', () => { expect(icon?.tagName).toEqual('svg'); }); - test('inputWrapperProps are passed through', () => { - const { queryByTestId } = render( - -
- , - ); - const wrapper = queryByTestId('input-wrapper'); - expect(wrapper).toBeInTheDocument(); - expect(wrapper).toHaveAttribute('role', 'combobox'); - expect(wrapper?.classList.contains('input-class')).toBeTruthy(); - }); - // eslint-disable-next-line jest/no-disabled-tests test.skip('Types', () => { render( diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 6fc62d3c3f..64a070376d 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -9,7 +9,7 @@ import { useUpdatedBaseFontSize, } from '@leafygreen-ui/typography'; -import { FormFieldProvider } from './FormFieldContext/FormFieldContext'; +import { FormFieldProvider } from '../FormFieldContext/FormFieldContext'; import { formFieldFontStyles, textContainerStyle } from './FormField.styles'; import { type FormFieldProps, FormFieldState } from './FormField.types'; import { useFormFieldProps } from './useFormFieldProps'; diff --git a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.spec.tsx b/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.spec.tsx deleted file mode 100644 index e1f297b318..0000000000 --- a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.spec.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { FormFieldInput } from '.'; - -describe('packages/form-field-input-wrapper', () => { - test('condition', () => {}); -}); diff --git a/packages/form-field/src/FormField/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx similarity index 84% rename from packages/form-field/src/FormField/FormFieldContext/FormFieldContext.tsx rename to packages/form-field/src/FormFieldContext/FormFieldContext.tsx index d663f62458..30d073ff49 100644 --- a/packages/form-field/src/FormField/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -2,8 +2,8 @@ import React, { PropsWithChildren, useContext } from 'react'; import { Size } from '@leafygreen-ui/tokens'; -import { FormFieldState } from '../FormField.types'; -import { FormFieldInputElementProps } from '../useFormFieldProps'; +import { FormFieldState } from '../FormField/FormField.types'; +import { FormFieldInputElementProps } from '../FormField/useFormFieldProps'; interface FormFieldContextProps { disabled: boolean; diff --git a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.styles.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts similarity index 99% rename from packages/form-field/src/FormField/FormFieldInput/FormFieldInput.styles.ts rename to packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts index 0a8b8b110a..eba9ed0426 100644 --- a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.styles.ts +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts @@ -11,7 +11,7 @@ import { transitionDuration, } from '@leafygreen-ui/tokens'; -import { FormFieldState } from '../FormField.types'; +import { FormFieldState } from '../FormField/FormField.types'; export const inputElementClassName = createUniqueClassName('form-field-input'); export const iconClassName = createUniqueClassName('form-field-icon'); diff --git a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx b/packages/form-field/src/FormFieldInput/FormFieldInput.tsx similarity index 97% rename from packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx rename to packages/form-field/src/FormFieldInput/FormFieldInput.tsx index 1ef1584ca3..7997952eee 100644 --- a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.tsx +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.tsx @@ -5,7 +5,7 @@ import Icon from '@leafygreen-ui/icon'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { Size } from '@leafygreen-ui/tokens'; -import { FormFieldState } from '../FormField.types'; +import { FormFieldState } from '../FormField/FormField.types'; import { useFormFieldContext } from '../FormFieldContext/FormFieldContext'; import { diff --git a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.types.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.types.ts similarity index 74% rename from packages/form-field/src/FormField/FormFieldInput/FormFieldInput.types.ts rename to packages/form-field/src/FormFieldInput/FormFieldInput.types.ts index d16b587557..e4af4f5b8d 100644 --- a/packages/form-field/src/FormField/FormFieldInput/FormFieldInput.types.ts +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.types.ts @@ -1,6 +1,6 @@ import { HTMLElementProps } from '@leafygreen-ui/lib'; -import { FormFieldChildren } from '../FormField.types'; +import { FormFieldChildren } from '../FormField/FormField.types'; export interface FormFieldInputProps extends HTMLElementProps<'div'> { children: FormFieldChildren; diff --git a/packages/form-field/src/FormField/FormFieldInput/index.ts b/packages/form-field/src/FormFieldInput/index.ts similarity index 100% rename from packages/form-field/src/FormField/FormFieldInput/index.ts rename to packages/form-field/src/FormFieldInput/index.ts From 88033c5284f48b82c0edd30cbc6f39a07c7b121f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 16:46:56 -0400 Subject: [PATCH 167/351] reorg components. update tests --- .../src/{FormField => }/FormField.spec.tsx | 120 ++++++++++++++---- packages/form-field/src/FormField.stories.tsx | 11 +- .../src/FormFieldContext/FormFieldContext.tsx | 6 +- .../form-field/src/FormFieldContext/index.ts | 7 + packages/form-field/src/index.ts | 8 ++ 5 files changed, 120 insertions(+), 32 deletions(-) rename packages/form-field/src/{FormField => }/FormField.spec.tsx (76%) create mode 100644 packages/form-field/src/FormFieldContext/index.ts diff --git a/packages/form-field/src/FormField/FormField.spec.tsx b/packages/form-field/src/FormField.spec.tsx similarity index 76% rename from packages/form-field/src/FormField/FormField.spec.tsx rename to packages/form-field/src/FormField.spec.tsx index db5f75722a..5375b56ef5 100644 --- a/packages/form-field/src/FormField/FormField.spec.tsx +++ b/packages/form-field/src/FormField.spec.tsx @@ -3,13 +3,20 @@ import { render } from '@testing-library/react'; import Icon from '@leafygreen-ui/icon'; -import { FormField, FormFieldState } from '.'; +import { + FormField, + FormFieldInput, + FormFieldState, + useFormFieldContext, +} from '.'; describe('packages/form-field', () => { test('rest passed to outer element', () => { const { getByTestId } = render( -
+ +
+ , ); const formField = getByTestId('form-field'); @@ -19,7 +26,9 @@ describe('packages/form-field', () => { test('className passed to outer element', () => { const { getByTestId } = render( -
+ +
+ , ); const formField = getByTestId('form-field'); @@ -33,7 +42,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > -
+ +
+ , ); const description = getByText('Description'); @@ -47,7 +58,9 @@ describe('packages/form-field', () => { description={description} data-testid="form-field" > -
+ +
+ , ); const descriptionSpan = queryByTestId('description-span'); @@ -58,7 +71,9 @@ describe('packages/form-field', () => { test('input has id,', () => { const { getByTestId } = render( -
+ +
+ , ); const input = getByTestId('input'); @@ -69,7 +84,9 @@ describe('packages/form-field', () => { test('label element has id & htmlFor', () => { const { getByText } = render( -
+ +
+ , ); const label = getByText('Label'); @@ -83,7 +100,9 @@ describe('packages/form-field', () => { label={Label} data-testid="form-field" > -
+ +
+ , ); const labelSpan = queryByTestId('label-span'); @@ -98,7 +117,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > -
+ +
+ , ); const label = getByText('Label'); @@ -113,7 +134,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > -
+ +
+ , ); const labelSpan = queryByTestId('label-span'); @@ -131,7 +154,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > -
+ +
+ , ); const description = getByText('Description'); @@ -146,7 +171,9 @@ describe('packages/form-field', () => { description={description} data-testid="form-field" > -
+ +
+ , ); const descriptionSpan = queryByTestId('description-span'); @@ -160,7 +187,9 @@ describe('packages/form-field', () => { test('when aria-label is provided, input has that aria-label', () => { const { getByTestId } = render( -
+ +
+ , ); const input = getByTestId('input'); @@ -170,7 +199,9 @@ describe('packages/form-field', () => { test('when aria-labelledby is provided, input has that aria-labelledby', () => { const { getByTestId } = render( -
+ +
+ , ); const input = getByTestId('input'); @@ -186,7 +217,9 @@ describe('packages/form-field', () => { errorMessage="This is an error message" data-testid="form-field" > -
+ +
+ , ); const error = queryByText('This is an error message'); @@ -201,7 +234,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > -
+ +
+ , ); const error = queryByText('This is an error message'); @@ -216,7 +251,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > -
+ +
+ , ); const error = queryByText('This is an error message'); @@ -231,7 +268,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > -
+ +
+ , ); const input = getByTestId('input'); @@ -248,7 +287,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > -
+ +
+ , ); const input = getByTestId('input'); @@ -263,12 +304,10 @@ describe('packages/form-field', () => { test('Renders an icon', () => { const { queryByTestId } = render( - } - data-testid="form-field" - > -
+ + }> +
+ , ); @@ -277,6 +316,37 @@ describe('packages/form-field', () => { expect(icon?.tagName).toEqual('svg'); }); + describe('custom children', () => { + const TestChild = () => { + const { inputProps } = useFormFieldContext(); + return
; + }; + + test('custom child is rendered', () => { + const { queryByTestId } = render( + + + , + ); + + const child = queryByTestId('child'); + expect(child).toBeInTheDocument(); + }); + + test('custom child is labelled by the label element', () => { + const { queryByTestId, getByText } = render( + + + , + ); + const label = getByText('Label'); + const child = queryByTestId('child'); + expect(child!.id).toBeDefined(); + expect(child!.id).toEqual(label.getAttribute('for')); + expect(child!.getAttribute('aria-labelledby')).toEqual(label.id); + }); + }); + // eslint-disable-next-line jest/no-disabled-tests test.skip('Types', () => { render( diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 3c2138423b..9b3319745e 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -9,10 +9,13 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; -import { FormFieldProps, FormFieldState } from './FormField/FormField.types'; -import { useFormFieldContext } from './FormFieldContext/FormFieldContext'; -import { FormFieldInput } from './FormFieldInput'; -import { FormField } from '.'; +import { + FormField, + FormFieldInput, + FormFieldProps, + FormFieldState, + useFormFieldContext, +} from '.'; const meta: StoryMetaType = { title: 'Components/FormField', diff --git a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx index 30d073ff49..08ea16798c 100644 --- a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -5,20 +5,20 @@ import { Size } from '@leafygreen-ui/tokens'; import { FormFieldState } from '../FormField/FormField.types'; import { FormFieldInputElementProps } from '../FormField/useFormFieldProps'; -interface FormFieldContextProps { +export interface FormFieldContextProps { disabled: boolean; size: Size; state: FormFieldState; inputProps?: FormFieldInputElementProps; } -const defaultFormFieldContext = { +export const defaultFormFieldContext = { disabled: false, size: Size.Default, state: FormFieldState.Unset, }; -const FormFieldContext = React.createContext( +export const FormFieldContext = React.createContext( defaultFormFieldContext, ); diff --git a/packages/form-field/src/FormFieldContext/index.ts b/packages/form-field/src/FormFieldContext/index.ts new file mode 100644 index 0000000000..9b8dfd5416 --- /dev/null +++ b/packages/form-field/src/FormFieldContext/index.ts @@ -0,0 +1,7 @@ +export { + defaultFormFieldContext, + FormFieldContext, + FormFieldContextProps, + FormFieldProvider, + useFormFieldContext, +} from './FormFieldContext'; diff --git a/packages/form-field/src/index.ts b/packages/form-field/src/index.ts index e4b6ffc72a..02e7140128 100644 --- a/packages/form-field/src/index.ts +++ b/packages/form-field/src/index.ts @@ -5,3 +5,11 @@ export { type FormFieldProps, FormFieldState, } from './FormField'; +export { + defaultFormFieldContext, + FormFieldContext, + type FormFieldContextProps, + FormFieldProvider, + useFormFieldContext, +} from './FormFieldContext'; +export { FormFieldInput, type FormFieldInputProps } from './FormFieldInput'; From e8777ae6b227580e3e32d4ed175c546a9165ab26 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 17:10:56 -0400 Subject: [PATCH 168/351] Address RA UI bugs --- packages/form-field/src/FormField.stories.tsx | 48 ++++++++++++-- .../src/FormField/FormField.styles.ts | 6 +- .../form-field/src/FormField/FormField.tsx | 64 +++++++++++-------- .../FormFieldInput/FormFieldInput.styles.ts | 6 ++ .../src/FormFieldInput/FormFieldInput.tsx | 37 ++++++----- 5 files changed, 110 insertions(+), 51 deletions(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 9b3319745e..6b0120c81b 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { StoryFn } from '@storybook/react'; import Button from '@leafygreen-ui/button'; +import { css } from '@leafygreen-ui/emotion'; import Icon, { glyphs } from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; @@ -12,19 +13,20 @@ import { Size } from '@leafygreen-ui/tokens'; import { FormField, FormFieldInput, + FormFieldInputProps, FormFieldProps, FormFieldState, useFormFieldContext, } from '.'; -const meta: StoryMetaType = { +type FormFieldStoryProps = FormFieldProps & + FormFieldInputProps & { glyph: string }; + +const meta: StoryMetaType = { title: 'Components/FormField', component: FormField, parameters: { default: 'Basic', - controls: { - exclude: ['inputWrapperProps', 'icon'], - }, generate: { combineArgs: { darkMode: [false, true], @@ -34,7 +36,12 @@ const meta: StoryMetaType = { state: Object.values(FormFieldState), disabled: [false, true], }, - excludeCombinations: [{}], + excludeCombinations: [ + { + disabled: true, + state: FormFieldState.Error, + }, + ], args: { children: , }, @@ -70,7 +77,6 @@ const meta: StoryMetaType = { export default meta; -type FormFieldStoryProps = FormFieldProps & { glyph: string }; export const Basic: StoryFn = ({ label, description, @@ -125,6 +131,34 @@ export const WithIconButton: StoryFn = ({ ); +export const Custom_TwoIcons: StoryFn = ({ + glyph, + ...props +}: FormFieldStoryProps) => ( + + + + + + + + } + > + + + +); + const DemoFormFieldButton = (props: FormFieldStoryProps) => { const { inputProps } = useFormFieldContext(); return ( @@ -134,7 +168,7 @@ const DemoFormFieldButton = (props: FormFieldStoryProps) => { ); }; -export const WithButtonInput: StoryFn = ( +export const Custom_ButtonInput: StoryFn = ( props: FormFieldStoryProps, ) => ( diff --git a/packages/form-field/src/FormField/FormField.styles.ts b/packages/form-field/src/FormField/FormField.styles.ts index 8f1493ad26..facd664527 100644 --- a/packages/form-field/src/FormField/FormField.styles.ts +++ b/packages/form-field/src/FormField/FormField.styles.ts @@ -12,8 +12,12 @@ export const formFieldFontStyles: Record = { `, }; -export const textContainerStyle = css` +export const labelTextContainerStyle = css` display: flex; flex-direction: column; margin-bottom: ${spacing[1]}px; `; + +export const errorTextContainerStyle = css` + margin-top: ${spacing[1]}px; +`; diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 64a070376d..3b59c6c4df 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -1,6 +1,7 @@ import React, { forwardRef } from 'react'; import { cx } from '@leafygreen-ui/emotion'; +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { Size } from '@leafygreen-ui/tokens'; import { Description, @@ -9,8 +10,13 @@ import { useUpdatedBaseFontSize, } from '@leafygreen-ui/typography'; -import { FormFieldProvider } from '../FormFieldContext/FormFieldContext'; -import { formFieldFontStyles, textContainerStyle } from './FormField.styles'; +import { FormFieldProvider } from '../FormFieldContext'; + +import { + errorTextContainerStyle, + formFieldFontStyles, + labelTextContainerStyle, +} from './FormField.styles'; import { type FormFieldProps, FormFieldState } from './FormField.types'; import { useFormFieldProps } from './useFormFieldProps'; @@ -37,36 +43,42 @@ export const FormField = forwardRef( }: FormFieldProps, fwdRef, ) => { - const baseFontSize = useUpdatedBaseFontSize(); + const baseFontSize = useUpdatedBaseFontSize( + size === Size.Large ? 16 : undefined, + ); const { labelId, descriptionId, errorId, inputId, inputProps } = useFormFieldProps({ label, description, state, ...rest }); return ( - -
-
- {label && ( - - )} - {description && ( - - {description} - - )} + + +
+
+ {label && ( + + )} + {description && ( + + {description} + + )} +
+ {children} +
+ {state === FormFieldState.Error && ( + {errorMessage} + )} +
- {children} - {state === FormFieldState.Error && ( - {errorMessage} - )} -
- + + ); }, ); diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts index eba9ed0426..58b54130c9 100644 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts @@ -302,6 +302,12 @@ export const childrenWrapperStyles = css` width: 100%; `; +export const iconsWrapperStyles = css` + display: flex; + align-items: center; + gap: ${spacing[1]}px; +`; + export const iconStyles: Record = { [Theme.Light]: css` color: ${palette.gray.base}; diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.tsx b/packages/form-field/src/FormFieldInput/FormFieldInput.tsx index 7997952eee..69170eacfd 100644 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.tsx +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.tsx @@ -13,6 +13,7 @@ import { errorIconStyles, iconClassName, iconStyles, + iconsWrapperStyles, inputElementClassName, inputWrapperBaseStyles, inputWrapperDisabledStyles, @@ -52,23 +53,25 @@ export const FormFieldInput = forwardRef( )} >
{renderedChildren}
- {state === FormFieldState.Error && ( - - )} - {icon && - React.cloneElement(icon, { - className: cx( - iconClassName, - iconStyles[theme], - icon.props.className, - ), - disabled, - })} +
+ {state === FormFieldState.Error && ( + + )} + {icon && + React.cloneElement(icon, { + className: cx( + iconClassName, + iconStyles[theme], + icon.props.className, + ), + disabled, + })} +
); }, From 595bc199beafd6058f002dd790a0d4f47e34f32a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 17:27:38 -0400 Subject: [PATCH 169/351] Update yarn.lock --- yarn.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4c4040c9e5..a00b7c8d3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1413,6 +1413,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.21.0": + version "7.23.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d" + integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.20.7", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -2267,6 +2274,18 @@ lodash "^4.17.21" prop-types "^15.7.2" +"@leafygreen-ui/typography@^16.5.5": + version "16.5.5" + resolved "https://registry.yarnpkg.com/@leafygreen-ui/typography/-/typography-16.5.5.tgz#65cc0ce7e39f5b8f72f9b9a4dbea80868ac09694" + integrity sha512-mErhTYM0C1PZaeADTkp5v/MAS6aEhavWHZ3otHthBSo/zwI5uAYnkreheiYElc66B/0bcOxCikLVkP3zaFnX2A== + dependencies: + "@leafygreen-ui/emotion" "^4.0.7" + "@leafygreen-ui/icon" "^11.22.2" + "@leafygreen-ui/lib" "^11.0.0" + "@leafygreen-ui/palette" "^4.0.7" + "@leafygreen-ui/polymorphic" "^1.3.6" + "@leafygreen-ui/tokens" "^2.1.4" + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" @@ -6083,6 +6102,18 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +date-fns-tz@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-2.0.0.tgz#1b14c386cb8bc16fc56fe333d4fc34ae1d1099d5" + integrity sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ== + +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -10005,6 +10036,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mockdate@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/mockdate/-/mockdate-3.0.5.tgz#789be686deb3149e7df2b663d2bc4392bc3284fb" + integrity sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ== + moo-color@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.3.tgz#d56435f8359c8284d83ac58016df7427febece74" @@ -12406,6 +12442,11 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +timezone-mock@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.3.6.tgz#44e4c5aeb57e6c07ae630a05c528fc4d9aab86f4" + integrity sha512-YcloWmZfLD9Li5m2VcobkCDNVaLMx8ohAb/97l/wYS3m+0TIEK5PFNMZZfRcusc6sFjIfxu8qcJT0CNnOdpqmg== + tiny-emitter@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" @@ -13137,6 +13178,11 @@ webpack@5.88.0: watchpack "^2.4.0" webpack-sources "^3.2.3" +weekstart@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/weekstart/-/weekstart-2.0.0.tgz#3925b0626b311b353097d81638bd0403a53d81f7" + integrity sha512-HjYc14IQUwDcnGICuc8tVtqAd6EFpoAQMqgrqcNtWWZB+F1b7iTq44GzwM1qvnH4upFgbhJsaNHuK93NOFheSg== + whatwg-encoding@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" From 01694aa074d516758964bdac333c5acc34418f07 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 18:01:13 -0400 Subject: [PATCH 170/351] refactor DateFormField --- .../CalendarButton/CalendarButton.styles.ts | 3 + .../CalendarButton/CalendarButton.tsx | 25 ++++++++ .../src/DateInput/CalendarButton/index.ts | 1 + .../DateFormField/DateFormField.stories.tsx | 53 ++++++++--------- .../DateInput/DateFormField/DateFormField.tsx | 57 ++++++++++--------- .../DateFormField/DateFormField.types.ts | 6 +- packages/date-picker/src/DateInput/index.ts | 1 - .../DatePickerInput/DatePickerInput.tsx | 24 ++------ .../DateRangeComponent.styles.ts | 1 - .../DateRangeComponent/index.ts | 3 +- .../DateRangeInput/DateRangeInput.tsx | 20 ++++--- .../DateRangeInput/DateRangeInput.types.ts | 2 +- .../DateRangePicker/DateRangeInput/index.ts | 3 +- .../DateRangePicker/DateRangePicker.styles.ts | 1 - .../date-picker/src/DateRangePicker/index.ts | 3 +- 15 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts create mode 100644 packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx create mode 100644 packages/date-picker/src/DateInput/CalendarButton/index.ts diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts new file mode 100644 index 0000000000..f11767d057 --- /dev/null +++ b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts @@ -0,0 +1,3 @@ +import { css } from '@leafygreen-ui/emotion'; + +export const iconButtonStyles = css``; diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx new file mode 100644 index 0000000000..0fff74a6de --- /dev/null +++ b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx @@ -0,0 +1,25 @@ +import React, { forwardRef } from 'react'; + +import { cx } from '@leafygreen-ui/emotion'; +import Icon from '@leafygreen-ui/icon'; +import IconButton, { BaseIconButtonProps } from '@leafygreen-ui/icon-button'; + +import { iconButtonStyles } from './CalendarButton.styles'; + +export const CalendarButton = forwardRef< + HTMLButtonElement, + BaseIconButtonProps +>(({ className, ...rest }: BaseIconButtonProps) => { + return ( + + + + ); +}); + +CalendarButton.displayName = 'CalendarButton'; diff --git a/packages/date-picker/src/DateInput/CalendarButton/index.ts b/packages/date-picker/src/DateInput/CalendarButton/index.ts new file mode 100644 index 0000000000..b4b4375473 --- /dev/null +++ b/packages/date-picker/src/DateInput/CalendarButton/index.ts @@ -0,0 +1 @@ +export { CalendarButton } from './CalendarButton'; diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx index 7229c7ce19..da70bcd793 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { StoryFn } from '@storybook/react'; import { css } from '@leafygreen-ui/emotion'; +import { FormFieldState } from '@leafygreen-ui/form-field'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; @@ -45,7 +46,7 @@ const meta: StoryMetaType< > @@ -81,37 +82,29 @@ const meta: StoryMetaType< export default meta; -const Template: StoryFn = ({ - label, - description, - state, - errorMessage, -}) => { - const inputId = 'input'; - const descriptionId = 'descr'; - const errorId = 'error'; - +const Template: StoryFn = () => { return ( - - - + + + + ); }; diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx index 3365772244..e07f021ddf 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { FormField } from '@leafygreen-ui/form-field'; -import Icon from '@leafygreen-ui/icon'; -import IconButton from '@leafygreen-ui/icon-button'; +import { FormField, FormFieldInput } from '@leafygreen-ui/form-field'; + +import { useDatePickerContext } from '../../DatePickerContext'; +import { CalendarButton } from '../CalendarButton'; -import { iconButtonStyles } from './DateFormField.styles'; import { DateFormFieldProps } from './DateFormField.types'; /** A wrapper around `FormField` that sets the icon */ @@ -13,36 +13,39 @@ export const DateFormField = React.forwardRef< DateFormFieldProps >( ( - { - children, - onIconButtonClick, - inputWrapperProps, - ...rest - }: DateFormFieldProps, + { children, onInputClick, onIconButtonClick, ...rest }: DateFormFieldProps, fwdRef, ) => { + const { + label, + description, + state, + errorMessage, + disabled, + isOpen, + menuId, + } = useDatePickerContext(); + return ( - - - } - inputWrapperProps={{ - ...inputWrapperProps, - role: 'combobox', - tabIndex: -1, - }} + label={label} + description={description} + disabled={disabled} + state={state} + errorMessage={errorMessage} {...rest} > - {children} + } + > + {children} + ); }, diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts b/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts index fb5e4f38cb..1743e5df11 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts @@ -1,6 +1,7 @@ import { MouseEventHandler } from 'react'; import { FormFieldProps } from '@leafygreen-ui/form-field'; +import { HTMLElementProps } from '@leafygreen-ui/lib'; export const InputState = { Unset: 'unset', @@ -9,7 +10,10 @@ export const InputState = { export type InputState = (typeof InputState)[keyof typeof InputState]; -export type DateFormFieldProps = FormFieldProps & { +export type DateFormFieldProps = HTMLElementProps<'div'> & { + children: FormFieldProps['children']; + /** Callback fired when the input is clicked */ + onInputClick?: MouseEventHandler; /** Fired then the calendar icon button is clicked */ onIconButtonClick?: MouseEventHandler; }; diff --git a/packages/date-picker/src/DateInput/index.ts b/packages/date-picker/src/DateInput/index.ts index 0af14bdff2..eadb3a750a 100644 --- a/packages/date-picker/src/DateInput/index.ts +++ b/packages/date-picker/src/DateInput/index.ts @@ -1,2 +1 @@ -export { DateFormField, type DateFormFieldProps } from './DateFormField'; export { DateInputBox, type DateInputBoxProps } from './DateInputBox'; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 25955d2422..1d73551549 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -7,7 +7,8 @@ import React, { import { keyMap } from '@leafygreen-ui/lib'; -import { DateFormField, DateInputBox } from '../../DateInput'; +import { DateInputBox } from '../../DateInput'; +import { DateFormField } from '../../DateInput/DateFormField'; import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; import { isZeroLike } from '../../utils'; @@ -30,16 +31,8 @@ export const DatePickerInput = forwardRef( }: DatePickerInputProps, fwdRef, ) => { - const { - label, - description, - menuId, - formatParts, - disabled, - isOpen, - setOpen, - setIsDirty, - } = useDatePickerContext(); + const { formatParts, disabled, setOpen, setIsDirty } = + useDatePickerContext(); const segmentRefs = useSegmentRefs(); /** Called when the input, or any of its children, is clicked */ @@ -160,16 +153,9 @@ export const DatePickerInput = forwardRef( return ( ( - (_props: DateRangeInputProps) => { - const { - label, - description, - // formatParts, disabled, setOpen, setIsDirty - } = useDatePickerContext(); + (_props: DateRangeInputProps, fwdRef) => { + // const { + // label, + // description, + // isOpen, + // menuId, + // // formatParts, disabled, setOpen, setIsDirty + // } = useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); const endSegmentRefs = useSegmentRefs(); return ( - +
{EN_DASH} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts index 17ff6df75a..fdbfeeb41d 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts @@ -1 +1 @@ -export interface DateRangeInputProps {} \ No newline at end of file +export interface DateRangeInputProps {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts index e04609a059..ffa12d47f9 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts @@ -1,3 +1,2 @@ - -export { DateRangeInput } from './DateRangeInput'; +export { DateRangeInput } from './DateRangeInput'; export { DateRangeInputProps } from './DateRangeInput.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts index 928608f58d..356c065c34 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts @@ -1,4 +1,3 @@ - import { css } from '@leafygreen-ui/emotion'; export const baseStyles = css``; diff --git a/packages/date-picker/src/DateRangePicker/index.ts b/packages/date-picker/src/DateRangePicker/index.ts index ccb7711d7b..4b0004f344 100644 --- a/packages/date-picker/src/DateRangePicker/index.ts +++ b/packages/date-picker/src/DateRangePicker/index.ts @@ -1,3 +1,2 @@ - -export { DateRangePicker } from './DateRangePicker'; +export { DateRangePicker } from './DateRangePicker'; export { DateRangePickerProps } from './DateRangePicker.types'; From e5e55595b8aed6a294aed14cea8282d9681c739a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 18:38:34 -0400 Subject: [PATCH 171/351] adds tests for getSegmentToFocus --- .../DatePickerInput/DatePickerInput.tsx | 6 +- .../getSegmentToFocus.spec.ts | 94 +++++++++++++++++++ .../index.ts | 45 +++++---- 3 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts rename packages/date-picker/src/DatePicker/utils/{focusRelevantSegment => getSegmentToFocus}/index.ts (58%) diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 1d73551549..9370cdbc00 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -12,8 +12,8 @@ import { DateFormField } from '../../DateInput/DateFormField'; import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; import { isZeroLike } from '../../utils'; -import { focusRelevantSegment } from '../utils/focusRelevantSegment'; import { getRelativeSegment } from '../utils/getRelativeSegment'; +import { getSegmentToFocus } from '../utils/getSegmentToFocus'; import { isElementInputSegment } from '../utils/isElementInputSegment'; import { DatePickerInputProps } from './DatePickerInput.types'; @@ -40,11 +40,13 @@ export const DatePickerInput = forwardRef( if (!disabled) { setOpen(true); - focusRelevantSegment({ + const segmentToFocus = getSegmentToFocus({ target, formatParts, segmentRefs, }); + + segmentToFocus?.focus(); } }; diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts new file mode 100644 index 0000000000..47f8b944e6 --- /dev/null +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts @@ -0,0 +1,94 @@ +import { createRef } from 'react'; + +import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { getFormatParts } from '../../../utils'; + +import { getSegmentToFocus } from '.'; + +describe('packages/date-picker/utils/getSegmentToFocus', () => { + const formatParts = getFormatParts('iso8601'); + + test('if target is a segment, return target', () => { + const target = document.createElement('input'); + + const segmentRefs: SegmentRefs = { + year: createRef(), + month: createRef(), + day: { current: target }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(target); + }); + + test('if all inputs are filled, return the last input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + yearEl.id = 'year'; + const monthEl = document.createElement('input'); + monthEl.value = '12'; + monthEl.id = 'month'; + const dayEl = document.createElement('input'); + dayEl.value = '26'; + dayEl.id = 'day'; + + const segmentRefs: SegmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(dayEl); + }); + + test('if first input is filled, return second input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + yearEl.id = 'year'; + const monthEl = document.createElement('input'); + monthEl.id = 'month'; + const dayEl = document.createElement('input'); + dayEl.id = 'day'; + + const segmentRefs: SegmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(monthEl); + }); + + test('returns undefined for undefined input', () => { + const segment = getSegmentToFocus({ + // @ts-expect-error + target: undefined, + formatParts: undefined, + // @ts-expect-error + segmentRefs: undefined, + }); + + expect(segment).toBeUndefined(); + }); +}); diff --git a/packages/date-picker/src/DatePicker/utils/focusRelevantSegment/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts similarity index 58% rename from packages/date-picker/src/DatePicker/utils/focusRelevantSegment/index.ts rename to packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts index b9b84b7bf6..93894cc243 100644 --- a/packages/date-picker/src/DatePicker/utils/focusRelevantSegment/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts @@ -2,13 +2,13 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; import { DatePickerContextProps } from '../../../DatePickerContext'; -import { isDateSegment } from '../../../hooks/useDateSegments'; +import { DateSegment } from '../../../hooks/useDateSegments'; import { SegmentRefs } from '../../../hooks/useSegmentRefs'; interface FocusRelevantSegmentArgs { target: EventTarget; formatParts: DatePickerContextProps['formatParts']; - segmentRefs: SegmentRefs; + segmentRefs: SegmentRefs; //| Array; } /** @@ -19,11 +19,11 @@ interface FocusRelevantSegmentArgs { * 2) otherwise, if all segments are filled, focus the last one * 3) but, if some segments are empty, focus the first empty one */ -export const focusRelevantSegment = ({ +export const getSegmentToFocus = ({ target, formatParts, segmentRefs, -}: FocusRelevantSegmentArgs): void => { +}: FocusRelevantSegmentArgs): HTMLElement | undefined | null => { if ( isUndefined(target) || isUndefined(formatParts) || @@ -34,39 +34,38 @@ export const focusRelevantSegment = ({ const segmentRefsArray = Object.values(segmentRefs).map(r => r.current); + const isTargetASegment = segmentRefsArray.includes( + target as HTMLInputElement, + ); + // If we didn't explicitly click on an input segment... - if (!segmentRefsArray.includes(target as HTMLInputElement)) { + if (!isTargetASegment) { + const allSegmentsFilled = segmentRefsArray.every(el => el?.value); // filter out the literals from the format parts const formatSegments = formatParts.filter(part => part.type !== 'literal'); // Check which segments are filled, - if (segmentRefsArray.every(el => el?.value)) { + if (allSegmentsFilled) { // if all are filled, focus the last one, - const keyOfLastSegment = (last(formatSegments) as Intl.DateTimeFormatPart) - .type; - - if (isDateSegment(keyOfLastSegment)) { - const lastSegmentRef = segmentRefs[keyOfLastSegment]; - lastSegmentRef?.current?.focus(); - } + const lastSegmentPart = last(formatSegments) as Intl.DateTimeFormatPart; + const keyOfLastSegment = lastSegmentPart.type as DateSegment; + const lastSegmentRef = segmentRefs[keyOfLastSegment]; + return lastSegmentRef.current; } else { // if 1+ are empty, focus the first empty one const emptySegmentKeys = formatSegments .map(p => p.type) .filter(type => { - if (isDateSegment(type)) { - const element = segmentRefs[type]; - return !element?.current?.value; - } + const element = segmentRefs[type as DateSegment]; + return !element?.current?.value; }); - const firstEmptySegmentKey = emptySegmentKeys[0]; - - if (isDateSegment(firstEmptySegmentKey)) { - const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; - firstEmptySegmentRef?.current?.focus(); - } + const firstEmptySegmentKey = emptySegmentKeys[0] as DateSegment; + const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; + return firstEmptySegmentRef.current; } } + // otherwise, we clicked a specific segment, // so we focus on that segment (default behavior) + return target as HTMLInputElement; }; From c37b02e1436f45aaf7877d7869324d36401ab5cd Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 19:09:41 -0400 Subject: [PATCH 172/351] creates getRangeSegmentToFocus --- .../utils/getSegmentToFocus/index.ts | 20 +++--- .../DateRangeInput/DateRangeInput.tsx | 29 ++++++--- .../utils/getRangeSegmentToFocus/index.ts | 65 +++++++++++++++++++ .../src/utils/getFirstEmptySegment/index.ts | 27 ++++++++ packages/date-picker/src/utils/index.ts | 1 + 5 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts create mode 100644 packages/date-picker/src/utils/getFirstEmptySegment/index.ts diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts index 93894cc243..b6b47d4af9 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts @@ -4,11 +4,12 @@ import last from 'lodash/last'; import { DatePickerContextProps } from '../../../DatePickerContext'; import { DateSegment } from '../../../hooks/useDateSegments'; import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { getFirstEmptySegment } from '../../../utils'; -interface FocusRelevantSegmentArgs { +interface GetSegmentToFocusProps { target: EventTarget; formatParts: DatePickerContextProps['formatParts']; - segmentRefs: SegmentRefs; //| Array; + segmentRefs: SegmentRefs; } /** @@ -23,7 +24,7 @@ export const getSegmentToFocus = ({ target, formatParts, segmentRefs, -}: FocusRelevantSegmentArgs): HTMLElement | undefined | null => { +}: GetSegmentToFocusProps): HTMLElement | undefined | null => { if ( isUndefined(target) || isUndefined(formatParts) || @@ -53,15 +54,10 @@ export const getSegmentToFocus = ({ return lastSegmentRef.current; } else { // if 1+ are empty, focus the first empty one - const emptySegmentKeys = formatSegments - .map(p => p.type) - .filter(type => { - const element = segmentRefs[type as DateSegment]; - return !element?.current?.value; - }); - const firstEmptySegmentKey = emptySegmentKeys[0] as DateSegment; - const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; - return firstEmptySegmentRef.current; + return getFirstEmptySegment({ + formatParts, + segmentRefs, + }); } } diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index 30a97faf25..b6e401916c 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -1,8 +1,10 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, MouseEventHandler } from 'react'; import { DateInputBox } from '../../DateInput'; import { DateFormField } from '../../DateInput/DateFormField'; +import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; +import { getRangeSegmentToFocus } from '../utils/getRangeSegmentToFocus'; import { inputWrapperStyles } from './DateRangeInput.styles'; import { DateRangeInputProps } from './DateRangeInput.types'; @@ -11,19 +13,28 @@ const EN_DASH = '–'; export const DateRangeInput = forwardRef( (_props: DateRangeInputProps, fwdRef) => { - // const { - // label, - // description, - // isOpen, - // menuId, - // // formatParts, disabled, setOpen, setIsDirty - // } = useDatePickerContext(); + const { disabled, formatParts, setOpen } = useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); const endSegmentRefs = useSegmentRefs(); + /** Called when the input, or any of its children, is clicked */ + const handleInputClick: MouseEventHandler = ({ target }) => { + if (!disabled) { + setOpen(true); + } + + const segmentToFocus = getRangeSegmentToFocus({ + target, + formatParts, + segmentRefs: [startSegmentRefs, endSegmentRefs], + }); + + segmentToFocus?.focus(); + }; + return ( - +
{EN_DASH} diff --git a/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts b/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts new file mode 100644 index 0000000000..3b9497002b --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts @@ -0,0 +1,65 @@ +import isUndefined from 'lodash/isUndefined'; +import last from 'lodash/last'; +import { DatePickerContextProps } from 'src/DatePickerContext'; + +import { DateSegment } from '../../../hooks/useDateSegments'; +import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { getFirstEmptySegment } from '../../../utils'; + +interface GetRangeSegmentToFocusProps { + target: EventTarget; + formatParts: DatePickerContextProps['formatParts']; + segmentRefs: [SegmentRefs, SegmentRefs]; +} + +/** Returns the range segment to focus on when the input is clicked */ +export const getRangeSegmentToFocus = ({ + target, + formatParts, + segmentRefs: [startSegmentRefs, endSegmentRefs], +}: GetRangeSegmentToFocusProps): HTMLElement | undefined | null => { + if ( + isUndefined(target) || + isUndefined(formatParts) || + isUndefined(startSegmentRefs) || + isUndefined(endSegmentRefs) + ) { + return; + } + + const startSegmentsArray = Object.values(startSegmentRefs).map( + r => r.current, + ); + const endSegmentsArray = Object.values(endSegmentRefs).map(r => r.current); + const segmentRefsArray = [...startSegmentsArray, ...endSegmentsArray]; + + const isTargetASegment = segmentRefsArray.includes( + target as HTMLInputElement, + ); + + if (!isTargetASegment) { + const formatSegments = formatParts.filter(part => part.type !== 'literal'); + const allSegmentsFilled = segmentRefsArray.every(el => el?.value); + + if (allSegmentsFilled) { + const lastSegmentPart = last(formatSegments) as Intl.DateTimeFormatPart; + const keyOfLastSegment = lastSegmentPart.type as DateSegment; + const lastSegmentRef = endSegmentRefs[keyOfLastSegment]; + return lastSegmentRef.current; + } else { + const allStartSegmentsFilled = startSegmentsArray.every(el => el?.value); + + if (allStartSegmentsFilled) { + return getFirstEmptySegment({ + formatParts, + segmentRefs: endSegmentRefs, + }); + } else { + return getFirstEmptySegment({ + formatParts, + segmentRefs: startSegmentRefs, + }); + } + } + } +}; diff --git a/packages/date-picker/src/utils/getFirstEmptySegment/index.ts b/packages/date-picker/src/utils/getFirstEmptySegment/index.ts new file mode 100644 index 0000000000..1f95d6a2ac --- /dev/null +++ b/packages/date-picker/src/utils/getFirstEmptySegment/index.ts @@ -0,0 +1,27 @@ +import { DatePickerContextProps } from '../../DatePickerContext'; +import { DateSegment } from '../../hooks/useDateSegments'; +import { SegmentRefs } from '../../hooks/useSegmentRefs'; + +/** + * + * @returns The first empty date segment for the given format + */ +export const getFirstEmptySegment = ({ + formatParts, + segmentRefs, +}: { + formatParts: Required['formatParts']; + segmentRefs: SegmentRefs; +}) => { + // if 1+ are empty, focus the first empty one + const formatSegments = formatParts.filter(part => part.type !== 'literal'); + const emptySegmentKeys = formatSegments + .map(p => p.type) + .filter(type => { + const element = segmentRefs[type as DateSegment]; + return !element?.current?.value; + }); + const firstEmptySegmentKey = emptySegmentKeys[0] as DateSegment; + const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; + return firstEmptySegmentRef.current; +}; diff --git a/packages/date-picker/src/utils/index.ts b/packages/date-picker/src/utils/index.ts index 5a54d18b0e..c45173d610 100644 --- a/packages/date-picker/src/utils/index.ts +++ b/packages/date-picker/src/utils/index.ts @@ -1,5 +1,6 @@ export { cloneReverse } from './cloneReverse'; export { getDaysInUTCMonth } from './getDaysInUTCMonth'; +export { getFirstEmptySegment } from './getFirstEmptySegment'; export { getFirstOfMonth } from './getFirstOfMonth'; export { getFormatParts } from './getFormatParts'; export { getFullMonthLabel } from './getFullMonthLabel'; From 84f0eb017abb078aeb48f7ea13a1793bce80e2a8 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 3 Oct 2023 19:22:36 -0400 Subject: [PATCH 173/351] scaffolds keydown behavior --- .../DatePickerInput/DatePickerInput.tsx | 4 +- .../DatePickerInput/DatePickerInput.types.ts | 4 +- .../DateRangeInput/DateRangeInput.tsx | 59 ++++++++++++++++++- .../DateRangeInput/DateRangeInput.types.ts | 5 +- .../utils/getRelativeRangeSegment/index.ts | 3 + packages/date-picker/src/utils/index.ts | 1 + .../utils/isElementInputSegment/index.ts | 2 +- 7 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts rename packages/date-picker/src/{DatePicker => }/utils/isElementInputSegment/index.ts (86%) diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 9370cdbc00..78ca1497ea 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -12,9 +12,9 @@ import { DateFormField } from '../../DateInput/DateFormField'; import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; import { isZeroLike } from '../../utils'; +import { isElementInputSegment } from '../../utils/isElementInputSegment'; import { getRelativeSegment } from '../utils/getRelativeSegment'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; -import { isElementInputSegment } from '../utils/isElementInputSegment'; import { DatePickerInputProps } from './DatePickerInput.types'; @@ -54,9 +54,9 @@ export const DatePickerInput = forwardRef( const handleKeyDown: KeyboardEventHandler = e => { const { target: _target, key } = e; const target = _target as HTMLElement; - // if target is not a segment, do nothing const isSegment = isElementInputSegment(target, segmentRefs); + // if target is not a segment, do nothing if (!isSegment) return; const isInputEmpty = isZeroLike(target.value); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts index 408cbec601..2dd51e906d 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts @@ -8,8 +8,8 @@ import { DateInputBoxProps } from '../../DateInput'; import { DatePickerProps } from '../DatePicker.types'; export interface DatePickerInputProps - extends Pick, - Pick, + extends Pick, + Pick, HTMLElementProps<'div'> { /** * Click handler diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index b6e401916c..0fe07cf4f4 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -1,10 +1,18 @@ -import React, { forwardRef, MouseEventHandler } from 'react'; +import React, { + forwardRef, + KeyboardEventHandler, + MouseEventHandler, +} from 'react'; + +import { keyMap } from '@leafygreen-ui/lib'; import { DateInputBox } from '../../DateInput'; import { DateFormField } from '../../DateInput/DateFormField'; import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; +import { isElementInputSegment, isZeroLike } from '../../utils'; import { getRangeSegmentToFocus } from '../utils/getRangeSegmentToFocus'; +import { getRelativeRangeSegment } from '../utils/getRelativeRangeSegment'; import { inputWrapperStyles } from './DateRangeInput.styles'; import { DateRangeInputProps } from './DateRangeInput.types'; @@ -12,7 +20,7 @@ import { DateRangeInputProps } from './DateRangeInput.types'; const EN_DASH = '–'; export const DateRangeInput = forwardRef( - (_props: DateRangeInputProps, fwdRef) => { + ({ start, end, handleValidation }: DateRangeInputProps, fwdRef) => { const { disabled, formatParts, setOpen } = useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); @@ -33,6 +41,53 @@ export const DateRangeInput = forwardRef( segmentToFocus?.focus(); }; + const handleKeyDown: KeyboardEventHandler = e => { + const { target: _target, key } = e; + const target = _target as HTMLElement; + const isSegment = + isElementInputSegment(target, startSegmentRefs) || + isElementInputSegment(target, endSegmentRefs); + + // if target is not a segment, do nothing + if (!isSegment) return; + + const isInputEmpty = isZeroLike(target.value); + const cursorPosition = target.selectionEnd; + + switch (key) { + case keyMap.ArrowLeft: + // TODO: + getRelativeRangeSegment(); + break; + case keyMap.ArrowRight: + // TODO: + break; + case keyMap.ArrowDown: + // TODO: + break; + case keyMap.ArrowUp: + // TODO: + break; + case keyMap.Backspace: { + // TODO: + break; + } + + case keyMap.Enter: + handleValidation?.([start || null, end || null]); + break; + + case keyMap.Escape: + setOpen(false); + handleValidation?.([start || null, end || null]); + break; + + default: + // any other keydown should open the menu + setOpen(true); + } + }; + return (
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts index fdbfeeb41d..003152f17d 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts @@ -1 +1,4 @@ -export interface DateRangeInputProps {} +import { DateRangePickerProps } from '../DateRangePicker.types'; + +export interface DateRangeInputProps + extends Pick {} diff --git a/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts b/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts new file mode 100644 index 0000000000..b0228d89a8 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts @@ -0,0 +1,3 @@ +export const getRelativeRangeSegment = () => { + //TODO: +}; diff --git a/packages/date-picker/src/utils/index.ts b/packages/date-picker/src/utils/index.ts index c45173d610..ae08549a5a 100644 --- a/packages/date-picker/src/utils/index.ts +++ b/packages/date-picker/src/utils/index.ts @@ -12,6 +12,7 @@ export { getUTCDateString } from './getUTCDateString'; export { getValueFormatter } from './getValueFormatter'; export { getWeeksArray } from './getWeeksArray'; export { isCurrentUTCDay } from './isCurrentUTCDay'; +export { isElementInputSegment } from './isElementInputSegment'; export { isSameTZDay } from './isSameTZDay'; export { isSameUTCDay } from './isSameUTCDay'; export { isSameUTCMonth } from './isSameUTCMonth'; diff --git a/packages/date-picker/src/DatePicker/utils/isElementInputSegment/index.ts b/packages/date-picker/src/utils/isElementInputSegment/index.ts similarity index 86% rename from packages/date-picker/src/DatePicker/utils/isElementInputSegment/index.ts rename to packages/date-picker/src/utils/isElementInputSegment/index.ts index ffa8e37856..28ac985fb7 100644 --- a/packages/date-picker/src/DatePicker/utils/isElementInputSegment/index.ts +++ b/packages/date-picker/src/utils/isElementInputSegment/index.ts @@ -1,4 +1,4 @@ -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { SegmentRefs } from '../../hooks/useSegmentRefs'; /** * Returns whether the given element is a segment From 8607e562359f09e5dfe4fb5205ecbb919d95a12f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 13:43:25 -0400 Subject: [PATCH 174/351] add darkMode prop. Update 'unset' -> 'default' --- packages/form-field/src/FormField.stories.tsx | 2 +- packages/form-field/src/FormField/FormField.tsx | 8 ++++++-- packages/form-field/src/FormField/FormField.types.ts | 7 ++++--- .../form-field/src/FormFieldContext/FormFieldContext.tsx | 2 +- .../src/FormFieldInput/FormFieldInput.styles.ts | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 6b0120c81b..eb04c42372 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -61,7 +61,7 @@ const meta: StoryMetaType = { description: 'Description', errorMessage: 'This is a notification', size: Size.Default, - state: FormFieldState.Unset, + state: FormFieldState.Default, glyph: 'Beaker', }, argTypes: { diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 3b59c6c4df..2af8ac070f 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -34,11 +34,12 @@ export const FormField = forwardRef( label, description, children, - state = FormFieldState.Unset, + state = FormFieldState.Default, size = Size.Default, disabled = false, errorMessage, className, + darkMode, ...rest }: FormFieldProps, fwdRef, @@ -51,7 +52,10 @@ export const FormField = forwardRef( useFormFieldProps({ label, description, state, ...rest }); return ( - +
, 'children'> & - AriaLabelProps & { + AriaLabelProps & + DarkModeProps & { children: FormFieldChildren; description?: React.ReactNode; state?: FormFieldState; diff --git a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx index 08ea16798c..de9e706bff 100644 --- a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -15,7 +15,7 @@ export interface FormFieldContextProps { export const defaultFormFieldContext = { disabled: false, size: Size.Default, - state: FormFieldState.Unset, + state: FormFieldState.Default, }; export const FormFieldContext = React.createContext( diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts index 58b54130c9..8b50bb2db9 100644 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts @@ -231,7 +231,7 @@ export const inputWrapperStateStyles: Record< } `, }, - [FormFieldState.Unset]: { + [FormFieldState.Default]: { [Theme.Light]: css``, [Theme.Dark]: css``, }, From 05f9f9e41e50695d843b800dc1f1bcc3e78d91d3 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 13:48:59 -0400 Subject: [PATCH 175/351] rm Valid state from SB --- packages/form-field/src/FormField.stories.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index eb04c42372..8adefb293d 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; import { StoryFn } from '@storybook/react'; +import { omit } from 'lodash'; import Button from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; @@ -33,7 +34,7 @@ const meta: StoryMetaType = { description: [undefined, 'Description'], icon: [undefined, ], size: Object.values(Size), - state: Object.values(FormFieldState), + state: omit(Object.values(FormFieldState), 'valid'), disabled: [false, true], }, excludeCombinations: [ @@ -70,7 +71,10 @@ const meta: StoryMetaType = { description: { control: 'text' }, errorMessage: { control: 'text' }, size: { control: 'select' }, - state: { control: 'select' }, + state: { + control: 'select', + options: omit(Object.values(FormFieldState), 'valid'), + }, glyph: { control: 'select', options: Object.keys(glyphs) }, }, }; From 125bdaaea3db324619925cafc32d23ebacabbdf0 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 13:51:29 -0400 Subject: [PATCH 176/351] update Default -> None --- packages/form-field/src/FormField.stories.tsx | 2 +- packages/form-field/src/FormField/FormField.tsx | 2 +- packages/form-field/src/FormField/FormField.types.ts | 2 +- packages/form-field/src/FormFieldContext/FormFieldContext.tsx | 2 +- packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 8adefb293d..a0a1c3f6d4 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -62,7 +62,7 @@ const meta: StoryMetaType = { description: 'Description', errorMessage: 'This is a notification', size: Size.Default, - state: FormFieldState.Default, + state: FormFieldState.None, glyph: 'Beaker', }, argTypes: { diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 2af8ac070f..399f5ef1e4 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -34,7 +34,7 @@ export const FormField = forwardRef( label, description, children, - state = FormFieldState.Default, + state = FormFieldState.None, size = Size.Default, disabled = false, errorMessage, diff --git a/packages/form-field/src/FormField/FormField.types.ts b/packages/form-field/src/FormField/FormField.types.ts index c0e7b1abf7..6c8284ce01 100644 --- a/packages/form-field/src/FormField/FormField.types.ts +++ b/packages/form-field/src/FormField/FormField.types.ts @@ -2,7 +2,7 @@ import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; export const FormFieldState = { - Default: 'default', + None: 'none', Error: 'error', Valid: 'valid', } as const; diff --git a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx index de9e706bff..99339d0073 100644 --- a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -15,7 +15,7 @@ export interface FormFieldContextProps { export const defaultFormFieldContext = { disabled: false, size: Size.Default, - state: FormFieldState.Default, + state: FormFieldState.None, }; export const FormFieldContext = React.createContext( diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts index 8b50bb2db9..664fd69f12 100644 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts +++ b/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts @@ -231,7 +231,7 @@ export const inputWrapperStateStyles: Record< } `, }, - [FormFieldState.Default]: { + [FormFieldState.None]: { [Theme.Light]: css``, [Theme.Dark]: css``, }, From 882b76dfd09b92c91b1196650e36974445731e2e Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 13:52:32 -0400 Subject: [PATCH 177/351] fix build --- packages/form-field/src/FormField.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index a0a1c3f6d4..f0c3e3ac6b 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; import { StoryFn } from '@storybook/react'; -import { omit } from 'lodash'; +import omit from 'lodash/omit'; import Button from '@leafygreen-ui/button'; import { css } from '@leafygreen-ui/emotion'; From 8a6c427f1bfb8eb5e86ba8a617d5e392c160dc3d Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 14:10:18 -0400 Subject: [PATCH 178/351] Rename to FormFieldInputContainer --- packages/form-field/src/FormField.stories.tsx | 26 +++++++++++-------- .../src/FormFieldContext/FormFieldContext.tsx | 4 +++ .../FormFieldInput/FormFieldInput.types.ts | 8 ------ .../form-field/src/FormFieldInput/index.ts | 2 -- .../FormFieldInputContainer.styles.ts} | 0 .../FormFieldInputContainer.tsx} | 21 ++++++++++----- .../FormFieldInputContainer.types.ts | 15 +++++++++++ .../src/FormFieldInputContainer/index.ts | 2 ++ packages/form-field/src/index.ts | 5 +++- 9 files changed, 55 insertions(+), 28 deletions(-) delete mode 100644 packages/form-field/src/FormFieldInput/FormFieldInput.types.ts delete mode 100644 packages/form-field/src/FormFieldInput/index.ts rename packages/form-field/src/{FormFieldInput/FormFieldInput.styles.ts => FormFieldInputContainer/FormFieldInputContainer.styles.ts} (100%) rename packages/form-field/src/{FormFieldInput/FormFieldInput.tsx => FormFieldInputContainer/FormFieldInputContainer.tsx} (81%) create mode 100644 packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts create mode 100644 packages/form-field/src/FormFieldInputContainer/index.ts diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index f0c3e3ac6b..5cbd9bab4f 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -13,15 +13,15 @@ import { Size } from '@leafygreen-ui/tokens'; import { FormField, - FormFieldInput, - FormFieldInputProps, + FormFieldInputContainer, + FormFieldInputContainerProps, FormFieldProps, FormFieldState, useFormFieldContext, } from '.'; type FormFieldStoryProps = FormFieldProps & - FormFieldInputProps & { glyph: string }; + FormFieldInputContainerProps & { glyph: string }; const meta: StoryMetaType = { title: 'Components/FormField', @@ -49,9 +49,9 @@ const meta: StoryMetaType = { decorator: (Instance, ctx) => ( - + {ctx?.args.children} - + ), @@ -98,9 +98,13 @@ export const Basic: StoryFn = ({ disabled={disabled} {...rest} > - }> + } + > - + ); @@ -121,7 +125,7 @@ export const WithIconButton: StoryFn = ({ disabled={disabled} {...rest} > - = ({ } > - + ); @@ -140,7 +144,7 @@ export const Custom_TwoIcons: StoryFn = ({ ...props }: FormFieldStoryProps) => ( - = ({ } > - + ); diff --git a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx index 99339d0073..49d448119f 100644 --- a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -33,4 +33,8 @@ export const FormFieldProvider = ({ ); +/** + * Returns {@link FormFieldContextProps} used within the FormFieldInputContainer, + * or within and custom FormField children + */ export const useFormFieldContext = () => useContext(FormFieldContext); diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.types.ts b/packages/form-field/src/FormFieldInput/FormFieldInput.types.ts deleted file mode 100644 index e4af4f5b8d..0000000000 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { HTMLElementProps } from '@leafygreen-ui/lib'; - -import { FormFieldChildren } from '../FormField/FormField.types'; - -export interface FormFieldInputProps extends HTMLElementProps<'div'> { - children: FormFieldChildren; - icon?: React.ReactElement; -} diff --git a/packages/form-field/src/FormFieldInput/index.ts b/packages/form-field/src/FormFieldInput/index.ts deleted file mode 100644 index 290dd4e09b..0000000000 --- a/packages/form-field/src/FormFieldInput/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { FormFieldInput } from './FormFieldInput'; -export { FormFieldInputProps } from './FormFieldInput.types'; diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts similarity index 100% rename from packages/form-field/src/FormFieldInput/FormFieldInput.styles.ts rename to packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts diff --git a/packages/form-field/src/FormFieldInput/FormFieldInput.tsx b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.tsx similarity index 81% rename from packages/form-field/src/FormFieldInput/FormFieldInput.tsx rename to packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.tsx index 69170eacfd..e61eba0a58 100644 --- a/packages/form-field/src/FormFieldInput/FormFieldInput.tsx +++ b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.tsx @@ -21,12 +21,21 @@ import { inputWrapperModeStyles, inputWrapperSizeStyles, inputWrapperStateStyles, -} from './FormFieldInput.styles'; -import { FormFieldInputProps } from './FormFieldInput.types'; +} from './FormFieldInputContainer.styles'; +import { FormFieldInputContainerProps } from './FormFieldInputContainer.types'; -/** Applies styling around the input of a FormField element */ -export const FormFieldInput = forwardRef( - ({ icon, className, children, ...rest }: FormFieldInputProps, fwdRef) => { +/** + * Applies styling around the `input` of a FormField element + * @internal + */ +export const FormFieldInputContainer = forwardRef< + HTMLDivElement, + FormFieldInputContainerProps +>( + ( + { icon, className, children, ...rest }: FormFieldInputContainerProps, + fwdRef, + ) => { const { theme } = useDarkMode(); const { disabled, size, state, inputProps } = useFormFieldContext(); @@ -77,4 +86,4 @@ export const FormFieldInput = forwardRef( }, ); -FormFieldInput.displayName = 'FormFieldInputWrapper'; +FormFieldInputContainer.displayName = 'FormFieldInputWrapper'; diff --git a/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts new file mode 100644 index 0000000000..6188b8dc2d --- /dev/null +++ b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts @@ -0,0 +1,15 @@ +import { HTMLElementProps } from '@leafygreen-ui/lib'; + +import { FormFieldChildren } from '../FormField/FormField.types'; + +export interface FormFieldInputContainerProps extends HTMLElementProps<'div'> { + /** + * Must pass a single element in order to label the input element appropriately + */ + children: FormFieldChildren; + + /** + * The icon rendered on the right of the input box + */ + icon?: React.ReactElement; +} diff --git a/packages/form-field/src/FormFieldInputContainer/index.ts b/packages/form-field/src/FormFieldInputContainer/index.ts new file mode 100644 index 0000000000..b27aa2c610 --- /dev/null +++ b/packages/form-field/src/FormFieldInputContainer/index.ts @@ -0,0 +1,2 @@ +export { FormFieldInputContainer } from './FormFieldInputContainer'; +export { FormFieldInputContainerProps } from './FormFieldInputContainer.types'; diff --git a/packages/form-field/src/index.ts b/packages/form-field/src/index.ts index 02e7140128..1f871bb92b 100644 --- a/packages/form-field/src/index.ts +++ b/packages/form-field/src/index.ts @@ -12,4 +12,7 @@ export { FormFieldProvider, useFormFieldContext, } from './FormFieldContext'; -export { FormFieldInput, type FormFieldInputProps } from './FormFieldInput'; +export { + FormFieldInputContainer, + type FormFieldInputContainerProps, +} from './FormFieldInputContainer'; From e932e7fc1592df6d2f13e3c6968b55bd7490ffe5 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 14:56:40 -0400 Subject: [PATCH 179/351] fix tests --- packages/form-field/src/FormField.spec.tsx | 80 ++++++++++--------- .../src/FormFieldContext/FormFieldContext.tsx | 2 +- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/form-field/src/FormField.spec.tsx b/packages/form-field/src/FormField.spec.tsx index 5375b56ef5..336f8efd70 100644 --- a/packages/form-field/src/FormField.spec.tsx +++ b/packages/form-field/src/FormField.spec.tsx @@ -5,7 +5,7 @@ import Icon from '@leafygreen-ui/icon'; import { FormField, - FormFieldInput, + FormFieldInputContainer, FormFieldState, useFormFieldContext, } from '.'; @@ -14,9 +14,9 @@ describe('packages/form-field', () => { test('rest passed to outer element', () => { const { getByTestId } = render( - +
- + , ); const formField = getByTestId('form-field'); @@ -26,9 +26,9 @@ describe('packages/form-field', () => { test('className passed to outer element', () => { const { getByTestId } = render( - +
- + , ); const formField = getByTestId('form-field'); @@ -42,9 +42,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > - +
- + , ); const description = getByText('Description'); @@ -58,9 +58,9 @@ describe('packages/form-field', () => { description={description} data-testid="form-field" > - +
- + , ); const descriptionSpan = queryByTestId('description-span'); @@ -71,9 +71,9 @@ describe('packages/form-field', () => { test('input has id,', () => { const { getByTestId } = render( - +
- + , ); const input = getByTestId('input'); @@ -84,9 +84,9 @@ describe('packages/form-field', () => { test('label element has id & htmlFor', () => { const { getByText } = render( - +
- + , ); const label = getByText('Label'); @@ -100,9 +100,9 @@ describe('packages/form-field', () => { label={Label} data-testid="form-field" > - +
- + , ); const labelSpan = queryByTestId('label-span'); @@ -117,9 +117,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > - +
- + , ); const label = getByText('Label'); @@ -134,9 +134,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > - +
- + , ); const labelSpan = queryByTestId('label-span'); @@ -154,9 +154,9 @@ describe('packages/form-field', () => { description="Description" data-testid="form-field" > - +
- + , ); const description = getByText('Description'); @@ -171,9 +171,9 @@ describe('packages/form-field', () => { description={description} data-testid="form-field" > - +
- + , ); const descriptionSpan = queryByTestId('description-span'); @@ -187,9 +187,9 @@ describe('packages/form-field', () => { test('when aria-label is provided, input has that aria-label', () => { const { getByTestId } = render( - +
- + , ); const input = getByTestId('input'); @@ -199,9 +199,9 @@ describe('packages/form-field', () => { test('when aria-labelledby is provided, input has that aria-labelledby', () => { const { getByTestId } = render( - +
- + , ); const input = getByTestId('input'); @@ -217,9 +217,9 @@ describe('packages/form-field', () => { errorMessage="This is an error message" data-testid="form-field" > - +
- + , ); const error = queryByText('This is an error message'); @@ -234,9 +234,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > - +
- + , ); const error = queryByText('This is an error message'); @@ -251,9 +251,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > - +
- + , ); const error = queryByText('This is an error message'); @@ -268,9 +268,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > - +
- + , ); const input = getByTestId('input'); @@ -287,9 +287,9 @@ describe('packages/form-field', () => { state={FormFieldState.Error} data-testid="form-field" > - +
- + , ); const input = getByTestId('input'); @@ -305,9 +305,11 @@ describe('packages/form-field', () => { test('Renders an icon', () => { const { queryByTestId } = render( - }> + } + >
- + , ); diff --git a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx index 49d448119f..28fb425f18 100644 --- a/packages/form-field/src/FormFieldContext/FormFieldContext.tsx +++ b/packages/form-field/src/FormFieldContext/FormFieldContext.tsx @@ -34,7 +34,7 @@ export const FormFieldProvider = ({ ); /** - * Returns {@link FormFieldContextProps} used within the FormFieldInputContainer, + * Returns {@link FormFieldContextProps} to be used within the FormFieldInputContainer, * or within and custom FormField children */ export const useFormFieldContext = () => useContext(FormFieldContext); From 669499c610e938c3dcd97ab015cb6a4b7a7c9379 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:08:23 -0400 Subject: [PATCH 180/351] Refactors re FormField changes --- .../DateInput/DateFormField/DateFormField.stories.tsx | 8 ++++---- .../src/DateInput/DateFormField/DateFormField.tsx | 6 +++--- .../src/DateInput/DateFormField/DateFormField.types.ts | 7 ------- .../date-picker/src/DateInput/DateFormField/index.ts | 5 +---- .../src/DatePickerContext/DatePickerContext.utils.ts | 4 ++-- packages/date-picker/src/types.ts | 9 ++++++++- 6 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx index da70bcd793..0125736668 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { StoryFn } from '@storybook/react'; import { css } from '@leafygreen-ui/emotion'; -import { FormFieldState } from '@leafygreen-ui/form-field'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; @@ -11,6 +10,7 @@ import { DatePickerContextProps, DatePickerProvider, } from '../../DatePickerContext'; +import { DatePickerState } from '../../types'; import { DateFormField } from './DateFormField'; @@ -30,7 +30,7 @@ const meta: StoryMetaType< darkMode: [false, true], label: ['Label', undefined], description: [undefined, 'Description'], - state: ['unset', 'error'], + state: Object.values(DatePickerState), disabled: [false, true], }, excludeCombinations: [ @@ -72,7 +72,7 @@ const meta: StoryMetaType< args: { label: 'Label', description: 'Description', - state: 'error', + state: DatePickerState.Error, errorMessage: 'This is an error message', }, argTypes: { @@ -88,7 +88,7 @@ const Template: StoryFn = () => { value={{ label: 'Label', description: 'Description', - state: FormFieldState.Error, + state: DatePickerState.Error, errorMessage: 'This is an error message', }} > diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx index e07f021ddf..578682c8f7 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FormField, FormFieldInput } from '@leafygreen-ui/form-field'; +import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; import { useDatePickerContext } from '../../DatePickerContext'; import { CalendarButton } from '../CalendarButton'; @@ -36,7 +36,7 @@ export const DateFormField = React.forwardRef< errorMessage={errorMessage} {...rest} > - } > {children} - + ); }, diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts b/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts index 1743e5df11..e1c4414330 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts @@ -3,13 +3,6 @@ import { MouseEventHandler } from 'react'; import { FormFieldProps } from '@leafygreen-ui/form-field'; import { HTMLElementProps } from '@leafygreen-ui/lib'; -export const InputState = { - Unset: 'unset', - Error: 'error', -} as const; - -export type InputState = (typeof InputState)[keyof typeof InputState]; - export type DateFormFieldProps = HTMLElementProps<'div'> & { children: FormFieldProps['children']; /** Callback fired when the input is clicked */ diff --git a/packages/date-picker/src/DateInput/DateFormField/index.ts b/packages/date-picker/src/DateInput/DateFormField/index.ts index b5d07bcf35..6c423345b5 100644 --- a/packages/date-picker/src/DateInput/DateFormField/index.ts +++ b/packages/date-picker/src/DateInput/DateFormField/index.ts @@ -1,5 +1,2 @@ export { DateFormField } from './DateFormField'; -export { - type DateFormFieldProps, - type InputState, -} from './DateFormField.types'; +export { type DateFormFieldProps } from './DateFormField.types'; diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts index 092e69b433..cce88a5463 100644 --- a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts @@ -4,7 +4,7 @@ import defaultTo from 'lodash/defaultTo'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; -import { BaseDatePickerProps } from '../types'; +import { BaseDatePickerProps, DatePickerState } from '../types'; import { getFormatParts, toDate } from '../utils'; import { @@ -50,7 +50,7 @@ export const defaultDatePickerContext: DatePickerContextProps = { isInRange: () => true, disabled: false, size: Size.Default, - state: 'unset', + state: DatePickerState.None, errorMessage: '', baseFontSize: BaseFontSize.Body1, darkMode: false, diff --git a/packages/date-picker/src/types.ts b/packages/date-picker/src/types.ts index 0cb518743b..7972a5d2b5 100644 --- a/packages/date-picker/src/types.ts +++ b/packages/date-picker/src/types.ts @@ -1,6 +1,13 @@ +import { omit } from 'lodash'; + +import { FormFieldState } from '@leafygreen-ui/form-field'; import { DarkModeProps } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; +export const DatePickerState = omit(FormFieldState, 'Valid'); +export type DatePickerState = + (typeof DatePickerState)[keyof typeof DatePickerState]; + export type DateType = Date | null; export interface BaseDatePickerProps extends DarkModeProps { @@ -61,7 +68,7 @@ export interface BaseDatePickerProps extends DarkModeProps { /** * Whether to show an error message */ - state?: 'unset' | 'error'; + state?: DatePickerState; /** * A message to show in red underneath the input when in an error state From 8120eab1e77d0df96344027087cb86a0ad767d3c Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:27:19 -0400 Subject: [PATCH 181/351] scaffolds range menu --- .../DatePickerInput.stories.tsx | 83 ++++++ .../DateRangeInput/DateRangeInput.tsx | 6 +- .../DateRangeMenu/DateRangeMenu.spec.tsx | 11 + .../DateRangeMenu/DateRangeMenu.stories.tsx | 239 ++++++++++++++++++ .../DateRangeMenu/DateRangeMenu.styles.ts | 4 + .../DateRangeMenu/DateRangeMenu.tsx | 9 + .../DateRangeMenu/DateRangeMenu.types.ts | 1 + .../DateRangePicker/DateRangeMenu/index.ts | 3 + 8 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx new file mode 100644 index 0000000000..c7a04a2f1e --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx @@ -0,0 +1,83 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect, useState } from 'react'; +import { StoryFn } from '@storybook/react'; +import { isValid } from 'date-fns'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import { StoryMetaType } from '@leafygreen-ui/lib'; +import { Size } from '@leafygreen-ui/tokens'; + +import { + DatePickerContextProps, + DatePickerProvider, +} from '../../DatePickerContext'; + +import { DateRangeInput } from './DateRangeInput'; + +const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( + + + + + +); + +const meta: StoryMetaType = { + title: 'Components/DatePicker/Range/DateRangeInput', + component: DateRangeInput, + decorators: [ProviderWrapper], + parameters: { + default: null, + controls: { + exclude: ['segmentRefs'], + }, + generate: { + combineArgs: { + darkMode: [false, true], + // value: [null, new Date('1993-12-26')], + dateFormat: ['iso8601', 'en-US', 'en-UK', 'de-DE'], + size: Object.values(Size), + }, + decorator: ProviderWrapper, + }, + }, + args: { + label: 'Label', + dateFormat: 'en-UK', + timeZone: 'Europe/London', + }, + argTypes: { + // value: { control: 'date' }, + }, +}; + +export default meta; + +export const Basic: StoryFn = props => { + const [date, setDate] = useState(null); + + // useEffect(() => { + // if (props.value && isValid(new Date(props.value))) { + // setDate(new Date(props.value)); + // } + // }, [props.value]); + + const updateDate = (date: Date | null) => { + setDate(date); + }; + + return ( + <> + + + ); +}; + +export const Generated = () => {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index 0fe07cf4f4..fd26e76d13 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -89,7 +89,11 @@ export const DateRangeInput = forwardRef( }; return ( - +
{EN_DASH} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx new file mode 100644 index 0000000000..f14bc52f79 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx @@ -0,0 +1,11 @@ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { DateRangeMenu } from '.'; + +describe('packages/date-range-menu', () => { + test('condition', () => { + + }) +}) diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx new file mode 100644 index 0000000000..88fe41766d --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx @@ -0,0 +1,239 @@ +/* eslint-disable react-hooks/rules-of-hooks, react/prop-types */ +import React, { useRef, useState } from 'react'; +import { StoryFn, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; +import { last, omit } from 'lodash'; +import MockDate from 'mockdate'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import { StoryMetaType } from '@leafygreen-ui/lib'; +import { transitionDuration } from '@leafygreen-ui/tokens'; +import { InlineCode } from '@leafygreen-ui/typography'; + +import { Month } from '../../constants'; +import { + DatePickerContextProps, + DatePickerProvider, +} from '../../DatePickerContext'; +import { + contextPropNames, + defaultDatePickerContext, +} from '../../DatePickerContext/DatePickerContext.utils'; +import { newUTC, pickAndOmit } from '../../utils'; + +import { DateRangeMenu } from './DateRangeMenu'; +import { DateRangeMenuProps } from './DateRangeMenu.types'; + +const mockToday = newUTC(2023, Month.September, 14); +type DecoratorArgs = DateRangeMenuProps & DatePickerContextProps; + +const MenuDecorator = (Story: StoryFn, ctx: any) => { + const [{ darkMode, ...contextProps }, { ...props }] = pickAndOmit( + ctx?.args as DecoratorArgs, + [...contextPropNames], + ); + + // Force `new Date()` to return `mockToday` + MockDate.set(mockToday); + + return ( + + + + + + ); +}; + +const meta: StoryMetaType = { + title: 'Components/DatePicker/Range/DateRangeMenu', + component: DateRangeMenu, + decorators: [MenuDecorator], + parameters: { + default: null, + chromatic: { + delay: transitionDuration.slower, + }, + }, + args: { + isOpen: true, + dateFormat: 'en-UK', + timeZone: 'Europe/London', + min: new Date('1996-10-14'), + max: new Date('2026-10-14'), + }, + argTypes: { + value: { control: 'date' }, + }, +}; + +export default meta; + +type DateRangeMenuStoryType = StoryObj; + +export const Basic: DateRangeMenuStoryType = { + render: args => { + const [value, setValue] = useState(null); + + const props = omit(args, [...contextPropNames, 'isOpen']); + const refEl = useRef(null); + return ( + <> + refEl + + + ); + }, +}; + +export const WithValue: DateRangeMenuStoryType = { + render: args => { + const props = omit(args, [...contextPropNames, 'isOpen']); + const refEl = useRef(null); + return ( +
+ refEl + {}} + /> +
+ ); + }, +}; + +export const DarkMode: DateRangeMenuStoryType = { + ...WithValue, + args: { + // @ts-expect-error - DateRangeMenuStoryType does not include Context props + darkMode: true, + }, +}; + +/** + * Chromatic Interaction tests + */ +// TODO: + +// type DateRangeMenuInteractionTestType = Omit & +// Required>; + +// export const InitialFocusToday: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// const { findByRole } = within(ctx.canvasElement.parentElement!); +// await findByRole('listbox'); +// userEvent.tab(); +// }, +// }; +// export const InitialFocusValue: DateRangeMenuInteractionTestType = { +// ...WithValue, +// play: async ctx => { +// const { findByRole } = within(ctx.canvasElement.parentElement!); +// await findByRole('listbox'); +// userEvent.tab(); +// }, +// }; + +// export const LeftArrowKey: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowleft}'); +// }, +// }; + +// export const RightArrowKey: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowright}'); +// }, +// }; + +// export const UpArrowKey: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowup}'); +// }, +// }; + +// export const DownArrowKey: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowdown}'); +// }, +// }; + +// export const UpToPrevMonth: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowup}{arrowup}'); +// }, +// }; + +// export const DownToNextMonth: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await InitialFocusToday.play(ctx); +// userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}'); +// }, +// }; + +// export const OpenMonthMenu: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// const canvas = within(ctx.canvasElement.parentElement!); +// await canvas.findByRole('listbox'); +// const monthMenu = await canvas.findByLabelText('Select month'); +// userEvent.click(monthMenu); +// }, +// }; + +// export const SelectJanuary: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await OpenMonthMenu.play(ctx); +// const { findAllByRole } = within(ctx.canvasElement.parentElement!); +// const options = await findAllByRole('option'); +// const Jan = options[0]; +// userEvent.click(Jan); +// }, +// }; + +// export const OpenYearMenu: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// const canvas = within(ctx.canvasElement.parentElement!); +// await canvas.findByRole('listbox'); +// const monthMenu = await canvas.findByLabelText('Select year'); +// userEvent.click(monthMenu); +// }, +// }; + +// export const Select2026: DateRangeMenuInteractionTestType = { +// ...Basic, +// play: async ctx => { +// await OpenYearMenu.play(ctx); +// const { findAllByRole } = within(ctx.canvasElement.parentElement!); +// const options = await findAllByRole('option'); +// const _2026 = last(options); +// userEvent.click(_2026!); +// }, +// }; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts new file mode 100644 index 0000000000..928608f58d --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts @@ -0,0 +1,4 @@ + +import { css } from '@leafygreen-ui/emotion'; + +export const baseStyles = css``; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx new file mode 100644 index 0000000000..d65e2b0ff6 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { DateRangeMenuProps } from './DateRangeMenu.types'; + +// TODO: forwardRef +export function DateRangeMenu({}: DateRangeMenuProps) { + return
your content here
; +} + +DateRangeMenu.displayName = 'DateRangeMenu'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts new file mode 100644 index 0000000000..ceb9ef6f49 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts @@ -0,0 +1 @@ +export interface DateRangeMenuProps {} \ No newline at end of file diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts new file mode 100644 index 0000000000..cf65c781fb --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts @@ -0,0 +1,3 @@ + +export { DateRangeMenu } from './DateRangeMenu'; +export { DateRangeMenuProps } from './DateRangeMenu.types'; From 00453d9135edd8573f450bae0316e9a716bb8746 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:31:58 -0400 Subject: [PATCH 182/351] renames icon -> contentEnd --- packages/form-field/src/FormField.stories.tsx | 41 ++++++++++++++++--- .../FormFieldInputContainer.tsx | 8 ++-- .../FormFieldInputContainer.types.ts | 4 +- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 5cbd9bab4f..564f851eee 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -32,7 +32,11 @@ const meta: StoryMetaType = { combineArgs: { darkMode: [false, true], description: [undefined, 'Description'], - icon: [undefined, ], + contentEnd: [ + undefined, + , + Optional, + ], size: Object.values(Size), state: omit(Object.values(FormFieldState), 'valid'), disabled: [false, true], @@ -49,7 +53,7 @@ const meta: StoryMetaType = { decorator: (Instance, ctx) => ( - + {ctx?.args.children} @@ -101,7 +105,34 @@ export const Basic: StoryFn = ({ } + contentEnd={} + > + + + +); + +export const WithOptionalText: StoryFn = ({ + label, + description, + state, + size, + disabled, + glyph: _, + ...rest +}: FormFieldStoryProps) => ( + + Optional} > @@ -128,7 +159,7 @@ export const WithIconButton: StoryFn = ({ @@ -147,7 +178,7 @@ export const Custom_TwoIcons: StoryFn = ({ ( ( - { icon, className, children, ...rest }: FormFieldInputContainerProps, + { contentEnd, className, children, ...rest }: FormFieldInputContainerProps, fwdRef, ) => { const { theme } = useDarkMode(); @@ -71,12 +71,12 @@ export const FormFieldInputContainer = forwardRef< className={errorIconStyles[theme]} /> )} - {icon && - React.cloneElement(icon, { + {contentEnd && + React.cloneElement(contentEnd, { className: cx( iconClassName, iconStyles[theme], - icon.props.className, + contentEnd.props.className, ), disabled, })} diff --git a/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts index 6188b8dc2d..1217028e3c 100644 --- a/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts +++ b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.types.ts @@ -9,7 +9,7 @@ export interface FormFieldInputContainerProps extends HTMLElementProps<'div'> { children: FormFieldChildren; /** - * The icon rendered on the right of the input box + * The content rendered after the children */ - icon?: React.ReactElement; + contentEnd?: React.ReactElement; } From 6fd9da0e58ea21bf7974ea3b92c8f6357cf634b6 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:35:04 -0400 Subject: [PATCH 183/351] Update FormField.stories.tsx --- packages/form-field/src/FormField.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 564f851eee..3d6c7217a3 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -38,7 +38,7 @@ const meta: StoryMetaType = { Optional, ], size: Object.values(Size), - state: omit(Object.values(FormFieldState), 'valid'), + state: Object.values(omit(FormFieldState, 'Valid')), disabled: [false, true], }, excludeCombinations: [ From da1297b676a5dc173bd95b474bc530cb02716fe0 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:35:33 -0400 Subject: [PATCH 184/351] Update FormField.stories.tsx --- packages/form-field/src/FormField.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/form-field/src/FormField.stories.tsx b/packages/form-field/src/FormField.stories.tsx index 3d6c7217a3..14e793df9e 100644 --- a/packages/form-field/src/FormField.stories.tsx +++ b/packages/form-field/src/FormField.stories.tsx @@ -77,7 +77,7 @@ const meta: StoryMetaType = { size: { control: 'select' }, state: { control: 'select', - options: omit(Object.values(FormFieldState), 'valid'), + options: Object.values(omit(FormFieldState, 'Valid')), }, glyph: { control: 'select', options: Object.keys(glyphs) }, }, From 6cc638a363c0b8f46e4a29e790a506e80aae0925 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:41:38 -0400 Subject: [PATCH 185/351] Adds Base Font Size --- .../src/FormField/FormField.types.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/form-field/src/FormField/FormField.types.ts b/packages/form-field/src/FormField/FormField.types.ts index 6c8284ce01..35ed97fe5e 100644 --- a/packages/form-field/src/FormField/FormField.types.ts +++ b/packages/form-field/src/FormField/FormField.types.ts @@ -1,5 +1,5 @@ import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib'; -import { Size } from '@leafygreen-ui/tokens'; +import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; export const FormFieldState = { None: 'none', @@ -23,28 +23,67 @@ export type FormFieldChildren = React.ReactElement; type AriaLabelProps = | { + /** + * The label rendered before the input + */ label: React.ReactNode; 'aria-label'?: string; 'aria-labelledby'?: string; } | { label?: React.ReactNode; + + /** + * A label for screen readers + */ 'aria-label': string; 'aria-labelledby'?: string; } | { label?: React.ReactNode; 'aria-label'?: string; + + /** + * A reference to an external label element + */ 'aria-labelledby': string; }; export type FormFieldProps = Omit, 'children'> & AriaLabelProps & DarkModeProps & { + /** + * `FormFieldInputContainer` component, or other custom input component + */ children: FormFieldChildren; + + /** + * A description for the form field + */ description?: React.ReactNode; + + /** + * The state of the component + */ state?: FormFieldState; + + /** + * The size of the component + */ size?: Size; + + /** + * Defines whether the component is disabled + */ disabled?: boolean; + + /** + * The message to display below the form field when in an error state + */ errorMessage?: React.ReactNode; + + /** + * Base font size of the component. Only effective when `size == 'default'` + */ + baseFontSize?: BaseFontSize; }; From 548e22f898dfae11bb688616b3d5930325694495 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 15:43:59 -0400 Subject: [PATCH 186/351] Update DateFormField.tsx --- .../date-picker/src/DateInput/DateFormField/DateFormField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx index 578682c8f7..9ebaa73e6e 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx @@ -42,7 +42,7 @@ export const DateFormField = React.forwardRef< aria-expanded={isOpen} aria-controls={menuId} onClick={onInputClick} - icon={} + contentEnd={} > {children} From a33f001b7fbed3b5e4fc7201487b28e84c43c7c4 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 18:25:20 -0400 Subject: [PATCH 187/351] major range menu buildout --- packages/date-picker/package.json | 9 +- .../CalendarGrid/CalendarGrid.styles.ts | 4 +- .../Calendar/CalendarGrid/CalendarGrid.tsx | 15 ++- .../src/Calendar/CalendarGrid/index.ts | 1 + packages/date-picker/src/Calendar/index.ts | 7 ++ .../src/DatePicker/DatePicker.stories.tsx | 2 +- .../DatePickerInput.stories.tsx | 2 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 2 +- .../DatePickerMenu/DatePickerMenu.tsx | 1 - .../DatePickerMenu/DatePickerMenu.types.ts | 6 +- .../DatePickerMenuHeader/index.tsx | 13 +-- .../DatePickerContext/DatePickerContext.tsx | 2 +- .../DateRangeComponent/DateRangeComponent.tsx | 2 + .../DateRangeComponent.types.ts | 5 +- .../DatePickerInput.stories.tsx | 2 +- .../DateRangeMenu/DateRangeMenu.stories.tsx | 7 +- .../DateRangeMenu/DateRangeMenu.styles.ts | 13 ++- .../DateRangeMenu/DateRangeMenu.tsx | 47 ++++++++- .../DateRangeMenu/DateRangeMenu.types.ts | 11 ++- .../DateRangeMenuCalendars.styles.ts | 9 ++ .../DateRangeMenuCalendars.tsx | 82 ++++++++++++++++ .../DateRangeMenuCalendars/index.ts | 1 + .../DateRangeMenuContext.tsx | 38 ++++++++ .../DateRangeMenuProvider.tsx | 50 ++++++++++ .../DateRangeMenuContext/index.ts | 9 ++ .../DateRangeMenuFooter.styles.ts | 18 ++++ .../DateRangeMenuFooter.tsx | 22 +++++ .../DateRangeMenuFooter/index.ts | 1 + .../DateRangeMenuQuickSelection.styles.ts | 19 ++++ .../DateRangeMenuQuickSelection.tsx | 96 +++++++++++++++++++ .../QuickRangeButton.stories.tsx | 37 +++++++ .../QuickRangeButton.styles.ts | 55 +++++++++++ .../QuickRangeButton/QuickRangeButton.tsx | 33 +++++++ .../QuickRangeButton/index.ts | 1 + .../DateRangeMenuQuickSelection/index.ts | 1 + .../DateRangePicker.stories.tsx | 2 +- .../src/DateRangePicker/DateRangePicker.tsx | 2 + packages/date-picker/src/constants.ts | 12 +++ packages/date-picker/src/types.ts | 2 +- packages/date-picker/tsconfig.json | 6 ++ packages/hooks/src/index.ts | 2 +- 41 files changed, 602 insertions(+), 47 deletions(-) create mode 100644 packages/date-picker/src/Calendar/index.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/index.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 36efe6e4dd..a1516e5919 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -15,6 +15,7 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/button": "^21.0.7", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/form-field": "^0.1.0", "@leafygreen-ui/hooks": "^8.0.0", @@ -34,6 +35,10 @@ "peerDependencies": { "@leafygreen-ui/leafygreen-provider": "^3.1.6" }, + "devDependencies": { + "mockdate": "^3.0.5", + "timezone-mock": "^1.3.6" + }, "resolutions": { "date-fns/@babel/runtime": "7.22.10", "date-fns-tz/@babel/runtime": "7.22.10" @@ -45,9 +50,5 @@ }, "bugs": { "url": "https://jira.mongodb.org/projects/PD/summary" - }, - "devDependencies": { - "mockdate": "^3.0.5", - "timezone-mock": "^1.3.6" } } diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts index ac75fd189a..ed3e2b3bb1 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts +++ b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts @@ -1,7 +1,9 @@ import { css } from '@leafygreen-ui/emotion'; import { fontWeights } from '@leafygreen-ui/tokens'; -export const baseStyles = css``; +export const calendarGridStyles = css` + height: max-content; +`; export const calendarHeaderCellStyles = css` font-weight: ${fontWeights.regular}; diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx index 8504bc6533..53d8a64f4d 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx @@ -2,20 +2,24 @@ import React, { forwardRef, useMemo } from 'react'; import range from 'lodash/range'; import { getWeekStartByLocale } from 'weekstart'; +import { cx } from '@leafygreen-ui/emotion'; import { Disclaimer } from '@leafygreen-ui/typography'; import { DaysOfWeek, daysPerWeek } from '../../constants'; import { useDatePickerContext } from '../../DatePickerContext'; import { getWeeksArray } from '../../utils'; -import { calendarHeaderCellStyles } from './CalendarGrid.styles'; +import { + calendarGridStyles, + calendarHeaderCellStyles, +} from './CalendarGrid.styles'; import { CalendarGridProps } from './CalendarGrid.types'; /** * A simple table that renders the `CalendarCell` components passed as children */ export const CalendarGrid = forwardRef( - ({ month, children, ...rest }: CalendarGridProps, fwdRef) => { + ({ month, children, className, ...rest }: CalendarGridProps, fwdRef) => { const { dateFormat } = useDatePickerContext(); const weekStartsOn = getWeekStartByLocale(dateFormat); const weeks = useMemo( @@ -24,7 +28,12 @@ export const CalendarGrid = forwardRef( ); return ( - +
{range(daysPerWeek).map(i => { diff --git a/packages/date-picker/src/Calendar/CalendarGrid/index.ts b/packages/date-picker/src/Calendar/CalendarGrid/index.ts index 5ce25f839c..e5e190618e 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/index.ts +++ b/packages/date-picker/src/Calendar/CalendarGrid/index.ts @@ -1 +1,2 @@ export { CalendarGrid } from './CalendarGrid'; +export { CalendarGridProps } from './CalendarGrid.types'; diff --git a/packages/date-picker/src/Calendar/index.ts b/packages/date-picker/src/Calendar/index.ts new file mode 100644 index 0000000000..b068ea776a --- /dev/null +++ b/packages/date-picker/src/Calendar/index.ts @@ -0,0 +1,7 @@ +export { + CalendarCell, + CalendarCellProps, + CalendarCellState, +} from './CalendarCell'; +export { CalendarGrid, CalendarGridProps } from './CalendarGrid'; +export { MenuWrapper } from './MenuWrapper'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx index a20c83c9f2..3e4661cf33 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx @@ -29,7 +29,7 @@ const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( ); const meta: StoryMetaType = { - title: 'Components/DatePicker/Single', + title: 'Components/DatePicker/DatePicker', component: DatePicker, decorators: [ProviderWrapper], parameters: { diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index e2bf91eea1..e90ed05179 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -27,7 +27,7 @@ const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( ); const meta: StoryMetaType = { - title: 'Components/DatePicker/Single/DatePickerInput', + title: 'Components/DatePicker/DatePicker/DatePickerInput', component: DatePickerInput, decorators: [ProviderWrapper], parameters: { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 1fb89c411a..243d323130 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -52,7 +52,7 @@ const MenuDecorator = (Story: StoryFn, ctx: any) => { }; const meta: StoryMetaType = { - title: 'Components/DatePicker/Single/DatePickerMenu', + title: 'Components/DatePicker/DatePicker/DatePickerMenu', component: DatePickerMenu, decorators: [MenuDecorator], parameters: { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index b3b998322c..f64bee57c5 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -224,7 +224,6 @@ export const DatePickerMenu = forwardRef( className={menuCalendarGridStyles} onKeyDown={handleCalendarKeyDown} aria-label={monthLabel} - // tabIndex={0} > {(day, i) => ( & - Pick & + Pick & HTMLElementProps<'div'> & { - /** The value of the component, provided in UTC time */ - value?: DateType; - /** Callback fired when a cell is clicked */ onCellClick: (cellDate: Date) => void; }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx index 7e0194484a..c83b70d52c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx @@ -4,9 +4,9 @@ import range from 'lodash/range'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; -import { DropdownWidthBasis, Option, Select } from '@leafygreen-ui/select'; +import { Option, Select } from '@leafygreen-ui/select'; -import { Months } from '../../../constants'; +import { Months, selectElementProps } from '../../../constants'; import { useDatePickerContext } from '../../../DatePickerContext'; import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../utils'; import { @@ -19,15 +19,6 @@ interface DatePickerMenuHeaderProps { setMonth: (newMonth: Date) => void; } -const selectElementProps = { - size: 'xsmall', - allowDeselect: false, - dropdownWidthBasis: DropdownWidthBasis.Option, - // using no portal so the select menus are included in the backdrop "foreground" - // there is currently no way to pass a ref into the Select portal to use in backdrop "foreground" - usePortal: false, -} as const; - /** * A helper component for DatePickerMenu. * Tests for this component are in DatePickerMenu diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/DatePickerContext/DatePickerContext.tsx index 10cc07ae59..c42b47eaea 100644 --- a/packages/date-picker/src/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/DatePickerContext/DatePickerContext.tsx @@ -36,5 +36,5 @@ export const DatePickerProvider = ({ ); }; -/** A hook to access DatePickerContext value */ +/** A hook to access {@link DatePickerContextProps} */ export const useDatePickerContext = () => useContext(DatePickerContext); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx index 8a0079c0fd..4cfa488951 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx @@ -1,6 +1,7 @@ import React, { forwardRef } from 'react'; import { DateRangeInput } from '../DateRangeInput'; +import { DateRangeMenu } from '../DateRangeMenu'; import { DateRangeComponentProps } from './DateRangeComponent.types'; @@ -11,6 +12,7 @@ export const DateRangeComponent = forwardRef< return ( <> + ); }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts index c094905cb8..d9c3abc30b 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts @@ -1,6 +1,7 @@ -import { DateRangeType } from '../DateRangePicker.types'; +import { DateRangePickerProps, DateRangeType } from '../DateRangePicker.types'; -export interface DateRangeComponentProps { +export interface DateRangeComponentProps + extends Pick { range?: DateRangeType; setRange: (newVal?: DateRangeType | undefined) => void; } diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx index c7a04a2f1e..443877b230 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx @@ -27,7 +27,7 @@ const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( ); const meta: StoryMetaType = { - title: 'Components/DatePicker/Range/DateRangeInput', + title: 'Components/DatePicker/DateRangePicker/DateRangeInput', component: DateRangeInput, decorators: [ProviderWrapper], parameters: { diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx index 88fe41766d..09f966a86a 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx @@ -52,7 +52,7 @@ const MenuDecorator = (Story: StoryFn, ctx: any) => { }; const meta: StoryMetaType = { - title: 'Components/DatePicker/Range/DateRangeMenu', + title: 'Components/DatePicker/DateRangePicker/DateRangeMenu', component: DateRangeMenu, decorators: [MenuDecorator], parameters: { @@ -67,10 +67,9 @@ const meta: StoryMetaType = { timeZone: 'Europe/London', min: new Date('1996-10-14'), max: new Date('2026-10-14'), + showQuickSelection: true, }, - argTypes: { - value: { control: 'date' }, - }, + argTypes: {}, }; export default meta; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts index 928608f58d..1ce1788872 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts @@ -1,4 +1,13 @@ - import { css } from '@leafygreen-ui/emotion'; -export const baseStyles = css``; +export const rangeMenuWrapperStyles = css` + padding: 0; // needs to be set by inner content + z-index: 1; +`; + +export const menuContentStyles = css` + display: grid; + grid-auto-flow: column; + grid-template-columns: max-content auto; + z-index: 1; +`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx index d65e2b0ff6..375b913e91 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -1,9 +1,46 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; + +import { spacing } from '@leafygreen-ui/tokens'; + +import { MenuWrapper } from '../../Calendar/MenuWrapper'; +import { useDatePickerContext } from '../../DatePickerContext'; + +import { + menuContentStyles, + rangeMenuWrapperStyles, +} from './DateRangeMenu.styles'; import { DateRangeMenuProps } from './DateRangeMenu.types'; +import { DateRangeMenuCalendars } from './DateRangeMenuCalendars'; +import { DateRangeMenuProvider } from './DateRangeMenuContext'; +import { DateRangeMenuFooter } from './DateRangeMenuFooter'; +import { DateRangeMenuQuickSelection } from './DateRangeMenuQuickSelection'; + +export const DateRangeMenu = forwardRef( + ({ start, end, showQuickSelection, ...rest }: DateRangeMenuProps, fwdRef) => { + const { isOpen } = useDatePickerContext(); + + // TODO: Focus trap -// TODO: forwardRef -export function DateRangeMenu({}: DateRangeMenuProps) { - return
your content here
; -} + return ( + + +
+ {showQuickSelection && } + +
+ +
+
+ ); + }, +); DateRangeMenu.displayName = 'DateRangeMenu'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts index ceb9ef6f49..b843c12d46 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts @@ -1 +1,10 @@ -export interface DateRangeMenuProps {} \ No newline at end of file +import { PopoverProps, PortalControlProps } from '@leafygreen-ui/popover'; + +import { DateRangePickerProps } from '../DateRangePicker.types'; + +export type DateRangeMenuProps = PortalControlProps & + Pick & + Pick< + DateRangePickerProps, + 'start' | 'end' | 'showQuickSelection' | 'handleValidation' // TODO: Setter + > & {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts new file mode 100644 index 0000000000..bde34568cd --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts @@ -0,0 +1,9 @@ +import { css } from '@leafygreen-ui/emotion'; +import { spacing } from '@leafygreen-ui/tokens'; + +export const calendarsFrameStyles = css` + display: grid; + grid-auto-flow: column; + gap: ${spacing[4] * 2}px; + padding: ${spacing[2] + spacing[1]}px; +`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx new file mode 100644 index 0000000000..8ca4832488 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -0,0 +1,82 @@ +import React, { forwardRef } from 'react'; + +import { Subtitle } from '@leafygreen-ui/typography'; + +import { CalendarCell, CalendarGrid } from '../../../Calendar'; +import { + getFullMonthLabel, + getMonthName, + getUTCDateString, + isSameUTCDay, +} from '../../../utils'; +import { useDateRangeMenuContext } from '../DateRangeMenuContext'; + +import { calendarsFrameStyles } from './DateRangeMenuCalendars.styles'; + +export const DateRangeMenuCalendars = forwardRef(() => { + const { + startMonth, + // setStartMonth, + startCellRefs, + endMonth, + // setEndMonth, + endCellRefs, + today, + } = useDateRangeMenuContext(); + + return ( +
+ {/* TODO: Month labels & chevrons */} + {/* {getMonthName(startMonth.getUTCMonth()).long} + {getMonthName(endMonth.getUTCMonth()).long} */} + + + {(day, i) => ( + + {day.getUTCDate()} + + )} + + + + {(day, i) => ( + + {day.getUTCDate()} + + )} + +
+ ); +}); + +DateRangeMenuCalendars.displayName = 'DateRangeMenuCalendars'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts new file mode 100644 index 0000000000..ac8f9eed41 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts @@ -0,0 +1 @@ +export { DateRangeMenuCalendars } from './DateRangeMenuCalendars'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx new file mode 100644 index 0000000000..25b70e5462 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx @@ -0,0 +1,38 @@ +import React, { createContext, useContext } from 'react'; + +import { DynamicRefGetter } from '@leafygreen-ui/hooks'; + +export interface DateRangeMenuContextProps { + /** The month displayed on the left */ + startMonth: Date; + + /** Setter for the start month */ + setStartMonth: React.Dispatch>; + + /** A dynamic ref setter/getter for the start calendar cells */ + startCellRefs: DynamicRefGetter; + + /** The month displayed on the right */ + endMonth: Date; + + /** Setter for the end month */ + setEndMonth: React.Dispatch>; + + /** A dynamic ref setter/getter for the end calendar cells */ + endCellRefs: DynamicRefGetter; + + /** Memoized reference for Date.now */ + today?: Date; +} + +export const DateRangeMenuContext = createContext({ + startMonth: new Date(), + setStartMonth: () => {}, + startCellRefs: (() => undefined) as DynamicRefGetter, + endMonth: new Date(), + setEndMonth: () => {}, + endCellRefs: (() => undefined) as DynamicRefGetter, +}); + +/** Hook to access {@link DateRangeMenuContextProps} */ +export const useDateRangeMenuContext = () => useContext(DateRangeMenuContext); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx new file mode 100644 index 0000000000..6081a20fcd --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx @@ -0,0 +1,50 @@ +import React, { PropsWithChildren, useMemo, useState } from 'react'; +import { addMonths } from 'date-fns'; + +import { useDynamicRefs } from '@leafygreen-ui/hooks'; + +import { getFirstOfMonth, setToUTCMidnight } from '../../../utils'; +import { DateRangeMenuProps } from '../DateRangeMenu.types'; + +import { DateRangeMenuContext } from './DateRangeMenuContext'; + +export interface DateRangeMenuProviderProps + extends Pick, + PropsWithChildren<{}> {} + +/** + * Receives the start & end dates + * and initializes the start & end display months + */ +export const DateRangeMenuProvider = ({ + start, + end, + children, +}: DateRangeMenuProviderProps) => { + const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); + const thisMonth = useMemo(() => getFirstOfMonth(today), [today]); + + const [startMonth, setStartMonth] = useState(start ?? thisMonth); + const [endMonth, setEndMonth] = useState( + end ?? addMonths(thisMonth, 1), + ); + + const startCellRefs = useDynamicRefs(); + const endCellRefs = useDynamicRefs(); + + return ( + + {children} + + ); +}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/index.ts new file mode 100644 index 0000000000..41fcc6a3fd --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/index.ts @@ -0,0 +1,9 @@ +export { + DateRangeMenuContext, + DateRangeMenuContextProps, + useDateRangeMenuContext, +} from './DateRangeMenuContext'; +export { + DateRangeMenuProvider, + DateRangeMenuProviderProps, +} from './DateRangeMenuProvider'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts new file mode 100644 index 0000000000..0337561a15 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts @@ -0,0 +1,18 @@ +import { css } from '@leafygreen-ui/emotion'; +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; + +export const footerStyles = css` + display: flex; + width: 100%; + justify-content: space-between; + padding: ${spacing[2] + spacing[1]}px; + border-block-start: 1px solid ${palette.gray.light2}; +`; + +export const clearButtonStyles = css` + outline: none; + border: none; + background-color: unset; + z-index: 0; +`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx new file mode 100644 index 0000000000..62041ff92e --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import Button, { Size, Variant } from '@leafygreen-ui/button'; +import { Link } from '@leafygreen-ui/typography'; + +import { clearButtonStyles, footerStyles } from './DateRangeMenuFooter.styles'; + +export const DateRangeMenuFooter = () => { + return ( +
+ + Clear + +
+ + +
+
+ ); +}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts new file mode 100644 index 0000000000..4744d49ce2 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts @@ -0,0 +1 @@ +export { DateRangeMenuFooter } from './DateRangeMenuFooter'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts new file mode 100644 index 0000000000..c336da459f --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts @@ -0,0 +1,19 @@ +import { css } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { palette } from '@leafygreen-ui/palette'; +import { spacing } from '@leafygreen-ui/tokens'; + +export const quickSelectMenuStyles = css` + padding: ${spacing[4]}px; + // TODO: Fix the menu vs clear button z-index +`; + +export const quickSelectMenuThemeStyles: Record = { + [Theme.Light]: css` + background-color: ${palette.gray.light3}; + border-inline-end: 1px solid ${palette.gray.light2}; + `, + [Theme.Dark]: css` + // TODO: + `, +}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx new file mode 100644 index 0000000000..bc0572b600 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { forwardRef } from 'react'; +import { addMonths } from 'date-fns'; +import { range } from 'lodash'; + +import { cx } from '@leafygreen-ui/emotion'; +import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import { Option, Select } from '@leafygreen-ui/select'; +import { Overline } from '@leafygreen-ui/typography'; + +import { Months, selectElementProps } from '../../../constants'; +import { useDatePickerContext } from '../../../DatePickerContext'; +import { setUTCMonth, setUTCYear } from '../../../utils'; +import { useDateRangeMenuContext } from '../DateRangeMenuContext'; + +import { + quickSelectMenuStyles, + quickSelectMenuThemeStyles, +} from './DateRangeMenuQuickSelection.styles'; +import { QuickRangeButton } from './QuickRangeButton'; + +export const DateRangeMenuQuickSelection = forwardRef( + (_props, fwdRef) => { + const { theme } = useDarkMode(); + const { min, max, isInRange } = useDatePickerContext(); + const { startMonth, setStartMonth, setEndMonth } = + useDateRangeMenuContext(); + + // TODO: is this the right logic? + const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); + + const updateMonth = (newMonth: Date) => { + // TODO: refine this logic + if (isInRange(newMonth)) { + setStartMonth(newMonth); + setEndMonth(addMonths(newMonth, 1)); + } + }; + + return ( +
+
+ + +
+
+ Quick Ranges: + {/* + TODO: this functionality + Do we want to set these up in some config object? + */} + Today + Yesterday + Last 7 days + Last 30 days + Last 90 days + Last 12 months + All time +
+
+ ); + }, +); + +DateRangeMenuQuickSelection.displayName = 'DateRangeMenuQuickSelection'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx new file mode 100644 index 0000000000..b696152851 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; +import { StoryMetaType, StoryType } from '@leafygreen-ui/lib'; + +import { QuickRangeButton } from './QuickRangeButton'; + +const meta: StoryMetaType = { + title: 'Components/DatePicker/DateRangePicker/QuickRangeButton', + component: QuickRangeButton, + parameters: { + default: null, + generate: { + combineArgs: { + darkMode: [false, true], + 'data-hover': [false, true], + 'data-focus': [false, true], + }, + decorator: (Instance, ctx) => ( + + + + ), + args: { + children: 'Last 12 months', + }, + }, + }, +}; + +export default meta; + +export const Basic: StoryType = () => { + return Last 12 months; +}; + +export const Generated = () => <>; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts new file mode 100644 index 0000000000..44bc927ef8 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts @@ -0,0 +1,55 @@ +import { css } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { palette } from '@leafygreen-ui/palette'; +import { + fontFamilies, + fontWeights, + spacing, + typeScales, +} from '@leafygreen-ui/tokens'; + +export const baseQuickRangeButtonStyles = css` + border: none; + outline: none; + background-color: unset; + display: block; + font-family: ${fontFamilies.default}; + font-size: ${typeScales.body1.fontSize}px; + line-height: ${typeScales.body1.lineHeight}px; + font-weight: ${fontWeights.regular}; + padding: 2px ${spacing[1]}px; + border-radius: ${spacing[2]}px; + cursor: pointer; +`; + +const hoverSelector = `&:hover, &[data-hover="true"]`; +const focusSelector = `&:focus-visible, &[data-focus="true"]`; + +export const baseQuickRangeButtonThemeStyles: Record = { + [Theme.Light]: css` + color: ${palette.black}; + + ${hoverSelector} { + outline: 2px solid ${palette.gray.light2}; + } + + ${focusSelector} { + color: ${palette.blue.dark1}; + outline: 2px solid ${palette.blue.light1}; + font-weight: ${fontWeights.bold}; + } + `, + [Theme.Dark]: css` + color: ${palette.white}; + + ${hoverSelector} { + outline: 2px solid ${palette.gray.dark2}; + } + + ${focusSelector} { + color: ${palette.blue.light1}; + outline: 2px solid ${palette.blue.light1}; + font-weight: ${fontWeights.bold}; + } + `, +}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx new file mode 100644 index 0000000000..9d2d894bcc --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { forwardRef } from 'react'; + +import { cx } from '@leafygreen-ui/emotion'; +import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import { HTMLElementProps } from '@leafygreen-ui/lib'; + +import { + baseQuickRangeButtonStyles, + baseQuickRangeButtonThemeStyles, +} from './QuickRangeButton.styles'; + +export const QuickRangeButton = forwardRef< + HTMLButtonElement, + HTMLElementProps<'button'> +>(({ children, className, ...rest }, fwdRef) => { + const { theme } = useDarkMode(); + return ( + + ); +}); + +QuickRangeButton.displayName = 'QuickRangeButton'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts new file mode 100644 index 0000000000..52c37b06e4 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts @@ -0,0 +1 @@ +export { QuickRangeButton } from './QuickRangeButton'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts new file mode 100644 index 0000000000..900d132295 --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts @@ -0,0 +1 @@ +export { DateRangeMenuQuickSelection } from './DateRangeMenuQuickSelection'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx index 76a9c8171a..5beb7234db 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx @@ -30,7 +30,7 @@ const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( ); const meta: StoryMetaType = { - title: 'Components/DatePicker/Range', + title: 'Components/DatePicker/DateRangePicker', component: DateRangePicker, decorators: [ProviderWrapper], parameters: { diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx index 81dbd04319..4d0c6b32f9 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx @@ -15,6 +15,7 @@ export const DateRangePicker = forwardRef( end: endProp, initialEnd: initialEndProp, onRangeChange, + showQuickSelection, ...props }: DateRangePickerProps, fwdRef, @@ -33,6 +34,7 @@ export const DateRangePicker = forwardRef( ref={fwdRef} range={range} setRange={setRange} + showQuickSelection={showQuickSelection} {...restProps} /> diff --git a/packages/date-picker/src/constants.ts b/packages/date-picker/src/constants.ts index 263f7e3814..d6cc90fd70 100644 --- a/packages/date-picker/src/constants.ts +++ b/packages/date-picker/src/constants.ts @@ -1,5 +1,7 @@ import range from 'lodash/range'; +import { DropdownWidthBasis } from '@leafygreen-ui/select'; + import { getMonthName } from './utils/getMonthName'; // Compute the long & short form of each month index @@ -37,3 +39,13 @@ export const DaysOfWeek = [ ] as const; export type DaysOfWeek = (typeof DaysOfWeek)[number]; + +/** Default props for the month & year select menus */ +export const selectElementProps = { + size: 'xsmall', + allowDeselect: false, + dropdownWidthBasis: DropdownWidthBasis.Option, + // using no portal so the select menus are included in the backdrop "foreground" + // there is currently no way to pass a ref into the Select portal to use in backdrop "foreground" + usePortal: false, +} as const; diff --git a/packages/date-picker/src/types.ts b/packages/date-picker/src/types.ts index 7972a5d2b5..b127208654 100644 --- a/packages/date-picker/src/types.ts +++ b/packages/date-picker/src/types.ts @@ -1,4 +1,4 @@ -import { omit } from 'lodash'; +import omit from 'lodash/omit'; import { FormFieldState } from '@leafygreen-ui/form-field'; import { DarkModeProps } from '@leafygreen-ui/lib'; diff --git a/packages/date-picker/tsconfig.json b/packages/date-picker/tsconfig.json index 8a43dbc9e5..fc67898510 100644 --- a/packages/date-picker/tsconfig.json +++ b/packages/date-picker/tsconfig.json @@ -15,6 +15,12 @@ ], "exclude": ["**/*.spec.*", "**/*.story.*"], "references": [ + { + "path": "../button" + }, + { + "path": "../emotion" + }, { "path": "../emotion" }, diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 6a75da1e8f..7475236db2 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -2,7 +2,7 @@ export { useAutoScroll } from './useAutoScroll'; export { default as useAvailableSpace } from './useAvailableSpace'; export { useBackdropClick } from './useBackdropClick'; export { useControlledValue } from './useControlledValue'; -export { useDynamicRefs } from './useDynamicRefs'; +export { type DynamicRefGetter, useDynamicRefs } from './useDynamicRefs'; export { default as useEscapeKey } from './useEscapeKey'; export { default as useEventListener } from './useEventListener'; export { useForceRerender } from './useForceRerender'; From 784eb841251bad77859b081b16827d555ad78bfb Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 18:29:53 -0400 Subject: [PATCH 188/351] fix tests --- packages/form-field/src/FormField.spec.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/form-field/src/FormField.spec.tsx b/packages/form-field/src/FormField.spec.tsx index 336f8efd70..37d566c4c8 100644 --- a/packages/form-field/src/FormField.spec.tsx +++ b/packages/form-field/src/FormField.spec.tsx @@ -306,7 +306,7 @@ describe('packages/form-field', () => { const { queryByTestId } = render( } + contentEnd={} >
@@ -315,7 +315,21 @@ describe('packages/form-field', () => { const icon = queryByTestId('icon'); expect(icon).toBeInTheDocument(); - expect(icon?.tagName).toEqual('svg'); + expect(icon?.tagName.toLowerCase()).toEqual('svg'); + }); + + test('Renders other content', () => { + const { queryByText } = render( + + Optional}> +
+ + , + ); + + const em = queryByText('Optional'); + expect(em).toBeInTheDocument(); + expect(em?.tagName.toLowerCase()).toEqual('em'); }); describe('custom children', () => { From 7c7d6f3401f902faee59de9b0036b634ba0b91a6 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 5 Oct 2023 18:30:38 -0400 Subject: [PATCH 189/351] Update package.json --- packages/form-field/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/form-field/package.json b/packages/form-field/package.json index f0269e9cc4..66e5c88157 100644 --- a/packages/form-field/package.json +++ b/packages/form-field/package.json @@ -28,7 +28,8 @@ }, "devDependencies": { "@leafygreen-ui/button": "^21.0.7", - "@leafygreen-ui/icon-button": "^15.0.18" + "@leafygreen-ui/icon-button": "^15.0.18", + "lodash": "^4.17.21" }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/form-field", "repository": { From 559d1a50b4662f8b18607d89ce9b104df7967ac7 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 6 Oct 2023 13:00:03 -0400 Subject: [PATCH 190/351] bump dp deps --- packages/date-picker/package.json | 6 +++--- yarn.lock | 12 ------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index a1516e5919..f44bd7f5be 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -23,10 +23,10 @@ "@leafygreen-ui/icon-button": "^15.0.17", "@leafygreen-ui/lib": "^12.0.0", "@leafygreen-ui/palette": "^4.0.7", - "@leafygreen-ui/popover": "^11.0.17", - "@leafygreen-ui/select": "^10.3.15", + "@leafygreen-ui/popover": "^11.1.0", + "@leafygreen-ui/select": "^11.0.0", "@leafygreen-ui/tokens": "^2.2.0", - "@leafygreen-ui/typography": "^16.5.5", + "@leafygreen-ui/typography": "^17.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "polished": "^4.2.2", diff --git a/yarn.lock b/yarn.lock index a00b7c8d3f..346aa1d955 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,18 +2274,6 @@ lodash "^4.17.21" prop-types "^15.7.2" -"@leafygreen-ui/typography@^16.5.5": - version "16.5.5" - resolved "https://registry.yarnpkg.com/@leafygreen-ui/typography/-/typography-16.5.5.tgz#65cc0ce7e39f5b8f72f9b9a4dbea80868ac09694" - integrity sha512-mErhTYM0C1PZaeADTkp5v/MAS6aEhavWHZ3otHthBSo/zwI5uAYnkreheiYElc66B/0bcOxCikLVkP3zaFnX2A== - dependencies: - "@leafygreen-ui/emotion" "^4.0.7" - "@leafygreen-ui/icon" "^11.22.2" - "@leafygreen-ui/lib" "^11.0.0" - "@leafygreen-ui/palette" "^4.0.7" - "@leafygreen-ui/polymorphic" "^1.3.6" - "@leafygreen-ui/tokens" "^2.1.4" - "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" From a34a80d5498b4258c8b5021c9226d719adab732f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 6 Oct 2023 13:10:33 -0400 Subject: [PATCH 191/351] updates tsdocs for shared components --- .../src/Calendar/CalendarCell/CalendarCell.tsx | 6 ++++++ .../src/Calendar/CalendarGrid/CalendarGrid.tsx | 15 +++++++++++++++ .../src/Calendar/MenuWrapper/MenuWrapper.tsx | 2 +- .../DateInput/CalendarButton/CalendarButton.tsx | 3 +++ .../src/DateInput/DateFormField/DateFormField.tsx | 5 ++++- .../src/DateInput/DateInputBox/DateInputBox.tsx | 5 ++++- .../DateInputSegment/DateInputSegment.tsx | 8 +++++++- 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx b/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx index 66fcd8d8d8..c2190b6a2a 100644 --- a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx +++ b/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx @@ -17,6 +17,12 @@ import { } from './CalendarCell.styles'; import { CalendarCellProps, CalendarCellState } from './CalendarCell.types'; +/** + * A single calendar cell. + * + * Renders the appropriate styles based on + * the provided state, current & highlight props + */ export const CalendarCell = React.forwardRef< HTMLTableCellElement, CalendarCellProps diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx index 53d8a64f4d..0c8de59508 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx @@ -17,6 +17,21 @@ import { CalendarGridProps } from './CalendarGrid.types'; /** * A simple table that renders the `CalendarCell` components passed as children + * + * Accepts a mapped render function as children. + * + * Example usage: + * ```tsx + * // Renders the current month + * + * {(day) => ( + * + * {day.getUTCDate()} + * + * )} + * + * ``` + * */ export const CalendarGrid = forwardRef( ({ month, children, className, ...rest }: CalendarGridProps, fwdRef) => { diff --git a/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx b/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx index 330d65001a..43e239c733 100644 --- a/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx +++ b/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx @@ -8,7 +8,7 @@ import Popover, { PopoverProps } from '@leafygreen-ui/popover'; import { menuStyles } from './MenuWrapper.styles'; /** - * A styled popover + * A simple styled popover component */ export const MenuWrapper = forwardRef< HTMLDivElement, diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx index 0fff74a6de..9cd3ba00b4 100644 --- a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx +++ b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx @@ -6,6 +6,9 @@ import IconButton, { BaseIconButtonProps } from '@leafygreen-ui/icon-button'; import { iconButtonStyles } from './CalendarButton.styles'; +/** + * The icon button on the right of the DatePicker form field + */ export const CalendarButton = forwardRef< HTMLButtonElement, BaseIconButtonProps diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx index 9ebaa73e6e..43e41d2070 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx @@ -7,7 +7,10 @@ import { CalendarButton } from '../CalendarButton'; import { DateFormFieldProps } from './DateFormField.types'; -/** A wrapper around `FormField` that sets the icon */ +/** + * A wrapper around `FormField` that sets the relevant + * attributes, styling & icon button + */ export const DateFormField = React.forwardRef< HTMLDivElement, DateFormFieldProps diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx index 15929b31bc..394bf9fa24 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx @@ -23,10 +23,13 @@ import { DateInputBoxProps } from './DateInputBox.types'; /** * Renders a styled date input with appropriate segment order & separator characters. * - * Uses vars value & dateFormat with `Intl.DateTimeFormat.prototype.formatToParts()` + * Depends on {@link DateInputSegment} + * + * Uses parameters `value` & `dateFormat` along with {@link Intl.DateTimeFormat.prototype.formatToParts} * to determine the segment order and separator characters. * * Provided value is assumed to be UTC. + * * Argument passed into `setValue` callback is also in UTC * @internal */ diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx index e2f03f5278..1a2e5106a9 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx @@ -25,6 +25,12 @@ import { } from './DateInputSegment.styles'; import { DateInputSegmentProps } from './DateInputSegment.types'; +/** + * Renders a single date segment with the + * appropriate character padding/truncation. + * + * Only fires a change handler when the input is blurred + */ export const DateInputSegment = React.forwardRef< HTMLInputElement, DateInputSegmentProps @@ -68,7 +74,7 @@ export const DateInputSegment = React.forwardRef< setInternalValue(e.target.value); }; - // When the user unfocuses the element, then we fire the change handler + // When the user un-focuses the element, then we fire the change handler const handleBlur: FocusEventHandler = e => { const formattedValue = formatValue(internalValue); From d70bd9b22af26c92362bd148eb1e2dadd29b2c01 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 6 Oct 2023 13:30:14 -0400 Subject: [PATCH 192/351] rename QuickSelectionMenu --- .../src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx | 4 ++-- .../DateRangeMenu/DateRangeMenuQuickSelection/index.ts | 1 - .../QuickRangeButton/QuickRangeButton.stories.tsx | 0 .../QuickRangeButton/QuickRangeButton.styles.ts | 0 .../QuickRangeButton/QuickRangeButton.tsx | 0 .../QuickRangeButton/index.ts | 0 .../QuickSelectionMenu.styles.ts} | 0 .../QuickSelectionMenu.tsx} | 8 ++++---- .../DateRangeMenu/QuickSelectionMenu/index.ts | 1 + 9 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection => QuickSelectionMenu}/QuickRangeButton/QuickRangeButton.stories.tsx (100%) rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection => QuickSelectionMenu}/QuickRangeButton/QuickRangeButton.styles.ts (100%) rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection => QuickSelectionMenu}/QuickRangeButton/QuickRangeButton.tsx (100%) rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection => QuickSelectionMenu}/QuickRangeButton/index.ts (100%) rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts => QuickSelectionMenu/QuickSelectionMenu.styles.ts} (100%) rename packages/date-picker/src/DateRangePicker/DateRangeMenu/{DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx => QuickSelectionMenu/QuickSelectionMenu.tsx} (93%) create mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx index 375b913e91..860f384a6e 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -13,7 +13,7 @@ import { DateRangeMenuProps } from './DateRangeMenu.types'; import { DateRangeMenuCalendars } from './DateRangeMenuCalendars'; import { DateRangeMenuProvider } from './DateRangeMenuContext'; import { DateRangeMenuFooter } from './DateRangeMenuFooter'; -import { DateRangeMenuQuickSelection } from './DateRangeMenuQuickSelection'; +import { QuickSelectionMenu } from './QuickSelectionMenu'; export const DateRangeMenu = forwardRef( ({ start, end, showQuickSelection, ...rest }: DateRangeMenuProps, fwdRef) => { @@ -33,7 +33,7 @@ export const DateRangeMenu = forwardRef( {...rest} >
- {showQuickSelection && } + {showQuickSelection && }
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts deleted file mode 100644 index 900d132295..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DateRangeMenuQuickSelection } from './DateRangeMenuQuickSelection'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.stories.tsx rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.styles.ts rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/QuickRangeButton.tsx rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/QuickRangeButton/index.ts rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.styles.ts rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx similarity index 93% rename from packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx rename to packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx index bc0572b600..39b23a0d2b 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuQuickSelection/DateRangeMenuQuickSelection.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx @@ -13,13 +13,13 @@ import { useDatePickerContext } from '../../../DatePickerContext'; import { setUTCMonth, setUTCYear } from '../../../utils'; import { useDateRangeMenuContext } from '../DateRangeMenuContext'; +import { QuickRangeButton } from './QuickRangeButton'; import { quickSelectMenuStyles, quickSelectMenuThemeStyles, -} from './DateRangeMenuQuickSelection.styles'; -import { QuickRangeButton } from './QuickRangeButton'; +} from './QuickSelectionMenu.styles'; -export const DateRangeMenuQuickSelection = forwardRef( +export const QuickSelectionMenu = forwardRef( (_props, fwdRef) => { const { theme } = useDarkMode(); const { min, max, isInRange } = useDatePickerContext(); @@ -93,4 +93,4 @@ export const DateRangeMenuQuickSelection = forwardRef( }, ); -DateRangeMenuQuickSelection.displayName = 'DateRangeMenuQuickSelection'; +QuickSelectionMenu.displayName = 'DateRangeMenuQuickSelection'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts new file mode 100644 index 0000000000..980234417b --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts @@ -0,0 +1 @@ +export { QuickSelectionMenu } from './QuickSelectionMenu'; From 27f298405bb8b341f9dc20b3555b969b8cb18b29 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 6 Oct 2023 14:17:01 -0400 Subject: [PATCH 193/351] update range calendar menu styling --- .../DatePickerMenu/DatePickerMenu.tsx | 2 +- .../DateRangeMenuCalendars.styles.ts | 36 +++++- .../DateRangeMenuCalendars.tsx | 118 ++++++++++-------- 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index f64bee57c5..1fb7c07652 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -44,7 +44,7 @@ export const DatePickerMenu = forwardRef( const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); const { isInRange, isOpen, setOpen } = useDatePickerContext(); - // TODO: + // TODO: https://jira.mongodb.org/browse/LG-3666 // useDynamicRefs may overflow if a user navigates to too many months. // consider purging the refs map within the hook const cellRefs = useDynamicRefs(); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts index bde34568cd..e86566cbb1 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts @@ -1,9 +1,39 @@ import { css } from '@leafygreen-ui/emotion'; import { spacing } from '@leafygreen-ui/tokens'; +const calendarGapX = spacing[4] * 2; + export const calendarsFrameStyles = css` display: grid; - grid-auto-flow: column; - gap: ${spacing[4] * 2}px; - padding: ${spacing[2] + spacing[1]}px; + grid-template-rows: 28px auto; // Size of icon-button + grid-template-areas: 'header' 'calendars'; + gap: ${spacing[3]}px; + padding: ${spacing[4]}px; + padding-top: ${spacing[6]}px; +`; + +export const calendarsContainerStyles = css` + grid-area: calendars; + display: flex; + gap: ${calendarGapX}px; + align-items: center; +`; + +export const calendarHeadersContainerStyle = css` + grid-area: header; + display: flex; + gap: ${calendarGapX}px; + align-items: center; +`; + +export const calendarHeaderStyles = css` + display: flex; + + width: 100%; + align-items: center; + + h6 { + width: 100%; + text-align: center; + } `; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index 8ca4832488..d7c2017e5c 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -1,17 +1,23 @@ import React, { forwardRef } from 'react'; +import Icon from '@leafygreen-ui/icon'; +import IconButton from '@leafygreen-ui/icon-button'; import { Subtitle } from '@leafygreen-ui/typography'; import { CalendarCell, CalendarGrid } from '../../../Calendar'; import { getFullMonthLabel, - getMonthName, getUTCDateString, isSameUTCDay, } from '../../../utils'; import { useDateRangeMenuContext } from '../DateRangeMenuContext'; -import { calendarsFrameStyles } from './DateRangeMenuCalendars.styles'; +import { + calendarHeadersContainerStyle, + calendarHeaderStyles, + calendarsContainerStyles, + calendarsFrameStyles, +} from './DateRangeMenuCalendars.styles'; export const DateRangeMenuCalendars = forwardRef(() => { const { @@ -26,55 +32,69 @@ export const DateRangeMenuCalendars = forwardRef(() => { return (
- {/* TODO: Month labels & chevrons */} - {/* {getMonthName(startMonth.getUTCMonth()).long} - {getMonthName(endMonth.getUTCMonth()).long} */} +
+ + {(day, i) => ( + + {day.getUTCDate()} + + )} + + + + {(day, i) => ( + + {day.getUTCDate()} + + )} + +
- - {(day, i) => ( - - {day.getUTCDate()} - - )} - +
+
+ + + + {getFullMonthLabel(startMonth)} +
- - {(day, i) => ( - - {day.getUTCDate()} - - )} - +
+ {getFullMonthLabel(endMonth)} + + + +
+
); }); From c5b04e63118b20720b535b65f94cfd9c080f489f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 6 Oct 2023 14:19:33 -0400 Subject: [PATCH 194/351] Create getFullMonthLabel.spec.ts --- .../getFullMonthLabel/getFullMonthLabel.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts diff --git a/packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts b/packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts new file mode 100644 index 0000000000..e525fcf604 --- /dev/null +++ b/packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts @@ -0,0 +1,12 @@ +import { Month } from '../../constants'; +import { newUTC } from '../newUTC'; + +import { getFullMonthLabel } from '.'; + +describe('packages/date-picker/utils/getMonthName', () => { + test('Jan', () => { + expect(getFullMonthLabel(newUTC(2023, Month.January, 5))).toEqual( + 'January 2023', + ); + }); +}); From 212fe72630472a38d1c9cf4d26438dc8b2eef36b Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 11:09:10 -0400 Subject: [PATCH 195/351] update range menu spacing --- .../DatePicker/DatePickerMenu/DatePickerMenu.tsx | 2 +- .../DateRangeMenuCalendars.styles.ts | 6 ++++-- .../QuickSelectionMenu.styles.ts | 16 ++++++++++++++++ .../QuickSelectionMenu/QuickSelectionMenu.tsx | 6 ++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 1fb7c07652..a961751b62 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -90,7 +90,7 @@ export const DatePickerMenu = forwardRef( }; /** - * When month changes, after the DOM changes, focus the relevant cell \ + * When month changes, after the DOM changes, focus the relevant cell */ useLayoutEffect(() => { if (highlight && !isSameUTCDay(highlight, prevHighlight)) { diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts index e86566cbb1..c595a70974 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts @@ -1,5 +1,5 @@ import { css } from '@leafygreen-ui/emotion'; -import { spacing } from '@leafygreen-ui/tokens'; +import { spacing, typeScales } from '@leafygreen-ui/tokens'; const calendarGapX = spacing[4] * 2; @@ -16,7 +16,7 @@ export const calendarsContainerStyles = css` grid-area: calendars; display: flex; gap: ${calendarGapX}px; - align-items: center; + align-items: start; `; export const calendarHeadersContainerStyle = css` @@ -33,6 +33,8 @@ export const calendarHeaderStyles = css` align-items: center; h6 { + font-size: ${typeScales.body2.fontSize}px; + line-height: ${typeScales.body2.lineHeight}px; width: 100%; text-align: center; } diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts index c336da459f..f1ee38c115 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts @@ -4,6 +4,9 @@ import { palette } from '@leafygreen-ui/palette'; import { spacing } from '@leafygreen-ui/tokens'; export const quickSelectMenuStyles = css` + display: flex; + flex-direction: column; + gap: ${spacing[4]}px; padding: ${spacing[4]}px; // TODO: Fix the menu vs clear button z-index `; @@ -17,3 +20,16 @@ export const quickSelectMenuThemeStyles: Record = { // TODO: `, }; + +export const quickSelectMenuMonthSelectContainerStyles = css` + display: flex; + flex-direction: column; + gap: ${spacing[2]}px; +`; + +export const quickSelectMenuSelectionsContainerStyles = css` + display: flex; + flex-direction: column; + align-items: start; + gap: ${spacing[2]}px; +`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx index 39b23a0d2b..c1a1179c51 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx @@ -15,6 +15,8 @@ import { useDateRangeMenuContext } from '../DateRangeMenuContext'; import { QuickRangeButton } from './QuickRangeButton'; import { + quickSelectMenuMonthSelectContainerStyles, + quickSelectMenuSelectionsContainerStyles, quickSelectMenuStyles, quickSelectMenuThemeStyles, } from './QuickSelectionMenu.styles'; @@ -42,7 +44,7 @@ export const QuickSelectionMenu = forwardRef( ref={fwdRef} className={cx(quickSelectMenuStyles, quickSelectMenuThemeStyles[theme])} > -
+
-
+
Quick Ranges: {/* TODO: this functionality From 84a3d427eaa48213df43e2767da828de6df13f1a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 12:44:22 -0400 Subject: [PATCH 196/351] adds range rendering tests --- .../DateRangeComponent/DateRangeComponent.tsx | 4 +- .../DateRangeInput/DateRangeInput.tsx | 3 +- .../DateRangePicker/DateRangePicker.spec.tsx | 119 +++++++++++++++++- .../DateRangePicker.testutils.tsx | 114 +++++++++++++++++ 4 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx index 4cfa488951..ba856c94e9 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx @@ -8,10 +8,10 @@ import { DateRangeComponentProps } from './DateRangeComponent.types'; export const DateRangeComponent = forwardRef< HTMLDivElement, DateRangeComponentProps ->((props: DateRangeComponentProps, fwdRef) => { +>(({ ...rest }: DateRangeComponentProps, fwdRef) => { return ( <> - + ); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index fd26e76d13..c88f605a3b 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -20,7 +20,7 @@ import { DateRangeInputProps } from './DateRangeInput.types'; const EN_DASH = '–'; export const DateRangeInput = forwardRef( - ({ start, end, handleValidation }: DateRangeInputProps, fwdRef) => { + ({ start, end, handleValidation, ...rest }: DateRangeInputProps, fwdRef) => { const { disabled, formatParts, setOpen } = useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); @@ -93,6 +93,7 @@ export const DateRangeInput = forwardRef( ref={fwdRef} onKeyDown={handleKeyDown} onInputClick={handleInputClick} + {...rest} >
diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index fe1ed2e9b2..1a1cb6a0cb 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -1,8 +1,123 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import { Month } from '../constants'; +import { newUTC } from '../utils'; + +import { renderDateRangePicker } from './DateRangePicker.testutils'; import { DateRangePicker } from '.'; describe('packages/date-picker/date-range-picker', () => { - test('condition', () => {}); + describe('Rendering', () => { + /// Note: Many rendering tests should be handled by Chromatic + + test('renders label', () => { + const { getByText } = render(); + const label = getByText('Label'); + expect(label).toBeInTheDocument(); + }); + + test('renders description', () => { + const { getByText } = render( + , + ); + const description = getByText('Description'); + expect(description).toBeInTheDocument(); + }); + + test('spreads rest to formField', () => { + const { getByTestId } = render( + , + ); + const formField = getByTestId('lg-date-range-picker'); + expect(formField).toBeInTheDocument(); + }); + + test('formField contains label & input elements', () => { + const { getByTestId, getByRole } = render( + , + ); + const formField = getByTestId('lg-date-range-picker'); + const inputContainer = getByRole('combobox'); + expect(formField.querySelector('label')).toBeInTheDocument(); + expect(formField.querySelector('label')).toHaveTextContent('Label'); + expect(inputContainer).toBeInTheDocument(); + }); + + test('renders 6 inputs', () => { + const { inputElements } = renderDateRangePicker(); + expect(inputElements).toHaveLength(6); + }); + + test('renders `start` & `end` prop', () => { + const { inputElements } = renderDateRangePicker({ + start: newUTC(2023, Month.January, 5), + end: newUTC(2023, Month.February, 14), + }); + expect(inputElements[0]).toEqual('05'); + expect(inputElements[1]).toEqual('01'); + expect(inputElements[2]).toEqual('2023'); + expect(inputElements[3]).toEqual('14'); + expect(inputElements[4]).toEqual('02'); + expect(inputElements[5]).toEqual('2023'); + }); + + test('renders `initialStart` & `initialEnd` prop', () => { + const { inputElements } = renderDateRangePicker({ + initialStart: newUTC(2023, Month.July, 5), + initialEnd: newUTC(2023, Month.August, 10), + }); + expect(inputElements[0]).toEqual('05'); + expect(inputElements[1]).toEqual('07'); + expect(inputElements[2]).toEqual('2023'); + expect(inputElements[3]).toEqual('10'); + expect(inputElements[4]).toEqual('08'); + expect(inputElements[5]).toEqual('2023'); + }); + + describe('Menu', () => { + test('menu is initially closed', () => { + const { getMenuElements } = renderDateRangePicker(); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).not.toBeInTheDocument(); + }); + + test('menu is initially open when rendered with `initialOpen`', async () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + }); + const { menuContainerEl } = getMenuElements(); + await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); + }); + + test('if no value is set, menu opens to current month', () => { + const { openMenu } = renderDateRangePicker(); + const { calendarGrids } = openMenu(); + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'December 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'January 2024', + ); + }); + + test('if a value is set, menu opens to the month of that value', () => { + const { openMenu } = renderDateRangePicker({ + start: newUTC(2023, Month.March, 10), + }); + const { calendarGrids } = openMenu(); + expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'March 2023'); + }); + + test('renders the appropriate number of cells', () => { + const { openMenu } = renderDateRangePicker({ + start: newUTC(2024, Month.February, 14), + }); + const { calendarCells } = openMenu(); + expect(calendarCells).toHaveLength(29 + 31); + }); + }); + }); }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx new file mode 100644 index 0000000000..bba0835bfc --- /dev/null +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { + getByRole as globalGetByRole, + render, + RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { DateRangePicker, DateRangePickerProps } from '.'; + +interface RenderDateRangePickerResult extends RenderResult { + formField: HTMLElement; + inputContainer: HTMLElement; + inputElements: Array; + calendarButton: HTMLButtonElement; + getMenuElements: () => RenderMenuResult; + openMenu: () => RenderMenuResult; +} + +interface RenderMenuResult { + menuContainerEl: HTMLElement | null; + leftChevron: HTMLButtonElement | null; + rightChevron: HTMLButtonElement | null; + monthSelect: HTMLButtonElement | null; + yearSelect: HTMLButtonElement | null; + calendarGrids: Array | null; + calendarCells: Array; + todayCell: HTMLTableCellElement | null; +} + +/** + * Renders a date picker for jest environments + */ +export const renderDateRangePicker = ( + props?: Partial, +): RenderDateRangePickerResult => { + const defaultProps = { label: '' }; + const result = render( + , + ); + + const formField = result.getByTestId('lg-date-picker'); + const inputContainer = result.getByRole('combobox'); + + const inputElements = Array.from(inputContainer.querySelectorAll('input')); + + const calendarButton = globalGetByRole( + inputContainer, + 'button', + ) as HTMLButtonElement; + + /** + * Returns relevant menu elements. + * Call this after the menu has been opened + */ + function getMenuElements(): RenderMenuResult { + const menuContainerEl = result.queryByRole('listbox'); + + const calendarGrids = result.queryAllByRole( + 'grid', + ) as Array; + + const calendarCells = result.queryAllByRole( + 'gridcell', + ) as Array; + + // label text is tested in DatePickerMenu.spec + const leftChevron = result.queryByLabelText( + 'Previous month', + ) as HTMLButtonElement; + const rightChevron = result.queryByLabelText( + 'Next month', + ) as HTMLButtonElement; + const monthSelect = result.queryByLabelText( + 'Select month', + ) as HTMLButtonElement; + const yearSelect = result.queryByLabelText( + 'Select year', + ) as HTMLButtonElement; + const todayCell = menuContainerEl?.querySelector( + '[aria-current="true"]', + ) as HTMLTableCellElement; + + return { + menuContainerEl, + calendarGrids, + calendarCells, + todayCell, + leftChevron, + rightChevron, + monthSelect, + yearSelect, + }; + } + + function openMenu(): RenderMenuResult { + userEvent.click(inputContainer); + return getMenuElements(); + } + + return { + ...result, + formField, + inputContainer, + inputElements, + calendarButton, + getMenuElements, + openMenu, + }; +}; From 1b552feb5ac5e0d900f641491e3fd4a27fde088c Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 13:39:44 -0400 Subject: [PATCH 197/351] outlines bulk of range tests --- .../src/DatePicker/DatePicker.spec.tsx | 6 +- .../DatePickerInput/DatePickerInput.spec.tsx | 17 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 6 +- .../DateRangeComponent.spec.tsx | 65 ++++++- .../DateRangeInput/DateRangeInput.spec.tsx | 25 ++- ...stories.tsx => DateRangeInput.stories.tsx} | 0 .../DateRangePicker/DateRangePicker.spec.tsx | 178 ++++++++++++++++++ 7 files changed, 281 insertions(+), 16 deletions(-) rename packages/date-picker/src/DateRangePicker/DateRangeInput/{DatePickerInput.stories.tsx => DateRangeInput.stories.tsx} (100%) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index fb642d36e3..a4f34016ed 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -15,7 +15,7 @@ import { newUTC } from '../utils/newUTC'; import { renderDatePicker } from './DatePicker.testutils'; import { DatePicker } from '.'; -const testToday = new Date(Date.UTC(2023, Month.December, 26)); +const testToday = newUTC(2023, Month.December, 26); describe('packages/date-picker', () => { beforeEach(() => { @@ -551,7 +551,7 @@ describe('packages/date-picker', () => { expect(handleValidation).toHaveBeenCalledWith(undefined); }); - test('if menu is closed, enter key on calendar button opens the menu', () => { + test('opens menu if calendar button is focused', () => { const { getMenuElements } = renderDatePicker(); tabNTimes(3); userEvent.keyboard('{enter}'); @@ -559,7 +559,7 @@ describe('packages/date-picker', () => { expect(menuContainerEl).toBeInTheDocument(); }); - test('if month/year select is open, updates the displayed month', async () => { + test('if month/year select is focused, opens the select menu', async () => { const { openMenu, findAllByRole } = renderDatePicker(); const { monthSelect } = openMenu(); tabNTimes(6); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index 1502823a2a..860e464aee 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -8,7 +8,6 @@ import { DatePickerProviderProps, } from '../../DatePickerContext'; import { defaultDatePickerContext } from '../../DatePickerContext/DatePickerContext.utils'; -import { SegmentRefs } from '../../hooks/useSegmentRefs'; import { DatePickerInput, DatePickerInputProps } from '.'; @@ -16,15 +15,9 @@ const renderDatePickerInput = ( props?: Omit, context?: DatePickerProviderProps, ) => { - const segmentRefsMock: SegmentRefs = { - day: React.createRef(), - month: React.createRef(), - year: React.createRef(), - }; - const result = render( - + , ); @@ -66,6 +59,10 @@ describe('packages/date-picker/date-picker-input', () => { userEvent.type(monthInput, '{arrowleft}'); expect(monthInput).toHaveFocus(); }); + + test.todo( + 'focuses the previous segment if the cursor is at the start of the input text', + ); }); describe('Right Arrow', () => { @@ -82,6 +79,10 @@ describe('packages/date-picker/date-picker-input', () => { userEvent.type(monthInput, '{arrowright}'); expect(monthInput).toHaveFocus(); }); + + test.todo( + 'focuses the next segment if the cursor is at the end of the input text', + ); }); describe('Backspace key', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 9527c70922..95b89ac4ef 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -129,16 +129,14 @@ describe('packages/date-picker/date-picker-menu', () => { expect(yearSelect).toHaveValue('2024'); }); }); - }); - describe('Keyboard navigation', () => { test('default highlight is on today', () => { const { todayCell } = renderDatePickerMenu(); userEvent.tab(); expect(todayCell).toHaveFocus(); }); - test('highlight starts on on current value when provided', () => { + test('highlight starts on current value when provided', () => { const { getCellWithValue } = renderDatePickerMenu({ value: testValue, }); @@ -146,7 +144,9 @@ describe('packages/date-picker/date-picker-menu', () => { const valueCell = getCellWithValue(testValue); expect(valueCell).toHaveFocus(); }); + }); + describe('Keyboard navigation', () => { describe('Arrow Keys', () => { test('left arrow moves focus to the previous day', async () => { const { getCellWithValue } = renderDatePickerMenu({ diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx index a808d7c1f2..20231bbd04 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx @@ -1,8 +1,71 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { Month } from '../../constants'; + import { DateRangeComponent } from '.'; +const testToday = new Date(Date.UTC(2023, Month.September, 10)); + describe('packages/date-picker/date-range-picker/date-range-component', () => { - test('condition', () => {}); + describe('Rendering', () => { + test.todo('renders two calendar grids'); + test.todo('left calendar is labelled as the current month'); + test.todo('right calendar is labelled as the following month'); + test.todo('chevrons have aria labels'); + + describe('rendered cells', () => { + test.todo('have correct `aria-label`'); + }); + + describe('with quick select menu', () => { + test.todo('select menu triggers have aria labels'); + test.todo('select menus have correct values'); + test.todo('quick select buttons are rendered'); + }); + + describe('when value is updated', () => { + test.todo('grid is labelled as the current month'); + test.todo('select menus have correct values'); + }); + + test.todo('default highlight is on today'); + test.todo('highlight starts on start value when provided'); + test.todo('highlight starts on end value when only end provided'); + }); + + describe('Keyboard navigation', () => { + describe('Arrow Keys', () => { + test.todo('left arrow moves focus to the previous day'); + test.todo('right arrow moves focus to the next day'); + test.todo('up arrow moves focus to the previous week'); + test.todo('down arrow moves focus to the next week'); + + describe('when next day would be out of range', () => { + const props = { + // value: testToday, + }; + + test.todo('left arrow does nothing'); + test.todo('right arrow does nothing'); + test.todo('up arrow does nothing'); + test.todo('down arrow does nothing'); + }); + + describe('update the displayed month', () => { + test.todo('left arrow updates displayed month to previous'); + test.todo('right arrow updates displayed month to next'); + test.todo('up arrow updates displayed month to previous'); + test.todo('down arrow updates displayed month to next'); + test.todo('does not update month when month does not need to change'); + }); + + describe('when month should be updated', () => { + test.todo('left arrow focuses the previous day'); + test.todo('right arrow focuses the next day'); + test.todo('up arrow focuses the previous week'); + test.todo('down arrow focuses the next week'); + }); + }); + }); }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx index e7ab2b16ac..5451ca576f 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx @@ -4,5 +4,28 @@ import { render } from '@testing-library/react'; import { DateRangeInput } from '.'; describe('packages/date-picker/date-range-picker/date-range-input', () => { - test('condition', () => {}); + describe('Keyboard interaction', () => { + // yyyy-mm-dd + describe('Left Arrow', () => { + test.todo('moves the cursor when the segment has a value'); + test.todo('focuses the previous segment when the segment is empty'); + test.todo( + 'focuses the previous segment if the cursor is at the start of the input text', + ); + }); + + describe('Right Arrow', () => { + test.todo('moves the cursor when the segment has a value'); + test.todo('focuses the next segment when the segment is empty'); + test.todo( + 'focuses the next segment if the cursor is at the end of the input text', + ); + }); + + describe('Backspace key', () => { + test.todo('deletes any value in the input'); + test.todo('deletes the whole value on multiple presses'); + test.todo('focuses the previous segment if current segment is empty'); + }); + }); }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx similarity index 100% rename from packages/date-picker/src/DateRangePicker/DateRangeInput/DatePickerInput.stories.tsx rename to packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index 1a1cb6a0cb..3b3d3b00f8 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; +import range from 'lodash/range'; import { Month } from '../constants'; import { newUTC } from '../utils'; @@ -7,7 +8,14 @@ import { newUTC } from '../utils'; import { renderDateRangePicker } from './DateRangePicker.testutils'; import { DateRangePicker } from '.'; +const testToday = newUTC(2023, Month.December, 26); + describe('packages/date-picker/date-range-picker', () => { + beforeEach(() => { + // Set the current time to midnight UTC on 2023-12-26 + jest.useFakeTimers().setSystemTime(testToday); + }); + describe('Rendering', () => { /// Note: Many rendering tests should be handled by Chromatic @@ -120,4 +128,174 @@ describe('packages/date-picker/date-range-picker', () => { }); }); }); + + describe('Interaction', () => { + describe('Mouse interaction', () => { + describe('Clicking the input', () => { + test.todo('opens the menu'); + test.todo('focuses the clicked segment'); + test.todo('focuses the first segment when all are empty'); + test.todo('focuses the first empty segment in start input'); + test.todo('focuses the first empty segment in end input'); + test.todo('focuses the last segment when all are filled'); + }); + + describe('Clicking a calendar cell', () => { + test.todo( + 'if no value is set, fires a change handler for the start date', + ); + test.todo( + 'if only start value is set, fires change handler for the end date', + ); + test.todo( + 'if only end value is set, fires change handler for the start date', + ); + + describe('if a full range is set', () => { + test.todo('fires a change handler for start date'); + test.todo('fires a change handler to clear the end date'); + }); + }); + + describe('Clicking the Apply button', () => { + test.todo('fires a change handler with the current input value'); + }); + + describe('Clicking the Cancel button', () => { + test.todo('fires an onCancel handler'); + test.todo('fires a change handler with the previous input value'); + }); + + describe('Clicking the Clear button', () => { + test.todo('fires an onClear handler'); + test.todo('fires a change handler with the to clear the range values'); + }); + + describe('Clicking a Chevron', () => { + describe('Left', () => { + test.todo('does not close the menu'); + + test.todo('updates the displayed month to the previous'); + + test.todo( + 'updates the displayed month to the previous, and updates year', + ); + }); + + describe('Right', () => { + test.todo('does not close the menu'); + + test.todo('updates the displayed month to the next'); + + test.todo('updates the displayed month to the next and updates year'); + }); + }); + + describe('Month select menu', () => { + test.todo('menu opens over the calendar menu'); + + test.todo('selecting the month updates the calendar'); + }); + + describe('Year select menu', () => { + test.todo('menu opens over the calendar menu'); + + test.todo('selecting the year updates the calendar'); + }); + + describe('Clicking backdrop', () => { + test.todo('closes the menu'); + test.todo('does not fire a change handler'); + }); + }); + + describe('Keyboard interaction', () => { + describe('Tab', () => { + test.todo('menu does not open on initial focus'); + + const closedTabStops = 3 + 3 + 1; // start + end + button + const basicMenuTabStops = closedTabStops + 3; // chevrons + cell + const quickSelectTabStops = basicMenuTabStops + 2 + 7; // selects + quick select buttons + + describe('Tab order', () => { + describe.each(range(0, closedTabStops))('when menu is closed', n => { + test.todo(`Tab ${n} times`); + }); + + describe.each(range(0, basicMenuTabStops))( + 'when basic menu is open', + n => { + test.todo(`Tab ${n} times`); + }, + ); + + describe.each(range(0, quickSelectTabStops))( + 'when quick select menu is open', + n => { + test.todo(`Tab ${n} times`); + }, + ); + }); + + test.todo('calls validation handler when last segment is unfocused'); + test.todo('does not call validation handler when changing segment'); + }); + + describe('Enter key', () => { + test.todo('if menu is closed, does not open the menu'); + test.todo('opens menu if calendar button is focused'); + test.todo('calls validation handler'); + test.todo('if month/year select is focused, opens the select menu'); + test.todo('if a cell is focused, fires a change handler'); + test.todo('if a cell is focused, closes the menu'); + test.todo('if a Chevron is focused, updates the displayed month'); + test.todo('if Quick Select button is clicked, fires change handler'); + }); + + describe('Escape key', () => { + test.todo('closes the menu'); + test.todo('does not fire a change handler'); + test.todo('fires a validation handler'); + test.todo('focus remains in the input element'); + }); + + /** + * Arrow Keys: + * Since arrow key behavior changes based on whether the input or menu is focused, + * many of these tests exist in the "DatePickerInput" and "DatePickerMenu" components + */ + }); + + describe('Typing', () => { + test.todo('opens the menu'); + + describe('into start date', () => { + test.todo('updates segment value'); + test.todo('does not fire range change handler'); + test.todo('does not fire segment change handler'); + + describe('on un-focus/blur', () => { + test.todo('fires a change handler if the value is valid'); + test.todo('does not fire a change handler if value is incomplete'); + test.todo('fires a segment change handler'); + test.todo('fires a validation handler when the value is first set'); + test.todo('fires a validation handler when the value is updated'); + }); + }); + + describe('into end date', () => { + test.todo('updates segment value'); + test.todo('does not fire range change handler'); + test.todo('does not fire segment change handler'); + + describe('on un-focus/blur', () => { + test.todo('fires a change handler if the value is valid'); + test.todo('does not fire a change handler if value is incomplete'); + test.todo('fires a segment change handler'); + test.todo('fires a validation handler when the value is first set'); + test.todo('fires a validation handler when the value is updated'); + }); + }); + }); + }); }); From e18c758d44e3ba6ed49df464b3aa3706dafdeabe Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 14:13:49 -0400 Subject: [PATCH 198/351] refactor to value/onchange --- .../DateRangeComponent.types.ts | 3 +- .../DateRangeInput/DateRangeInput.tsx | 9 ++++-- .../DateRangeInput/DateRangeInput.types.ts | 5 +++- .../DateRangeMenu/DateRangeMenu.tsx | 7 +++-- .../DateRangeMenu/DateRangeMenu.types.ts | 2 +- .../DateRangeMenuProvider.tsx | 9 +++--- .../DateRangePicker/DateRangePicker.spec.tsx | 16 +++++++---- .../DateRangePicker.stories.tsx | 19 ++----------- .../src/DateRangePicker/DateRangePicker.tsx | 14 ++++------ .../DateRangePicker/DateRangePicker.types.ts | 28 +++++++++---------- packages/date-picker/src/index.ts | 2 ++ packages/date-picker/src/types.ts | 1 + 12 files changed, 57 insertions(+), 58 deletions(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts index d9c3abc30b..6bf1d0996a 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts @@ -1,4 +1,5 @@ -import { DateRangePickerProps, DateRangeType } from '../DateRangePicker.types'; +import { DateRangeType } from '../../types'; +import { DateRangePickerProps } from '../DateRangePicker.types'; export interface DateRangeComponentProps extends Pick { diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index c88f605a3b..7127d1da9c 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -20,7 +20,10 @@ import { DateRangeInputProps } from './DateRangeInput.types'; const EN_DASH = '–'; export const DateRangeInput = forwardRef( - ({ start, end, handleValidation, ...rest }: DateRangeInputProps, fwdRef) => { + ( + { value, onChange, handleValidation, ...rest }: DateRangeInputProps, + fwdRef, + ) => { const { disabled, formatParts, setOpen } = useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); @@ -74,12 +77,12 @@ export const DateRangeInput = forwardRef( } case keyMap.Enter: - handleValidation?.([start || null, end || null]); + handleValidation?.(value); break; case keyMap.Escape: setOpen(false); - handleValidation?.([start || null, end || null]); + handleValidation?.(value); break; default: diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts index 003152f17d..76fb6ece75 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts @@ -1,4 +1,7 @@ import { DateRangePickerProps } from '../DateRangePicker.types'; export interface DateRangeInputProps - extends Pick {} + extends Pick< + DateRangePickerProps, + 'value' | 'onChange' | 'handleValidation' + > {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx index 860f384a6e..48c96b3369 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -16,13 +16,16 @@ import { DateRangeMenuFooter } from './DateRangeMenuFooter'; import { QuickSelectionMenu } from './QuickSelectionMenu'; export const DateRangeMenu = forwardRef( - ({ start, end, showQuickSelection, ...rest }: DateRangeMenuProps, fwdRef) => { + ( + { value, onChange, showQuickSelection, ...rest }: DateRangeMenuProps, + fwdRef, + ) => { const { isOpen } = useDatePickerContext(); // TODO: Focus trap return ( - + & Pick< DateRangePickerProps, - 'start' | 'end' | 'showQuickSelection' | 'handleValidation' // TODO: Setter + 'value' | 'onChange' | 'showQuickSelection' | 'handleValidation' // TODO: Setter > & {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx index 6081a20fcd..fd80b98333 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx @@ -9,7 +9,7 @@ import { DateRangeMenuProps } from '../DateRangeMenu.types'; import { DateRangeMenuContext } from './DateRangeMenuContext'; export interface DateRangeMenuProviderProps - extends Pick, + extends Pick, PropsWithChildren<{}> {} /** @@ -17,16 +17,15 @@ export interface DateRangeMenuProviderProps * and initializes the start & end display months */ export const DateRangeMenuProvider = ({ - start, - end, + value, children, }: DateRangeMenuProviderProps) => { const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); const thisMonth = useMemo(() => getFirstOfMonth(today), [today]); - const [startMonth, setStartMonth] = useState(start ?? thisMonth); + const [startMonth, setStartMonth] = useState(value?.[0] ?? thisMonth); const [endMonth, setEndMonth] = useState( - end ?? addMonths(thisMonth, 1), + value?.[1] ?? addMonths(thisMonth, 1), ); const startCellRefs = useDynamicRefs(); diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index 3b3d3b00f8..a1c051f3c0 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -59,8 +59,10 @@ describe('packages/date-picker/date-range-picker', () => { test('renders `start` & `end` prop', () => { const { inputElements } = renderDateRangePicker({ - start: newUTC(2023, Month.January, 5), - end: newUTC(2023, Month.February, 14), + value: [ + newUTC(2023, Month.January, 5), + newUTC(2023, Month.February, 14), + ], }); expect(inputElements[0]).toEqual('05'); expect(inputElements[1]).toEqual('01'); @@ -72,8 +74,10 @@ describe('packages/date-picker/date-range-picker', () => { test('renders `initialStart` & `initialEnd` prop', () => { const { inputElements } = renderDateRangePicker({ - initialStart: newUTC(2023, Month.July, 5), - initialEnd: newUTC(2023, Month.August, 10), + initialValue: [ + newUTC(2023, Month.July, 5), + newUTC(2023, Month.August, 10), + ], }); expect(inputElements[0]).toEqual('05'); expect(inputElements[1]).toEqual('07'); @@ -113,7 +117,7 @@ describe('packages/date-picker/date-range-picker', () => { test('if a value is set, menu opens to the month of that value', () => { const { openMenu } = renderDateRangePicker({ - start: newUTC(2023, Month.March, 10), + value: [newUTC(2023, Month.March, 10), null], }); const { calendarGrids } = openMenu(); expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'March 2023'); @@ -121,7 +125,7 @@ describe('packages/date-picker/date-range-picker', () => { test('renders the appropriate number of cells', () => { const { openMenu } = renderDateRangePicker({ - start: newUTC(2024, Month.February, 14), + value: [newUTC(2024, Month.February, 14), null], }); const { calendarCells } = openMenu(); expect(calendarCells).toHaveLength(29 + 31); diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx index 5beb7234db..cb2aafad29 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx @@ -12,10 +12,10 @@ import { DatePickerProvider, } from '../DatePickerContext'; import { Locales, TimeZones } from '../testUtils'; +import { DateRangeType } from '../types'; import { newUTC } from '../utils'; import { DateRangePicker } from './DateRangePicker'; -import { DateRangeType } from './DateRangePicker.types'; const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( @@ -80,22 +80,9 @@ const meta: StoryMetaType = { export default meta; export const Basic: StoryFn = props => { - const [start, setStart] = useState(); - const [end, setEnd] = useState(); + const [range, setRange] = useState(); - const setRange = (range?: DateRangeType) => { - setStart(range?.[0]); - setEnd(range?.[1]); - }; - - return ( - - ); + return ; }; // export const Uncontrolled: StoryFn = props => { diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx index 4d0c6b32f9..dd76954281 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx @@ -10,11 +10,9 @@ import { DateRangePickerProps } from './DateRangePicker.types'; export const DateRangePicker = forwardRef( ( { - start: startProp, - initialStart: initialStartProp, - end: endProp, - initialEnd: initialEndProp, - onRangeChange, + value: rangeProp, + initialValue: initialProp, + onChange, showQuickSelection, ...props }: DateRangePickerProps, @@ -23,9 +21,9 @@ export const DateRangePicker = forwardRef( const [contextProps, restProps] = pickAndOmit(props, contextPropNames); const { value: range, setValue: setRange } = useControlledValue( - [startProp || null, endProp || null], - onRangeChange, - [initialStartProp || null, initialEndProp || null], + rangeProp, + onChange, + initialProp, ); return ( diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts b/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts index 61b6e5be45..4e0ae1ba90 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts @@ -1,15 +1,12 @@ import { MouseEventHandler } from 'react'; -import { BaseDatePickerProps, DateType } from '../types'; - -export type DateRangeType = [DateType, DateType]; +import { BaseDatePickerProps, DateRangeType } from '../types'; export interface DateRangePickerProps extends BaseDatePickerProps { - /** The selected start date */ - start?: DateType; - - /** The selected end date */ - end?: DateType; + /** + * The selected start & end date + */ + value?: DateRangeType; /** * Callback fired when the “Apply” button is clicked, or when either the start or end date changes. If either start or end is unset, then that value will be null. @@ -19,13 +16,14 @@ export interface DateRangePickerProps extends BaseDatePickerProps { * * Callback date arguments will be in Date objects in UTC time, or null */ - onRangeChange?: (range?: DateRangeType) => void; + onChange?: (range?: DateRangeType) => void; - /** The initial selected start date. Ignored if `start` is provided */ - initialStart?: DateType; - - /** The initial selected end date. Ignored if `end` is provided */ - initialEnd?: DateType; + /** + * The initial selected start & end dates. + * + * A given initial index is ignored if `range[x]` is defined + */ + initialValue?: DateRangeType; // TODO: onSegmentChange: () => {}; @@ -37,7 +35,7 @@ export interface DateRangePickerProps extends BaseDatePickerProps { * * Callback date arguments will be in Date objects in UTC time, or null */ - handleValidation?: (range: DateRangeType) => void; + handleValidation?: (range?: DateRangeType) => void; /** Callback fired when the “clear” button is clicked. */ onClear?: MouseEventHandler; diff --git a/packages/date-picker/src/index.ts b/packages/date-picker/src/index.ts index adee6daa2b..fda2104d4f 100644 --- a/packages/date-picker/src/index.ts +++ b/packages/date-picker/src/index.ts @@ -1 +1,3 @@ export { DatePicker, type DatePickerProps } from './DatePicker'; +export { DateRangePicker, type DateRangePickerProps } from './DateRangePicker'; +export { type DateRangeType, type DateType } from './types'; diff --git a/packages/date-picker/src/types.ts b/packages/date-picker/src/types.ts index b127208654..ec8bde23e0 100644 --- a/packages/date-picker/src/types.ts +++ b/packages/date-picker/src/types.ts @@ -9,6 +9,7 @@ export type DatePickerState = (typeof DatePickerState)[keyof typeof DatePickerState]; export type DateType = Date | null; +export type DateRangeType = [DateType, DateType]; export interface BaseDatePickerProps extends DarkModeProps { /** From 2d870c493b21097d38feb7c975e07a52b8038e73 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 14:38:40 -0400 Subject: [PATCH 199/351] update calendar rendering --- .../DateRangeComponent/DateRangeComponent.tsx | 21 ++++++++----- .../DateRangeComponent.types.ts | 10 +++++-- .../DateRangeInput/DateRangeInput.tsx | 4 +-- .../DateRangeMenuCalendars.tsx | 23 +++++--------- .../DateRangeMenuContext.tsx | 30 ++++++++++--------- .../DateRangeMenuProvider.tsx | 15 +++++----- .../DateRangePicker/DateRangePicker.spec.tsx | 26 ++++++++-------- .../DateRangePicker.stories.tsx | 11 ++++--- .../src/DateRangePicker/DateRangePicker.tsx | 6 ++-- .../src/utils/addMonthsUTC/index.ts | 7 +++++ packages/date-picker/src/utils/index.ts | 1 + 11 files changed, 85 insertions(+), 69 deletions(-) create mode 100644 packages/date-picker/src/utils/addMonthsUTC/index.ts diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx index ba856c94e9..5a350ba2cf 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx @@ -8,13 +8,18 @@ import { DateRangeComponentProps } from './DateRangeComponent.types'; export const DateRangeComponent = forwardRef< HTMLDivElement, DateRangeComponentProps ->(({ ...rest }: DateRangeComponentProps, fwdRef) => { - return ( - <> - - - - ); -}); +>( + ( + { value, setValue, onCancel, onClear, ...rest }: DateRangeComponentProps, + fwdRef, + ) => { + return ( + <> + + + + ); + }, +); DateRangeComponent.displayName = 'DateRangeComponent'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts index 6bf1d0996a..266e4887bf 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts @@ -1,8 +1,12 @@ +import { contextPropNames } from '../../DatePickerContext'; +import { useControlledValue } from '../../hooks/useControlledValue'; import { DateRangeType } from '../../types'; import { DateRangePickerProps } from '../DateRangePicker.types'; export interface DateRangeComponentProps - extends Pick { - range?: DateRangeType; - setRange: (newVal?: DateRangeType | undefined) => void; + extends Omit< + DateRangePickerProps, + (typeof contextPropNames)[number] | 'onChange' + > { + setValue: ReturnType>['setValue']; } diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index 7127d1da9c..8e653af508 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -99,9 +99,9 @@ export const DateRangeInput = forwardRef( {...rest} >
- + {EN_DASH} - +
); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index d7c2017e5c..0c020e8c8a 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -20,25 +20,18 @@ import { } from './DateRangeMenuCalendars.styles'; export const DateRangeMenuCalendars = forwardRef(() => { - const { - startMonth, - // setStartMonth, - startCellRefs, - endMonth, - // setEndMonth, - endCellRefs, - today, - } = useDateRangeMenuContext(); + const { month, nextMonth, startCellRefs, endCellRefs, today } = + useDateRangeMenuContext(); return (
{(day, i) => ( (() => { {(day, i) => ( (() => { - {getFullMonthLabel(startMonth)} + {getFullMonthLabel(month)}
- {getFullMonthLabel(endMonth)} + {getFullMonthLabel(nextMonth)} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx index 25b70e5462..8cb33e9a02 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx @@ -3,21 +3,24 @@ import React, { createContext, useContext } from 'react'; import { DynamicRefGetter } from '@leafygreen-ui/hooks'; export interface DateRangeMenuContextProps { - /** The month displayed on the left */ - startMonth: Date; + /** + * The month displayed on the left + */ + month: Date; - /** Setter for the start month */ - setStartMonth: React.Dispatch>; + /** + * Setter for the displayed month + */ + setMonth: React.Dispatch>; + + /** + * The month displayed on the right + */ + nextMonth: Date; /** A dynamic ref setter/getter for the start calendar cells */ startCellRefs: DynamicRefGetter; - /** The month displayed on the right */ - endMonth: Date; - - /** Setter for the end month */ - setEndMonth: React.Dispatch>; - /** A dynamic ref setter/getter for the end calendar cells */ endCellRefs: DynamicRefGetter; @@ -26,11 +29,10 @@ export interface DateRangeMenuContextProps { } export const DateRangeMenuContext = createContext({ - startMonth: new Date(), - setStartMonth: () => {}, + month: new Date(), + nextMonth: new Date(), + setMonth: () => {}, startCellRefs: (() => undefined) as DynamicRefGetter, - endMonth: new Date(), - setEndMonth: () => {}, endCellRefs: (() => undefined) as DynamicRefGetter, }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx index fd80b98333..7d55d40a3c 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx @@ -4,6 +4,7 @@ import { addMonths } from 'date-fns'; import { useDynamicRefs } from '@leafygreen-ui/hooks'; import { getFirstOfMonth, setToUTCMidnight } from '../../../utils'; +import { addMonthsUTC } from '../../../utils'; import { DateRangeMenuProps } from '../DateRangeMenu.types'; import { DateRangeMenuContext } from './DateRangeMenuContext'; @@ -21,12 +22,11 @@ export const DateRangeMenuProvider = ({ children, }: DateRangeMenuProviderProps) => { const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); - const thisMonth = useMemo(() => getFirstOfMonth(today), [today]); - const [startMonth, setStartMonth] = useState(value?.[0] ?? thisMonth); - const [endMonth, setEndMonth] = useState( - value?.[1] ?? addMonths(thisMonth, 1), + const [month, setMonth] = useState( + getFirstOfMonth(value?.[0] ?? today), ); + const nextMonth = useMemo(() => addMonthsUTC(month, 1), [month]); const startCellRefs = useDynamicRefs(); const endCellRefs = useDynamicRefs(); @@ -34,11 +34,10 @@ export const DateRangeMenuProvider = ({ return ( { newUTC(2023, Month.February, 14), ], }); - expect(inputElements[0]).toEqual('05'); - expect(inputElements[1]).toEqual('01'); - expect(inputElements[2]).toEqual('2023'); - expect(inputElements[3]).toEqual('14'); - expect(inputElements[4]).toEqual('02'); - expect(inputElements[5]).toEqual('2023'); + expect(inputElements[0].value).toEqual('2023'); + expect(inputElements[1].value).toEqual('01'); + expect(inputElements[2].value).toEqual('05'); + + expect(inputElements[3].value).toEqual('2023'); + expect(inputElements[4].value).toEqual('02'); + expect(inputElements[5].value).toEqual('14'); }); test('renders `initialStart` & `initialEnd` prop', () => { @@ -79,12 +80,13 @@ describe('packages/date-picker/date-range-picker', () => { newUTC(2023, Month.August, 10), ], }); - expect(inputElements[0]).toEqual('05'); - expect(inputElements[1]).toEqual('07'); - expect(inputElements[2]).toEqual('2023'); - expect(inputElements[3]).toEqual('10'); - expect(inputElements[4]).toEqual('08'); - expect(inputElements[5]).toEqual('2023'); + expect(inputElements[0].value).toEqual('2023'); + expect(inputElements[1].value).toEqual('07'); + expect(inputElements[2].value).toEqual('05'); + + expect(inputElements[3].value).toEqual('2023'); + expect(inputElements[4].value).toEqual('08'); + expect(inputElements[5].value).toEqual('10'); }); describe('Menu', () => { diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx index cb2aafad29..d3019b94f6 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx @@ -80,13 +80,16 @@ const meta: StoryMetaType = { export default meta; export const Basic: StoryFn = props => { - const [range, setRange] = useState(); + const [range, setRange] = useState([ + newUTC(2023, Month.October, 14), + newUTC(2023, Month.December, 26), + ]); return ; }; -// export const Uncontrolled: StoryFn = props => { -// return ; -// }; +export const Uncontrolled: StoryFn = props => { + return ; +}; // export const Generated = () => {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx index dd76954281..ef6b0236cb 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx @@ -20,7 +20,7 @@ export const DateRangePicker = forwardRef( ) => { const [contextProps, restProps] = pickAndOmit(props, contextPropNames); - const { value: range, setValue: setRange } = useControlledValue( + const { value, setValue } = useControlledValue( rangeProp, onChange, initialProp, @@ -30,8 +30,8 @@ export const DateRangePicker = forwardRef( diff --git a/packages/date-picker/src/utils/addMonthsUTC/index.ts b/packages/date-picker/src/utils/addMonthsUTC/index.ts new file mode 100644 index 0000000000..361c3b4c57 --- /dev/null +++ b/packages/date-picker/src/utils/addMonthsUTC/index.ts @@ -0,0 +1,7 @@ +import { setUTCMonth } from '../setUTCMonth'; + +export const addMonthsUTC = (date: Date, months: number): Date => { + const utcMonth = date.getUTCMonth(); + const newDate = setUTCMonth(date, utcMonth + months); + return newDate; +}; diff --git a/packages/date-picker/src/utils/index.ts b/packages/date-picker/src/utils/index.ts index ae08549a5a..44e9fb8274 100644 --- a/packages/date-picker/src/utils/index.ts +++ b/packages/date-picker/src/utils/index.ts @@ -1,3 +1,4 @@ +export { addMonthsUTC } from './addMonthsUTC'; export { cloneReverse } from './cloneReverse'; export { getDaysInUTCMonth } from './getDaysInUTCMonth'; export { getFirstEmptySegment } from './getFirstEmptySegment'; From 91aa96dd6be4f0b235ed9e271110eb0fd54afb8e Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 16:04:23 -0400 Subject: [PATCH 200/351] scaffold calendar handlers --- .../DatePickerMenu/DatePickerMenu.tsx | 1 + .../DateRangeComponent/DateRangeComponent.tsx | 6 ++- .../DateRangeMenu/DateRangeMenu.tsx | 14 +++++- .../DateRangeMenu/DateRangeMenu.types.ts | 11 +++-- .../DateRangeMenuCalendars.tsx | 47 ++++++++++++++----- .../DateRangeMenuProvider.tsx | 1 - .../QuickSelectionMenu/QuickSelectionMenu.tsx | 17 +++---- 7 files changed, 67 insertions(+), 30 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index a961751b62..a076db927d 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -132,6 +132,7 @@ export const DatePickerMenu = forwardRef( } }; + // Focus trap const handleWrapperTabKeyPress: KeyboardEventHandler< HTMLDivElement > = e => { diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx index 5a350ba2cf..32f084386e 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx @@ -16,7 +16,11 @@ export const DateRangeComponent = forwardRef< return ( <> - + {}} + /> ); }, diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx index 48c96b3369..2d30ddf309 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -17,7 +17,13 @@ import { QuickSelectionMenu } from './QuickSelectionMenu'; export const DateRangeMenu = forwardRef( ( - { value, onChange, showQuickSelection, ...rest }: DateRangeMenuProps, + { + value, + setValue, + onCellClick, + showQuickSelection, + ...rest + }: DateRangeMenuProps, fwdRef, ) => { const { isOpen } = useDatePickerContext(); @@ -37,7 +43,11 @@ export const DateRangeMenu = forwardRef( >
{showQuickSelection && } - +
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts index 5826640beb..536fd85d78 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts @@ -1,10 +1,13 @@ import { PopoverProps, PortalControlProps } from '@leafygreen-ui/popover'; -import { DateRangePickerProps } from '../DateRangePicker.types'; +import { DateRangeComponentProps } from '../DateRangeComponent'; export type DateRangeMenuProps = PortalControlProps & Pick & Pick< - DateRangePickerProps, - 'value' | 'onChange' | 'showQuickSelection' | 'handleValidation' // TODO: Setter - > & {}; + DateRangeComponentProps, + 'value' | 'setValue' | 'showQuickSelection' | 'handleValidation' // TODO: Setter + > & { + /** Callback fired when a cell is clicked */ + onCellClick: (cellDate: Date) => void; + }; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index 0c020e8c8a..42cd0ca4c5 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -1,15 +1,21 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, KeyboardEventHandler } from 'react'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import { Subtitle } from '@leafygreen-ui/typography'; -import { CalendarCell, CalendarGrid } from '../../../Calendar'; +import { + CalendarCell, + CalendarCellState, + CalendarGrid, +} from '../../../Calendar'; +import { useDatePickerContext } from '../../../DatePickerContext'; import { getFullMonthLabel, getUTCDateString, isSameUTCDay, } from '../../../utils'; +import { DateRangeMenuProps } from '../DateRangeMenu.types'; import { useDateRangeMenuContext } from '../DateRangeMenuContext'; import { @@ -19,18 +25,37 @@ import { calendarsFrameStyles, } from './DateRangeMenuCalendars.styles'; -export const DateRangeMenuCalendars = forwardRef(() => { +export const DateRangeMenuCalendars = forwardRef< + HTMLDivElement, + DateRangeMenuProps +>(({ onCellClick }) => { + const { isInRange } = useDatePickerContext(); + const { month, nextMonth, startCellRefs, endCellRefs, today } = useDateRangeMenuContext(); + /** Creates a click handler for a specific cell date */ + const cellClickHandlerForDay = (day: Date) => () => { + if (isInRange(day)) { + onCellClick(day); + } + }; + + /** Returns the current state of the cell */ + const getCellState = (cellDay: Date | null): CalendarCellState => { + // TODO: + return CalendarCellState.Default; + }; + + /** Called on any keydown within the menu element */ + const handleCalendarKeyDown: KeyboardEventHandler = e => {}; + return (
{(day, i) => ( @@ -40,8 +65,8 @@ export const DateRangeMenuCalendars = forwardRef(() => { aria-label={getUTCDateString(day)} // isHighlighted={isSameUTCDay(day, highlight)} isCurrent={isSameUTCDay(day, today)} - // state={getCellState(day)} - // onClick={cellClickHandlerForDay(day)} + state={getCellState(day)} + onClick={cellClickHandlerForDay(day)} data-iso={day.toISOString()} > {day.getUTCDate()} @@ -50,9 +75,7 @@ export const DateRangeMenuCalendars = forwardRef(() => { @@ -63,8 +86,8 @@ export const DateRangeMenuCalendars = forwardRef(() => { aria-label={getUTCDateString(day)} // isHighlighted={isSameUTCDay(day, highlight)} isCurrent={isSameUTCDay(day, today)} - // state={getCellState(day)} - // onClick={cellClickHandlerForDay(day)} + state={getCellState(day)} + onClick={cellClickHandlerForDay(day)} data-iso={day.toISOString()} > {day.getUTCDate()} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx index 7d55d40a3c..0ac66a949e 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuProvider.tsx @@ -1,5 +1,4 @@ import React, { PropsWithChildren, useMemo, useState } from 'react'; -import { addMonths } from 'date-fns'; import { useDynamicRefs } from '@leafygreen-ui/hooks'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx index c1a1179c51..2c6b078666 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { forwardRef } from 'react'; -import { addMonths } from 'date-fns'; -import { range } from 'lodash'; +import range from 'lodash/range'; import { cx } from '@leafygreen-ui/emotion'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -25,8 +24,7 @@ export const QuickSelectionMenu = forwardRef( (_props, fwdRef) => { const { theme } = useDarkMode(); const { min, max, isInRange } = useDatePickerContext(); - const { startMonth, setStartMonth, setEndMonth } = - useDateRangeMenuContext(); + const { month, setMonth } = useDateRangeMenuContext(); // TODO: is this the right logic? const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); @@ -34,8 +32,7 @@ export const QuickSelectionMenu = forwardRef( const updateMonth = (newMonth: Date) => { // TODO: refine this logic if (isInRange(newMonth)) { - setStartMonth(newMonth); - setEndMonth(addMonths(newMonth, 1)); + setMonth(newMonth); } }; @@ -48,9 +45,9 @@ export const QuickSelectionMenu = forwardRef( { - const newMonth = setUTCYear(startMonth, Number(y)); + const newMonth = setUTCYear(month, Number(y)); updateMonth(newMonth); }} > From 95ea4b6ab9904e24099bd33b799624bfa16087ce Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 17:13:50 -0400 Subject: [PATCH 201/351] updates range input handlers --- .../DatePickerInput/DatePickerInput.tsx | 12 +-- .../utils/getRelativeSegment/index.ts | 51 ++++------- .../DatePicker/utils/getSegmentKey/index.ts | 5 +- .../index.ts | 2 + .../DateRangeInput/DateRangeInput.tsx | 62 +++++++++++-- .../DateRangeMenuCalendars.tsx | 71 +++++++++++---- .../DateRangeMenuContext.tsx | 3 +- .../utils/getRelativeRangeSegment/index.ts | 87 ++++++++++++++++++- 8 files changed, 224 insertions(+), 69 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 78ca1497ea..67cce1a564 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -59,7 +59,7 @@ export const DatePickerInput = forwardRef( // if target is not a segment, do nothing if (!isSegment) return; - const isInputEmpty = isZeroLike(target.value); + const isSegmentEmpty = isZeroLike(target.value); const cursorPosition = target.selectionEnd; switch (key) { @@ -67,7 +67,7 @@ export const DatePickerInput = forwardRef( // if input is empty, // or the cursor is at the beginning of the input // set focus to prev. input (if it exists) - if (isInputEmpty || cursorPosition === 0) { + if (isSegmentEmpty || cursorPosition === 0) { const segmentToFocus = getRelativeSegment('prev', { segment: target, formatParts, @@ -85,7 +85,7 @@ export const DatePickerInput = forwardRef( // if input is empty, // or the cursor is at the end of the input // set focus to next. input (if it exists) - if (isInputEmpty || cursorPosition === target.value.length) { + if (isSegmentEmpty || cursorPosition === target.value.length) { const segmentToFocus = getRelativeSegment('next', { segment: target, formatParts, @@ -108,7 +108,7 @@ export const DatePickerInput = forwardRef( } case keyMap.Backspace: { - if (isInputEmpty) { + if (isSegmentEmpty) { const segmentToFocus = getRelativeSegment('prev', { segment: target, formatParts, @@ -139,13 +139,13 @@ export const DatePickerInput = forwardRef( /** Called when any child of DatePickerInput is blurred */ const handleInputBlur: FocusEventHandler = e => { - const nextFocus = e.relatedTarget; + const nextFocus = e.relatedTarget as HTMLInputElement; // If the next focus is _not_ on a segment if ( !Object.values(segmentRefs) .map(ref => ref.current) - .includes(nextFocus as HTMLInputElement) + .includes(nextFocus) ) { setIsDirty(true); handleValidation?.(value); diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts index 51778ce06a..20b34c8461 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts @@ -2,9 +2,8 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; import { DatePickerContextProps } from '../../../DatePickerContext'; +import { DateSegment } from '../../../hooks/useDateSegments'; import { SegmentRefs } from '../../../hooks/useSegmentRefs'; -import { getSegmentKey } from '../getSegmentKey'; -import { getSegmentRefFromDateTimeFormatPart } from '../getSegmentRefFromDateTimeFormatPart'; type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; interface GetRelativeSegmentContext { @@ -32,24 +31,24 @@ export const getRelativeSegment = ( // only the relevant segments, not separators const formatSegments = formatParts.filter(part => part.type !== 'literal'); + const orderedSegmentRefs = formatSegments.map( + ({ type }) => segmentRefs[type as DateSegment], + ); + + const currentSegmentIndex: number | undefined = orderedSegmentRefs.findIndex( + ref => ref.current === segment, + ); switch (direction) { case 'first': { - const firstSegmentRef = getSegmentRefFromDateTimeFormatPart( - formatSegments[0], - segmentRefs, - ); + const firstSegmentRef = orderedSegmentRefs[0]; return firstSegmentRef; } case 'last': { - const lastSegment = last(formatSegments); + const lastSegmentRef = last(orderedSegmentRefs); - if (lastSegment) { - const lastSegmentRef = getSegmentRefFromDateTimeFormatPart( - lastSegment, - segmentRefs, - ); + if (lastSegmentRef) { return lastSegmentRef; } @@ -57,22 +56,13 @@ export const getRelativeSegment = ( } case 'next': { - const currentSegmentKey = getSegmentKey(segment, segmentRefs); - - if (currentSegmentKey) { - const currentSegmentIndex = formatSegments.findIndex( - p => p.type === currentSegmentKey, - ); - + if (currentSegmentIndex) { const nextSegmentIndex = Math.min( currentSegmentIndex + 1, - formatSegments.length - 1, + orderedSegmentRefs.length - 1, ); - const nextSegmentRef = getSegmentRefFromDateTimeFormatPart( - formatSegments[nextSegmentIndex], - segmentRefs, - ); + const nextSegmentRef = orderedSegmentRefs[nextSegmentIndex]; return nextSegmentRef; } @@ -80,19 +70,10 @@ export const getRelativeSegment = ( } case 'prev': { - const currentSegmentKey = getSegmentKey(segment, segmentRefs); - - if (currentSegmentKey) { - const currentSegmentIndex = formatSegments.findIndex( - p => p.type === currentSegmentKey, - ); - + if (currentSegmentIndex) { const prevSegmentIndex = Math.max(currentSegmentIndex - 1, 0); - const prevSegmentRef = getSegmentRefFromDateTimeFormatPart( - formatSegments[prevSegmentIndex], - segmentRefs, - ); + const prevSegmentRef = orderedSegmentRefs[prevSegmentIndex]; return prevSegmentRef; } diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts index 329eb3cee3..1be0cef9e7 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts @@ -1,6 +1,9 @@ import { SegmentRefs } from '../../../hooks/useSegmentRefs'; -/** Returns the key of a given segment ref or element */ +/** + * Returns the key of a given segment ref or element + * @deprecated + */ export const getSegmentKey = ( segment: HTMLInputElement | React.RefObject, segmentRefs: SegmentRefs, diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts index 5b2755dcc7..c75967c46e 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts @@ -6,6 +6,8 @@ import { SegmentRefs } from '../../../hooks/useSegmentRefs'; /** * Given a {@link Intl.DateTimeFormatPart}, * return the segmentRefs entry with the same type + * + * @deprecated */ export const getSegmentRefFromDateTimeFormatPart = ( formatPart: Intl.DateTimeFormatPart | undefined, diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index 8e653af508..ad64f65d6d 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -1,4 +1,5 @@ import React, { + FocusEventHandler, forwardRef, KeyboardEventHandler, MouseEventHandler, @@ -24,7 +25,8 @@ export const DateRangeInput = forwardRef( { value, onChange, handleValidation, ...rest }: DateRangeInputProps, fwdRef, ) => { - const { disabled, formatParts, setOpen } = useDatePickerContext(); + const { disabled, formatParts, setOpen, setIsDirty } = + useDatePickerContext(); const startSegmentRefs = useSegmentRefs(); const endSegmentRefs = useSegmentRefs(); @@ -54,25 +56,51 @@ export const DateRangeInput = forwardRef( // if target is not a segment, do nothing if (!isSegment) return; - const isInputEmpty = isZeroLike(target.value); + const isSegmentEmpty = isZeroLike(target.value); const cursorPosition = target.selectionEnd; + const ctx = { + target, + formatParts, + rangeSegmentRefs: [startSegmentRefs, endSegmentRefs], + }; + switch (key) { case keyMap.ArrowLeft: - // TODO: - getRelativeRangeSegment(); + // If the segment is empty, + // or if the cursor is at the beginning of the input, + // set focus to prev + if (isSegmentEmpty || cursorPosition === 0) { + const prevSegment = getRelativeRangeSegment('prev', ctx); + + prevSegment?.current?.focus(); + } + break; case keyMap.ArrowRight: - // TODO: + // If the segment is empty, + // or if the cursor is at the end of the input, + // set focus to prev + if (isSegmentEmpty || cursorPosition === target.value.length) { + const nextSegment = getRelativeRangeSegment('next', ctx); + + nextSegment?.current?.focus(); + } break; case keyMap.ArrowDown: - // TODO: - break; case keyMap.ArrowUp: - // TODO: + { + // if decrementing the segment's value is in range + // decrement that segment value + // This is the default `input type=number` behavior + } break; + case keyMap.Backspace: { - // TODO: + if (isSegmentEmpty) { + const prevSegment = getRelativeRangeSegment('prev', ctx); + prevSegment?.current?.focus(); + } break; } @@ -91,11 +119,27 @@ export const DateRangeInput = forwardRef( } }; + /** Called when any child of DatePickerInput is blurred */ + const handleInputBlur: FocusEventHandler = e => { + const nextFocus = e.relatedTarget as HTMLInputElement; + + // If the next focus is _not_ on a segment + if ( + ![startSegmentRefs, endSegmentRefs] + .flatMap(refs => Object.values(refs).map(ref => ref.current)) + .includes(nextFocus) + ) { + setIsDirty(true); + handleValidation?.(value); + } + }; + return (
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index 42cd0ca4c5..df1c3e4fe5 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, KeyboardEventHandler } from 'react'; +import React, { forwardRef, KeyboardEventHandler, useState } from 'react'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; @@ -10,10 +10,12 @@ import { CalendarGrid, } from '../../../Calendar'; import { useDatePickerContext } from '../../../DatePickerContext'; +import { DateType } from '../../../types'; import { getFullMonthLabel, getUTCDateString, isSameUTCDay, + isSameUTCMonth, } from '../../../utils'; import { DateRangeMenuProps } from '../DateRangeMenu.types'; import { useDateRangeMenuContext } from '../DateRangeMenuContext'; @@ -28,11 +30,50 @@ import { export const DateRangeMenuCalendars = forwardRef< HTMLDivElement, DateRangeMenuProps ->(({ onCellClick }) => { +>(({ value, onCellClick }) => { const { isInRange } = useDatePickerContext(); + const { + month, + nextMonth, + setMonth: setDisplayMonth, + startCellRefs, + endCellRefs, + today, + } = useDateRangeMenuContext(); - const { month, nextMonth, startCellRefs, endCellRefs, today } = - useDateRangeMenuContext(); + const [highlight, setHighlight] = useState( + value ? value[0] : today, + ); + + /** setDisplayMonth with side effects */ + const updateMonth = (newMonth: Date) => { + if (isSameUTCMonth(newMonth, month)) { + return; + } + + // const newHighlight = getNewHighlight(highlight, month, newMonth); + // const shouldUpdateHighlight = !isSameUTCDay(highlight, newHighlight); + + // if (newHighlight && shouldUpdateHighlight) { + // setHighlight(newHighlight); + // } + + setDisplayMonth(newMonth); + }; + + /** setHighlight with side effects */ + const updateHighlight = (newHighlight: Date) => { + // change month if nextHighlight is different than `month` or `nextMonth` + if ( + !isSameUTCMonth(month, newHighlight) && + !isSameUTCMonth(nextMonth, newHighlight) + ) { + setDisplayMonth(newHighlight); + } + + // keep track of the highlighted cell + setHighlight(newHighlight); + }; /** Creates a click handler for a specific cell date */ const cellClickHandlerForDay = (day: Date) => () => { @@ -48,22 +89,24 @@ export const DateRangeMenuCalendars = forwardRef< }; /** Called on any keydown within the menu element */ - const handleCalendarKeyDown: KeyboardEventHandler = e => {}; + const handleCalendarKeyDown: KeyboardEventHandler = e => { + // TODO: + }; return (
-
- + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
+ {(day, i) => ( {(day, i) => ( @@ -84,7 +126,7 @@ export const DateRangeMenuCalendars = forwardRef< key={i} ref={endCellRefs(day.toISOString())} aria-label={getUTCDateString(day)} - // isHighlighted={isSameUTCDay(day, highlight)} + isHighlighted={isSameUTCDay(day, highlight)} isCurrent={isSameUTCDay(day, today)} state={getCellState(day)} onClick={cellClickHandlerForDay(day)} @@ -95,7 +137,6 @@ export const DateRangeMenuCalendars = forwardRef< )}
-
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx index 8cb33e9a02..81417937e9 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuContext/DateRangeMenuContext.tsx @@ -25,7 +25,7 @@ export interface DateRangeMenuContextProps { endCellRefs: DynamicRefGetter; /** Memoized reference for Date.now */ - today?: Date; + today: Date; } export const DateRangeMenuContext = createContext({ @@ -34,6 +34,7 @@ export const DateRangeMenuContext = createContext({ setMonth: () => {}, startCellRefs: (() => undefined) as DynamicRefGetter, endCellRefs: (() => undefined) as DynamicRefGetter, + today: new Date(), }); /** Hook to access {@link DateRangeMenuContextProps} */ diff --git a/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts b/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts index b0228d89a8..13f1bc40f0 100644 --- a/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts +++ b/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts @@ -1,3 +1,86 @@ -export const getRelativeRangeSegment = () => { - //TODO: +import React from 'react'; +import isUndefined from 'lodash/isUndefined'; +import last from 'lodash/last'; +import { DateSegment } from 'src/hooks/useDateSegments'; + +import { DatePickerContextProps } from '../../../DatePickerContext'; +import { SegmentRefs } from '../../../hooks/useSegmentRefs'; + +type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; +interface GetRelativeSegmentContext { + target: HTMLInputElement | React.RefObject; + formatParts: DatePickerContextProps['formatParts']; + rangeSegmentRefs: Array; +} + +export const getRelativeRangeSegment = ( + direction: RelativeDirection, + { target, formatParts, rangeSegmentRefs }: GetRelativeSegmentContext, +): React.RefObject | undefined => { + if ( + isUndefined(direction) || + isUndefined(target) || + isUndefined(formatParts) || + isUndefined(rangeSegmentRefs) + ) { + return; + } + + // only the relevant segments, not separators + const formatSegments = formatParts.filter(part => part.type !== 'literal'); + + const orderedSegmentRefs = rangeSegmentRefs.flatMap(segmentRefs => + formatSegments.map(({ type }) => segmentRefs[type as DateSegment]), + ); + + const currentSegmentIndex: number | undefined = orderedSegmentRefs.findIndex( + ref => ref.current === target, + ); + + switch (direction) { + case 'first': { + const firstSegmentRef = orderedSegmentRefs[0]; + return firstSegmentRef; + } + + case 'last': { + const lastSegmentRef = last(orderedSegmentRefs); + + if (lastSegmentRef) { + return lastSegmentRef; + } + + break; + } + + case 'next': { + if (!isUndefined(currentSegmentIndex)) { + const nextSegmentIndex = Math.min( + currentSegmentIndex + 1, + orderedSegmentRefs.length - 1, + ); + + const nextSegmentRef = orderedSegmentRefs[nextSegmentIndex]; + + return nextSegmentRef; + } + + break; + } + + case 'prev': { + if (!isUndefined(currentSegmentIndex)) { + const prevSegmentIndex = Math.max(currentSegmentIndex - 1, 0); + + const prevSegmentRef = orderedSegmentRefs[prevSegmentIndex]; + + return prevSegmentRef; + } + + break; + } + + default: + break; + } }; From b0aac8daec95bf346e66309e3f09dcd2586df668 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 17:39:28 -0400 Subject: [PATCH 202/351] input handlers --- .../DateRangeComponent/DateRangeComponent.tsx | 53 +++++++++++++++++-- .../DateRangeInput/DateRangeInput.tsx | 35 +++++++----- .../DateRangeInput/DateRangeInput.types.ts | 6 +-- .../DateRangeMenu/DateRangeMenu.tsx | 14 +---- .../DateRangeMenu/DateRangeMenu.types.ts | 5 +- .../DateRangeMenuCalendars.styles.ts | 2 +- .../DateRangeMenuCalendars.tsx | 4 +- 7 files changed, 80 insertions(+), 39 deletions(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx index 32f084386e..4d5c342b17 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx @@ -1,5 +1,9 @@ -import React, { forwardRef } from 'react'; +import React, { forwardRef, useRef } from 'react'; +import { useBackdropClick, useForwardedRef } from '@leafygreen-ui/hooks'; + +import { useDatePickerContext } from '../../DatePickerContext'; +import { DateRangeType } from '../../types'; import { DateRangeInput } from '../DateRangeInput'; import { DateRangeMenu } from '../DateRangeMenu'; @@ -10,16 +14,55 @@ export const DateRangeComponent = forwardRef< DateRangeComponentProps >( ( - { value, setValue, onCancel, onClear, ...rest }: DateRangeComponentProps, + { + value, + setValue, + onCancel, + onClear, + showQuickSelection, + ...rest + }: DateRangeComponentProps, fwdRef, ) => { + const { isOpen, setOpen, isDirty, setIsDirty, menuId } = + useDatePickerContext(); + const closeMenu = () => setOpen(false); + + const formFieldRef = useForwardedRef(fwdRef, null); + const menuRef = useRef(null); + + /** setValue with possible side effects */ + const updateValue = (newVal?: DateRangeType) => { + setValue(newVal); + }; + + useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen); + + /** Called when the input's start or end value has changed */ + const handleInputValueChange = (newRange?: DateRangeType) => { + // TODO: more logic here + updateValue(newRange); + }; + + /** Called when any calendar cell is clicked */ + const handleCalendarValueChange = (newRange?: DateRangeType) => { + // TODO: more logic here + updateValue(newRange); + }; + return ( <> - + {}} + setValue={handleCalendarValueChange} /> ); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx index ad64f65d6d..f6b5727246 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx @@ -11,6 +11,7 @@ import { DateInputBox } from '../../DateInput'; import { DateFormField } from '../../DateInput/DateFormField'; import { useDatePickerContext } from '../../DatePickerContext'; import { useSegmentRefs } from '../../hooks/useSegmentRefs'; +import { DateType } from '../../types'; import { isElementInputSegment, isZeroLike } from '../../utils'; import { getRangeSegmentToFocus } from '../utils/getRangeSegmentToFocus'; import { getRelativeRangeSegment } from '../utils/getRelativeRangeSegment'; @@ -22,7 +23,7 @@ const EN_DASH = '–'; export const DateRangeInput = forwardRef( ( - { value, onChange, handleValidation, ...rest }: DateRangeInputProps, + { value, setValue, handleValidation, ...rest }: DateRangeInputProps, fwdRef, ) => { const { disabled, formatParts, setOpen, setIsDirty } = @@ -67,9 +68,6 @@ export const DateRangeInput = forwardRef( switch (key) { case keyMap.ArrowLeft: - // If the segment is empty, - // or if the cursor is at the beginning of the input, - // set focus to prev if (isSegmentEmpty || cursorPosition === 0) { const prevSegment = getRelativeRangeSegment('prev', ctx); @@ -78,9 +76,6 @@ export const DateRangeInput = forwardRef( break; case keyMap.ArrowRight: - // If the segment is empty, - // or if the cursor is at the end of the input, - // set focus to prev if (isSegmentEmpty || cursorPosition === target.value.length) { const nextSegment = getRelativeRangeSegment('next', ctx); @@ -90,9 +85,7 @@ export const DateRangeInput = forwardRef( case keyMap.ArrowDown: case keyMap.ArrowUp: { - // if decrementing the segment's value is in range - // decrement that segment value - // This is the default `input type=number` behavior + // default number input behavior } break; @@ -134,6 +127,16 @@ export const DateRangeInput = forwardRef( } }; + const handleStartInputChange = (newStart: DateType) => { + const end = value ? value[0] : null; + setValue([newStart, end]); + }; + + const handleEndInputChange = (newEnd: DateType) => { + const start = value ? value[0] : null; + setValue([start, newEnd]); + }; + return ( ( {...rest} >
- + {EN_DASH} - +
); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts index 76fb6ece75..3051dc4b49 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts @@ -1,7 +1,7 @@ -import { DateRangePickerProps } from '../DateRangePicker.types'; +import { DateRangeComponentProps } from '../DateRangeComponent'; export interface DateRangeInputProps extends Pick< - DateRangePickerProps, - 'value' | 'onChange' | 'handleValidation' + DateRangeComponentProps, + 'value' | 'setValue' | 'handleValidation' > {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx index 2d30ddf309..ae9f249171 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx @@ -17,13 +17,7 @@ import { QuickSelectionMenu } from './QuickSelectionMenu'; export const DateRangeMenu = forwardRef( ( - { - value, - setValue, - onCellClick, - showQuickSelection, - ...rest - }: DateRangeMenuProps, + { value, setValue, showQuickSelection, ...rest }: DateRangeMenuProps, fwdRef, ) => { const { isOpen } = useDatePickerContext(); @@ -43,11 +37,7 @@ export const DateRangeMenu = forwardRef( >
{showQuickSelection && } - +
diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts index 536fd85d78..49c4976dca 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts @@ -7,7 +7,4 @@ export type DateRangeMenuProps = PortalControlProps & Pick< DateRangeComponentProps, 'value' | 'setValue' | 'showQuickSelection' | 'handleValidation' // TODO: Setter - > & { - /** Callback fired when a cell is clicked */ - onCellClick: (cellDate: Date) => void; - }; + > & {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts index c595a70974..b29b4b3965 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts @@ -9,7 +9,7 @@ export const calendarsFrameStyles = css` grid-template-areas: 'header' 'calendars'; gap: ${spacing[3]}px; padding: ${spacing[4]}px; - padding-top: ${spacing[6]}px; + /* padding-top: ${spacing[6]}px; */ `; export const calendarsContainerStyles = css` diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index df1c3e4fe5..50cf0bdfa3 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -30,7 +30,7 @@ import { export const DateRangeMenuCalendars = forwardRef< HTMLDivElement, DateRangeMenuProps ->(({ value, onCellClick }) => { +>(({ value, setValue }) => { const { isInRange } = useDatePickerContext(); const { month, @@ -78,7 +78,7 @@ export const DateRangeMenuCalendars = forwardRef< /** Creates a click handler for a specific cell date */ const cellClickHandlerForDay = (day: Date) => () => { if (isInRange(day)) { - onCellClick(day); + // TODO: } }; From 0524d78f92dea10dcb58967c5e9ff874139d3d62 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 9 Oct 2023 17:53:40 -0400 Subject: [PATCH 203/351] fills out input tests --- .../DateRangePicker/DateRangePicker.spec.tsx | 55 ++++++++++++++++--- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index 15413c7e6f..f0ed8d5c92 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import range from 'lodash/range'; import { Month } from '../constants'; @@ -138,12 +139,47 @@ describe('packages/date-picker/date-range-picker', () => { describe('Interaction', () => { describe('Mouse interaction', () => { describe('Clicking the input', () => { - test.todo('opens the menu'); - test.todo('focuses the clicked segment'); - test.todo('focuses the first segment when all are empty'); - test.todo('focuses the first empty segment in start input'); - test.todo('focuses the first empty segment in end input'); - test.todo('focuses the last segment when all are filled'); + test('opens the menu', () => { + const { inputContainer, getMenuElements } = renderDateRangePicker(); + userEvent.click(inputContainer); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + }); + + test('focuses the clicked segment', () => { + const { inputElements } = renderDateRangePicker(); + userEvent.click(inputElements[2]); + expect(document.activeElement).toBe(inputElements[2]); + }); + + test('focuses the first segment when all are empty', () => { + const { inputContainer, inputElements } = renderDateRangePicker(); + userEvent.click(inputContainer); + expect(document.activeElement).toBe(inputElements[0]); + }); + + test('focuses the first empty segment in start input', () => { + const { inputContainer, inputElements } = renderDateRangePicker(); + userEvent.type(inputElements[0], '01'); + userEvent.click(inputContainer); + expect(document.activeElement).toBe(inputElements[1]); + }); + + test('focuses the first empty segment in end input', () => { + const { inputContainer, inputElements } = renderDateRangePicker({ + value: [newUTC(2023, 1, 1), null], + }); + userEvent.click(inputContainer); + expect(document.activeElement).toBe(inputElements[3]); + }); + + test('focuses the last segment when all are filled', () => { + const { inputContainer, inputElements } = renderDateRangePicker({ + value: [newUTC(2023, 1, 1), newUTC(2023, 1, 14)], + }); + userEvent.click(inputContainer); + expect(document.activeElement).toBe(inputElements[5]); + }); }); describe('Clicking a calendar cell', () => { @@ -273,7 +309,12 @@ describe('packages/date-picker/date-range-picker', () => { }); describe('Typing', () => { - test.todo('opens the menu'); + test('opens the menu', () => { + const { inputElements, getMenuElements } = renderDateRangePicker(); + userEvent.type(inputElements[0], '1'); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + }); describe('into start date', () => { test.todo('updates segment value'); From 4d97fe814f5c0609a74016f01dc052476d6f89db Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 10 Oct 2023 12:29:50 -0400 Subject: [PATCH 204/351] move typing tests --- .../DateRangePicker/DateRangePicker.spec.tsx | 91 +++++++++++-------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index f0ed8d5c92..eeb63cfe00 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -136,6 +136,60 @@ describe('packages/date-picker/date-range-picker', () => { }); }); + describe('Typing', () => { + test('opens the menu', () => { + const { inputElements, getMenuElements } = renderDateRangePicker(); + userEvent.type(inputElements[0], '1'); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + }); + + describe('into start date', () => { + test('updates segment value', () => { + const { inputElements } = renderDateRangePicker(); + userEvent.type(inputElements[0], '1'); + expect(inputElements[0].value).toBe('1'); // Not '01' if not blurred + }); + test('does not fire range change handler', () => { + const onChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ onChange }); + userEvent.type(inputElements[0], '1'); + expect(onChange).not.toHaveBeenCalled(); + }); + + test.skip('does not fire segment change handler', () => { + const onSegmentChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + onSegmentChange, + }); + userEvent.type(inputElements[0], '1'); + expect(onSegmentChange).not.toHaveBeenCalled(); + }); + + describe('on un-focus/blur', () => { + test.todo('fires a change handler if the value is valid'); + test.todo('does not fire a change handler if value is incomplete'); + test.todo('fires a segment change handler'); + test.todo('fires a validation handler when the value is first set'); + test.todo('fires a validation handler when the value is updated'); + }); + }); + + describe('into end date', () => { + test.todo('updates segment value'); + test.todo('does not fire range change handler'); + test.todo('does not fire segment change handler'); + + describe('on un-focus/blur', () => { + test.todo('fires a change handler if the value is valid'); + test.todo('does not fire a change handler if value is incomplete'); + test.todo('fires a segment change handler'); + test.todo('fires a validation handler when the value is first set'); + test.todo('fires a validation handler when the value is updated'); + }); + }); + }); + describe('Interaction', () => { describe('Mouse interaction', () => { describe('Clicking the input', () => { @@ -307,42 +361,5 @@ describe('packages/date-picker/date-range-picker', () => { * many of these tests exist in the "DatePickerInput" and "DatePickerMenu" components */ }); - - describe('Typing', () => { - test('opens the menu', () => { - const { inputElements, getMenuElements } = renderDateRangePicker(); - userEvent.type(inputElements[0], '1'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - describe('into start date', () => { - test.todo('updates segment value'); - test.todo('does not fire range change handler'); - test.todo('does not fire segment change handler'); - - describe('on un-focus/blur', () => { - test.todo('fires a change handler if the value is valid'); - test.todo('does not fire a change handler if value is incomplete'); - test.todo('fires a segment change handler'); - test.todo('fires a validation handler when the value is first set'); - test.todo('fires a validation handler when the value is updated'); - }); - }); - - describe('into end date', () => { - test.todo('updates segment value'); - test.todo('does not fire range change handler'); - test.todo('does not fire segment change handler'); - - describe('on un-focus/blur', () => { - test.todo('fires a change handler if the value is valid'); - test.todo('does not fire a change handler if value is incomplete'); - test.todo('fires a segment change handler'); - test.todo('fires a validation handler when the value is first set'); - test.todo('fires a validation handler when the value is updated'); - }); - }); - }); }); }); From 6b4ce05033035d8848446735539a2b879d722026 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 10 Oct 2023 12:42:16 -0400 Subject: [PATCH 205/351] Renames onChange to onDateChange --- .../src/DatePicker/DatePicker.spec.tsx | 75 ++++++++++--------- .../src/DatePicker/DatePicker.stories.tsx | 2 +- .../date-picker/src/DatePicker/DatePicker.tsx | 2 +- .../src/DatePicker/DatePicker.types.ts | 6 +- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index a4f34016ed..5b65c5b1a4 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -207,23 +207,22 @@ describe('packages/date-picker', () => { }); }); - // TODO: Move these tests to DatePickerMenu describe('Clicking a Calendar cell', () => { test('fires a change handler', () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ - onChange, + onDateChange, }); const { calendarCells } = openMenu(); const firstCell = calendarCells?.[0]; userEvent.click(firstCell); - expect(onChange).toHaveBeenCalled(); + expect(onDateChange).toHaveBeenCalled(); }); test('does nothing if the cell is out-of-range', () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ - onChange, + onDateChange, value: new Date(Date.UTC(2023, Month.September, 15)), min: new Date(Date.UTC(2023, Month.September, 10)), }); @@ -231,7 +230,7 @@ describe('packages/date-picker', () => { const firstCell = calendarCells?.[0]; userEvent.click(firstCell, {}, { skipPointerEventsCheck: true }); expect(firstCell).toHaveAttribute('aria-disabled', 'true'); - expect(onChange).not.toHaveBeenCalled(); + expect(onDateChange).not.toHaveBeenCalled(); }); test('fires a validation handler', () => { @@ -370,11 +369,11 @@ describe('packages/date-picker', () => { }); test('does not fire a change handler', () => { - const onChange = jest.fn(); - const { openMenu, container } = renderDatePicker({ onChange }); + const onDateChange = jest.fn(); + const { openMenu, container } = renderDatePicker({ onDateChange }); openMenu(); userEvent.click(container.parentElement!); - expect(onChange).not.toHaveBeenCalled(); + expect(onDateChange).not.toHaveBeenCalled(); }); }); }); @@ -570,13 +569,13 @@ describe('packages/date-picker', () => { }); test('if a cell is focused, fires a change handler', () => { - const onChange = jest.fn(); - const { openMenu } = renderDatePicker({ onChange }); + const onDateChange = jest.fn(); + const { openMenu } = renderDatePicker({ onDateChange }); const { todayCell } = openMenu(); tabNTimes(4); expect(todayCell).toHaveFocus(); userEvent.type(todayCell!, '{enter}'); - expect(onChange).toHaveBeenCalled(); + expect(onDateChange).toHaveBeenCalled(); }); test('if a cell is focused, closes the menu', async () => { @@ -600,16 +599,18 @@ describe('packages/date-picker', () => { }); test('does not fire a change handler', () => { - const onChange = jest.fn(); - const { openMenu } = renderDatePicker({ onChange }); + const onDateChange = jest.fn(); + const { openMenu } = renderDatePicker({ onDateChange }); openMenu(); userEvent.keyboard('{escape}'); - expect(onChange).not.toHaveBeenCalled(); + expect(onDateChange).not.toHaveBeenCalled(); }); test('focus remains in the input element', () => { - const onChange = jest.fn(); - const { openMenu, inputContainer } = renderDatePicker({ onChange }); + const onDateChange = jest.fn(); + const { openMenu, inputContainer } = renderDatePicker({ + onDateChange, + }); openMenu(); userEvent.keyboard('{escape}'); expect(inputContainer.contains(document.activeElement)).toBeTruthy(); @@ -644,12 +645,12 @@ describe('packages/date-picker', () => { }); test('does not fire a value change handler', () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { yearInput } = renderDatePicker({ - onChange, + onDateChange, }); userEvent.type(yearInput, '2023'); - expect(onChange).not.toHaveBeenCalled(); + expect(onDateChange).not.toHaveBeenCalled(); }); test('does not fire a segment change handler', () => { @@ -664,13 +665,13 @@ describe('packages/date-picker', () => { describe('when a segment is unfocused/blurred', () => { test('does not fire a change handler if the value is incomplete', () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { yearInput } = renderDatePicker({ - onChange, + onDateChange, }); userEvent.type(yearInput, '2023'); userEvent.tab(); - expect(onChange).not.toHaveBeenCalled(); + expect(onDateChange).not.toHaveBeenCalled(); }); test('fires a segment change handler', () => { @@ -682,15 +683,15 @@ describe('packages/date-picker', () => { }); test('fires a change handler when the value is a valid date', () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ - onChange, + onDateChange, }); userEvent.type(yearInput, '2023'); userEvent.type(monthInput, '12'); userEvent.type(dayInput, '26'); userEvent.tab(); - expect(onChange).toHaveBeenCalledWith( + expect(onDateChange).toHaveBeenCalledWith( expect.objectContaining(newUTC(2023, Month.December, 26)), ); }); @@ -727,22 +728,22 @@ describe('packages/date-picker', () => { describe('Controlled vs Uncontrolled', () => { test('(Controlled) fires a change handler if `value` is provided', async () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ value: new Date(), - onChange, + onDateChange, }); const { calendarCells } = openMenu(); const cell1 = calendarCells?.[0]; userEvent.click(cell1); - await waitFor(() => expect(onChange).toHaveBeenCalled()); + await waitFor(() => expect(onDateChange).toHaveBeenCalled()); }); test('(Controlled) does not change the value if `value` is provided', async () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu, dayInput, monthInput, yearInput } = renderDatePicker({ value: new Date(), - onChange, + onDateChange, }); const { calendarCells } = openMenu(); const cell1 = calendarCells?.[0]; @@ -755,20 +756,20 @@ describe('packages/date-picker', () => { }); test('(Uncontrolled) fires a change handler', async () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ - onChange, + onDateChange, }); const { calendarCells } = openMenu(); const cell1 = calendarCells?.[0]; userEvent.click(cell1); - await waitFor(() => expect(onChange).toHaveBeenCalled()); + await waitFor(() => expect(onDateChange).toHaveBeenCalled()); }); test('(Uncontrolled) changes the input value if `value` is not provided', async () => { - const onChange = jest.fn(); + const onDateChange = jest.fn(); const { openMenu, dayInput, monthInput, yearInput } = renderDatePicker({ - onChange, + onDateChange, initialValue: new Date(), }); const { calendarCells } = openMenu(); diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx index 3e4661cf33..6a8d92861b 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx @@ -80,7 +80,7 @@ export default meta; export const Basic: StoryFn = props => { const [value, setValue] = useState(); - return ; + return ; }; export const Uncontrolled: StoryFn = props => { diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 5f82d8ad1a..ba47423faa 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -16,7 +16,7 @@ export const DatePicker = forwardRef( { value: valueProp, initialValue: initialProp, - onChange: onChangeProp, + onDateChange: onChangeProp, ...props }: DatePickerProps, fwdRef, diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index f388a96dce..b2077ebbc6 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -9,11 +9,13 @@ export interface DatePickerProps extends BaseDatePickerProps { /** * Callback fired when the user makes a value change. - * Fired on click of a new date in the menu, or on keydown if the input contains a valid date + * Fired on click of a new date in the menu, or on keydown if the input contains a valid date. + * + * _Not_ fired when a date segment changes, but does not create a full date * * Callback date argument will be a Date object in ISO-8601 format, and in UTC time. */ - onChange?: (value?: DateType) => void; + onDateChange?: (value?: DateType) => void; /** The initial selected date. Ignored if `value` is provided */ initialValue?: DateType; From 6adb71c7fb5189caa23970595604e2e844e43ce5 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 10 Oct 2023 15:08:41 -0400 Subject: [PATCH 206/351] Updates onSegmentChange to onChange --- .../CalendarButton/CalendarButton.tsx | 3 +- .../DateInputBox/DateInputBox.spec.tsx | 158 +++++++++--------- .../DateInput/DateInputBox/DateInputBox.tsx | 104 ++++++++---- .../DateInputBox/DateInputBox.types.ts | 8 +- .../DateInputSegment.spec.tsx | 134 ++------------- .../DateInputSegment/DateInputSegment.tsx | 44 +---- .../DateInputSegment.types.ts | 5 +- .../src/DatePicker/DatePicker.spec.tsx | 20 ++- .../src/DatePicker/DatePicker.types.ts | 12 +- .../DatePickerComponent.types.ts | 12 +- .../DatePickerInput/DatePickerInput.tsx | 4 +- .../DatePickerInput/DatePickerInput.types.ts | 12 +- .../DatePickerContext.utils.ts | 7 +- .../DateRangeMenu/DateRangeMenu.stories.tsx | 14 +- .../useDateSegments/DateSegments.types.ts | 15 +- .../hooks/useDateSegments/useDateSegments.ts | 16 +- packages/date-picker/src/testUtils.ts | 5 + .../src/utils/getSegmentsFromDate/index.ts | 16 +- 18 files changed, 266 insertions(+), 323 deletions(-) diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx index 9cd3ba00b4..d163eafad8 100644 --- a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx +++ b/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx @@ -12,9 +12,10 @@ import { iconButtonStyles } from './CalendarButton.styles'; export const CalendarButton = forwardRef< HTMLButtonElement, BaseIconButtonProps ->(({ className, ...rest }: BaseIconButtonProps) => { +>(({ className, ...rest }: BaseIconButtonProps, fwdRef) => { return ( { + const testContext = { + dateFormat: 'iso8601', + timeZone: 'UTC', + }; + describe('rendering', () => { describe.each(['day', 'month', 'year'])('%i', segment => { test('renders the correct aria attributes', () => { @@ -91,7 +97,7 @@ describe('packages/date-picker/shared/date-input-box', () => { test('renders an empty text box when no value is passed', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( undefined, - { dateFormat: 'iso8601' }, + testContext, ); expect(dayInput).toHaveValue(null); expect(monthInput).toHaveValue(null); @@ -100,129 +106,125 @@ describe('packages/date-picker/shared/date-input-box', () => { test('renders a filled text box when value is passed', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( - { value: new Date('1993-12-26') }, - { dateFormat: 'iso8601', timeZone: 'UTC' }, + { value: newUTC(1993, Month.December, 26) }, + testContext, ); - expect(dayInput).toHaveValue(26); - expect(monthInput).toHaveValue(12); - expect(yearInput).toHaveValue(1993); + expect(dayInput.value).toBe('26'); + expect(monthInput.value).toBe('12'); + expect(yearInput.value).toBe('1993'); }); }); describe('typing', () => { test('typing into a segment updates the segment value', () => { - const { dayInput } = renderDateInputBox(undefined, { - dateFormat: 'iso8601', - timeZone: 'UTC', - }); - + const { dayInput } = renderDateInputBox(undefined, testContext); userEvent.type(dayInput, '26'); - expect(dayInput).toHaveValue(26); + expect(dayInput.value).toBe('26'); + }); + + test('segment value is not immediately formatted', () => { + const { dayInput } = renderDateInputBox(undefined, testContext); + userEvent.type(dayInput, '2'); + expect(dayInput.value).toBe('2'); + }); + + test('deleting characters works as expected', () => { + const { dayInput, yearInput } = renderDateInputBox( + { value: newUTC(1993, Month.December, 26) }, + testContext, + ); + userEvent.type(dayInput, '{backspace}'); + expect(dayInput.value).toBe('2'); + userEvent.type(yearInput, '{backspace}'); + expect(yearInput.value).toBe('199'); }); - test('typing into a segment does not fire the value change handler ', () => { + test('typing into a segment does not fire the value setter', () => { const setValue = jest.fn(); const { dayInput } = renderDateInputBox( { value: null, setValue, }, - { - dateFormat: 'iso8601', - timeZone: 'UTC', - }, + testContext, ); userEvent.type(dayInput, '26'); expect(setValue).not.toHaveBeenCalled(); }); - test('typing into a segment does not immediately fire the segment change handler', () => { - const onSegmentChange = jest.fn(); + test('typing into a segment fires the change handler', () => { + const onChange = jest.fn(); const { yearInput } = renderDateInputBox( { value: null, - onSegmentChange, - }, - { - dateFormat: 'iso8601', - timeZone: 'UTC', + onChange, }, + testContext, ); userEvent.type(yearInput, '1993'); - expect(onSegmentChange).not.toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith(eventContainingTargetValue('1993')); }); - test('only the segment change handler is fired when the input is blurred', () => { - const setValue = jest.fn(); - const onSegmentChange = jest.fn(); - - const { yearInput } = renderDateInputBox( - { - value: null, - setValue, - onSegmentChange, - }, - { - dateFormat: 'iso8601', - timeZone: 'UTC', - }, - ); - - userEvent.type(yearInput, '1993'); - userEvent.tab(); - expect(setValue).not.toHaveBeenCalled(); - expect(onSegmentChange).toHaveBeenCalledWith('year', 1993); - }); - - test('change handler is called when a complete date is entered, and is blurred', () => { + test('value setter is called when a complete date is entered', () => { const setValue = jest.fn(); const { dayInput, monthInput, yearInput } = renderDateInputBox( { value: null, setValue, }, - { - dateFormat: 'iso8601', - timeZone: 'UTC', - }, + testContext, ); userEvent.type(yearInput, '1993'); userEvent.type(monthInput, '12'); userEvent.type(dayInput, '26'); - expect(setValue).not.toHaveBeenCalled(); - userEvent.tab(); expect(setValue).toHaveBeenCalledWith( expect.objectContaining(newUTC(1993, Month.December, 26)), ); }); - test.todo( - 'typing a complete segment value focuses the next segment', - // () => { - // const { yearInput, monthInput } = renderDateInputBox(undefined, { - // dateFormat: 'iso8601', - // timeZone: 'UTC', - // }); - // userEvent.type(yearInput, '1993'); - // expect(monthInput).toHaveFocus(); - // }, - ); - - test.todo( - 'typing an incomplete segment does not focus the next segment', - // () => { - // const { yearInput } = renderDateInputBox(undefined, { - // dateFormat: 'iso8601', - // timeZone: 'UTC', - // }); - // userEvent.type(yearInput, '200'); - // expect(yearInput).toHaveFocus(); - // }, - ); + test('value is only formatted on segment blur', () => { + const { dayInput } = renderDateInputBox(undefined, testContext); + userEvent.type(dayInput, '2'); + userEvent.tab(); + expect(dayInput.value).toBe('02'); + }); + + // TODO: + describe.skip('Auto-focus', () => { + test('typing a complete segment value focuses the next segment', () => { + const { yearInput, monthInput } = renderDateInputBox( + undefined, + testContext, + ); + userEvent.type(yearInput, '1993'); + expect(monthInput).toHaveFocus(); + }); + + test('typing an incomplete segment does not focus the next segment', () => { + const { monthInput } = renderDateInputBox(undefined, testContext); + userEvent.type(monthInput, '1'); + expect(monthInput).toHaveFocus(); + }); + + test('typing an incomplete value focuses the next segment if there are no valid second characters', () => { + const { monthInput, dayInput } = renderDateInputBox( + undefined, + testContext, + ); + userEvent.type(monthInput, '2'); // There are no months that start with 2# + expect(dayInput).toHaveFocus(); + }); + + test('value is formatted on auto-focus', () => { + const { monthInput } = renderDateInputBox(undefined, testContext); + userEvent.type(monthInput, '2'); // There are no months that start with 2# + expect(monthInput).toHaveValue('02'); + }); + }); }); describe('mouse interaction', () => { @@ -260,7 +262,7 @@ describe('packages/date-picker/shared/date-input-box', () => { }); }); - test('Tab moves focus', () => { + test('Tab moves focus to next segment', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( undefined, { dateFormat: 'iso8601' }, diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx index 394bf9fa24..61bb6309e1 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx @@ -1,8 +1,13 @@ -import React from 'react'; +import React, { + ChangeEvent, + ChangeEventHandler, + FocusEventHandler, +} from 'react'; import { isSameDay } from 'date-fns'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; +import { createSyntheticEvent } from '@leafygreen-ui/lib'; import { useDatePickerContext } from '../../DatePickerContext'; import { @@ -11,7 +16,7 @@ import { isDateSegment, } from '../../hooks/useDateSegments/DateSegments.types'; import { useDateSegments } from '../../hooks/useDateSegments/useDateSegments'; -import { newDateFromSegments } from '../../utils'; +import { getValueFormatter, newDateFromSegments } from '../../utils'; import { DateInputSegment } from '../DateInputSegment'; import { @@ -36,12 +41,12 @@ import { DateInputBoxProps } from './DateInputBox.types'; export const DateInputBox = React.forwardRef( ( { - value, - setValue, + value: dateValue, + setValue: setDateValue, className, labelledBy, segmentRefs, - onSegmentChange, + onChange: onSegmentChange, ...rest }: DateInputBoxProps, fwdRef, @@ -51,45 +56,83 @@ export const DateInputBox = React.forwardRef( const containerRef = useForwardedRef(fwdRef, null); /** - * When a segment is updated, update the external value + * Fires a synthetic change event + * and calls the provided `onChange` handler */ - const onSegmentsUpdate = (newSegments: DateSegmentsState) => { - const { day, month, year } = newSegments; - /** New date in UTC */ - const utcDate = newDateFromSegments({ day, month, year }); + const triggerChangeEventForSegment = (segment: DateSegment) => { + const changeEvent = new Event('change'); + const eventTarget = segmentRefs[segment].current; + + if (eventTarget) { + const reactEvent = createSyntheticEvent( + changeEvent, + eventTarget, + ) as ChangeEvent; + onSegmentChange?.(reactEvent); + } + }; + + /** + * When a segment is updated, + * trigger a `change` event for the segment, and + * update the external Date value if necessary + */ + const onSegmentsUpdate = ( + newSegments: DateSegmentsState, + _prev?: DateSegmentsState, + updatedSegment?: DateSegment, + ) => { + const utcDate = newDateFromSegments(newSegments); + + // Synthetically trigger the onChange event passed in from the parent + if (updatedSegment) { + triggerChangeEventForSegment(updatedSegment); + } else { + Object.keys(newSegments).forEach(seg => { + triggerChangeEventForSegment(seg as DateSegment); + }); + } - // Only update the value iff all parts are set, and create a valid date. if (utcDate) { - /** Whether we need to update the external value */ - const shouldUpdate = !value || !isSameDay(utcDate, value); + // Only update the value iff all parts are set, and create a valid date. + const shouldUpdate = !dateValue || !isSameDay(utcDate, dateValue); if (shouldUpdate) { - setValue?.(utcDate); + setDateValue?.(utcDate); } - } else if (!(day || month || year)) { + } else if (!(newSegments.day || newSegments.month || newSegments.year)) { // if no segment exists, set the external value to null - setValue?.(null); + setDateValue?.(null); } }; - // Keep track of each date segment - const { segments, setSegment } = useDateSegments(value ?? null, { + /** Keep track of each date segment */ + const { segments, setSegment } = useDateSegments(dateValue, { onUpdate: onSegmentsUpdate, }); - /** - * Curried function that creates a callback function for each segment. - * Sets the segment value - */ - const handleSegmentChange = - (segment: DateSegment) => (newValue: string) => { - const newSegmentValue = Number(newValue); - setSegment(segment, newSegmentValue); - onSegmentChange?.(segment, newSegmentValue); - }; + /** fired when an individual segment value changes */ + const handleSegmentChange: ChangeEventHandler = e => { + const segmentName = e.target.getAttribute('id'); + const newValue = e.target.value; + + if (isDateSegment(segmentName)) { + setSegment(segmentName, newValue); + } + }; + + const handleSegmentBlur: FocusEventHandler = e => { + const segmentName = e.target.getAttribute('id'); + const newValue = e.target.value; + + if (isDateSegment(segmentName)) { + const formatter = getValueFormatter(segmentName); + const formattedValue = formatter(newValue); + setSegment(segmentName, formattedValue); + } + }; return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions
( aria-labelledby={labelledBy} segment={part.type} value={segments[part.type]} - onChange={handleSegmentChange(part.type)} + onChange={handleSegmentChange} + onBlur={handleSegmentBlur} /> ); } diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts index 6dece53d6e..4ead06f630 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts +++ b/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts @@ -1,6 +1,7 @@ +import { ChangeEventHandler } from 'react'; + import { HTMLElementProps } from '@leafygreen-ui/lib'; -import { DateSegment, DateSegmentValue } from '../../hooks/useDateSegments'; import { SegmentRefs } from '../../hooks/useSegmentRefs'; import { DateType } from '../../types'; @@ -20,10 +21,7 @@ export interface DateInputBoxProps /** * Callback fired when any segment changes, but not necessarily a full value */ - onSegmentChange?: ( - segment: DateSegment, - segmentValue: DateSegmentValue, - ) => void; + onChange?: ChangeEventHandler; /** * id of the labelling element diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx index d145c5c0c7..50b09e9b35 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { eventContainingTargetValue } from '../../testUtils'; + import { DateInputSegment, type DateInputSegmentProps } from '.'; const handler = jest.fn(); @@ -47,28 +49,18 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); test('Rendering with a value sets the input value', () => { - const { input } = renderSegment({ segment: 'day', value: 12 }); + const { input } = renderSegment({ segment: 'day', value: '12' }); expect(input.value).toBe('12'); }); - test('values get appropriately padded', () => { - const { input } = renderSegment({ segment: 'day', value: 8 }); - expect(input.value).toBe('08'); - }); - - test('values get appropriately truncated', () => { - const { input } = renderSegment({ segment: 'day', value: 123 }); - expect(input.value).toBe('23'); - }); - test('rerendering updates the value', () => { const { input, rerender } = renderSegment({ segment: 'day', - value: 12, + value: '12', }); rerender( - , + , ); expect(input.value).toBe('08'); }); @@ -81,27 +73,21 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); test('Rendering with a value sets the input value', () => { - const { input } = renderSegment({ segment: 'year', value: 2023 }); + const { input } = renderSegment({ segment: 'year', value: '2023' }); expect(input.value).toBe('2023'); }); - test('values get appropriately padded', () => { - const { input } = renderSegment({ segment: 'year', value: 123 }); - expect(input.value).toBe('0123'); - }); - - test('values get appropriately truncated', () => { - const { input } = renderSegment({ segment: 'year', value: 12031 }); - expect(input.value).toBe('2031'); - }); - test('rerendering updates the value', () => { const { input, rerender } = renderSegment({ segment: 'year', - value: 2023, + value: '2023', }); rerender( - , + , ); expect(input.value).toBe('1993'); }); @@ -109,22 +95,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); describe('Typing', () => { - test('does not immediately call the change handler', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, '12'); - expect(handler).not.toHaveBeenCalled(); - }); - }); - - describe('When the input is unfocused (onBlur)', () => { - test('entering a number calls the change handler', () => { + test('calls the change handler', () => { const result = render( { ); const input = result.getByTestId('testid'); userEvent.type(input, '12'); - userEvent.tab(); - expect(handler).toHaveBeenCalledWith('12'); - }); - - test('deleting a value calls the change handler', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, '{backspace}{backspace}'); - userEvent.tab(); - expect(handler).toHaveBeenCalledWith(''); - }); - - test('entering the same value as previous does not call the handler', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, '{backspace}{backspace}12'); - userEvent.tab(); - expect(handler).not.toHaveBeenCalled(); - }); - - test('entering letters does not call the handler', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, 'abc'); - userEvent.tab(); - expect(handler).not.toHaveBeenCalled(); - }); - - test('change handler is called with padded value', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, '1'); - userEvent.tab(); - expect(handler).toHaveBeenCalledWith('01'); - }); - - test('change handler is called with truncated value', () => { - const result = render( - , - ); - const input = result.getByTestId('testid'); - userEvent.type(input, '123'); - userEvent.tab(); - expect(handler).toHaveBeenCalledWith('23'); + expect(handler).toHaveBeenCalledWith(eventContainingTargetValue('12')); }); }); @@ -222,7 +120,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { segment="day" data-testid="testid" onChange={handler} - value={8} + value={'08'} />, ); const input = result.getByTestId('testid'); @@ -236,7 +134,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { segment="day" data-testid="testid" onChange={handler} - value={8} + value={'08'} />, ); const input = result.getByTestId('testid'); diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx index 1a2e5106a9..6178983959 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx @@ -1,10 +1,4 @@ -import React, { - ChangeEventHandler, - FocusEventHandler, - useEffect, - useMemo, - useState, -} from 'react'; +import React from 'react'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; @@ -13,7 +7,6 @@ import { Size } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { useDatePickerContext } from '../../DatePickerContext'; -import { getValueFormatter } from '../../utils'; import { defaultMax, defaultMin, defaultPlaceholder } from './constants'; import { @@ -57,35 +50,6 @@ export const DateInputSegment = React.forwardRef< const baseFontSize = useUpdatedBaseFontSize(); const { size, disabled } = useDatePickerContext(); - const formatValue = useMemo(() => getValueFormatter(segment), [segment]); - - // internally, keep track of the input value - const [internalValue, setInternalValue] = useState( - formatValue(value), - ); - - // If the value changes externally, update the internal value - useEffect(() => { - setInternalValue(formatValue(value)); - }, [formatValue, value]); - - // On input element change, we update the internal value - const handleChange: ChangeEventHandler = e => { - setInternalValue(e.target.value); - }; - - // When the user un-focuses the element, then we fire the change handler - const handleBlur: FocusEventHandler = e => { - const formattedValue = formatValue(internalValue); - - // If the value has changed, call the change handler - if (formattedValue !== formatValue(value)) { - onChange?.(formattedValue); - } - - onBlur?.(e); - }; - return ( , 'onChange'> { + HTMLElementProps<'input'> { /** Which date segment this input represents. Determines the aria-label, and min/max values where relevant */ segment: DateSegment; @@ -19,7 +19,4 @@ export interface DateInputSegmentProps /** Optional maximum value. Defaults to 31 for day, 12 for month, 2038 for year */ max?: number; - - /** Callback fired when the value changes */ - onChange?: (val: string) => void; } diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 5b65c5b1a4..128f07da92 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -9,7 +9,7 @@ import userEvent from '@testing-library/user-event'; import { range } from 'lodash'; import { Month } from '../constants'; -import { tabNTimes } from '../testUtils'; +import { eventContainingTargetValue, tabNTimes } from '../testUtils'; import { newUTC } from '../utils/newUTC'; import { renderDatePicker } from './DatePicker.testutils'; @@ -653,13 +653,15 @@ describe('packages/date-picker', () => { expect(onDateChange).not.toHaveBeenCalled(); }); - test('does not fire a segment change handler', () => { - const onSegmentChange = jest.fn(); + test('fires a segment change handler', () => { + const onChange = jest.fn(); const { yearInput } = renderDatePicker({ - onSegmentChange, + onChange, }); userEvent.type(yearInput, '2023'); - expect(onSegmentChange).not.toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2023'), + ); }); }); @@ -675,11 +677,13 @@ describe('packages/date-picker', () => { }); test('fires a segment change handler', () => { - const onSegmentChange = jest.fn(); - const { yearInput } = renderDatePicker({ onSegmentChange }); + const onChange = jest.fn(); + const { yearInput } = renderDatePicker({ onChange }); userEvent.type(yearInput, '2023'); userEvent.tab(); - expect(onSegmentChange).toHaveBeenCalledWith('year', 2023); + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2023'), + ); }); test('fires a change handler when the value is a valid date', () => { diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index b2077ebbc6..f3c3aff259 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -1,4 +1,5 @@ -import { DateSegment, DateSegmentValue } from '../hooks/useDateSegments'; +import { ChangeEvent } from 'react'; + import { BaseDatePickerProps, DateType } from '../types'; export interface DatePickerProps extends BaseDatePickerProps { @@ -31,8 +32,9 @@ export interface DatePickerProps extends BaseDatePickerProps { /** * Callback fired when any segment changes, but not necessarily a full value */ - onSegmentChange?: ( - segment: DateSegment, - segmentValue: DateSegmentValue, - ) => void; + onChange?: (event: ChangeEvent) => void; + // onSegmentChange?: ( + // segment: DateSegment, + // segmentValue: DateSegmentValue, + // ) => void; } diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts index 18b88d9a77..c2f2975343 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts @@ -1,12 +1,14 @@ -import { contextPropNames } from '../../DatePickerContext/DatePickerContext.utils'; +import { ContextPropKeys } from '../../DatePickerContext/DatePickerContext.utils'; import { useControlledValue } from '../../hooks/useControlledValue'; import { DateType } from '../../types'; import { DatePickerProps } from '../DatePicker.types'; +/** + * Extends {@link DatePickerProps}, + * but omits props that are added to the context. + * Replaces `onDateChange` with a `setValue` setter function + */ export interface DatePickerComponentProps - extends Omit< - DatePickerProps, - (typeof contextPropNames)[number] | 'onChange' - > { + extends Omit { setValue: ReturnType>['setValue']; } diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 67cce1a564..a993f02111 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -25,7 +25,7 @@ export const DatePickerInput = forwardRef( setValue, onClick, onKeyDown, - onSegmentChange, + onChange: onSegmentChange, handleValidation, ...rest }: DatePickerInputProps, @@ -164,7 +164,7 @@ export const DatePickerInput = forwardRef( value={value} setValue={setValue} segmentRefs={segmentRefs} - onSegmentChange={onSegmentChange} + onChange={onSegmentChange} /> ); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts index 2dd51e906d..706d2d999c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts @@ -3,14 +3,14 @@ import { MouseEventHandler } from 'react'; // import { DynamicRefGetter } from '@leafygreen-ui/hooks/src/useDynamicRefs'; import { HTMLElementProps } from '@leafygreen-ui/lib'; -import { DateInputBoxProps } from '../../DateInput'; -// import { DateSegment } from '../../hooks/useDateSegments/DateSegments.types'; -import { DatePickerProps } from '../DatePicker.types'; +import { DatePickerComponentProps } from '../DatePickerComponent'; export interface DatePickerInputProps - extends Pick, - Pick, - HTMLElementProps<'div'> { + extends Pick< + DatePickerComponentProps, + 'setValue' | 'value' | 'handleValidation' | 'onChange' + >, + Omit, 'onChange'> { /** * Click handler */ diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts index cce88a5463..8b664b2893 100644 --- a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts @@ -16,10 +16,11 @@ export const MIN_DATE = new Date('12-31-1969'); export const MAX_DATE = new Date('01-19-2038'); export const TZ = Intl.DateTimeFormat().resolvedOptions().timeZone; +export type ContextPropKeys = keyof DatePickerProviderProps & + keyof BaseDatePickerProps; + /** Prop names that are in both DatePickerProps and DatePickerProviderProps */ -export const contextPropNames: Array< - keyof DatePickerProviderProps & keyof BaseDatePickerProps -> = [ +export const contextPropNames: Array = [ 'label', 'description', 'dateFormat', diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx index 09f966a86a..dd39821678 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx @@ -19,6 +19,7 @@ import { contextPropNames, defaultDatePickerContext, } from '../../DatePickerContext/DatePickerContext.utils'; +import { DateRangeType } from '../../types'; import { newUTC, pickAndOmit } from '../../utils'; import { DateRangeMenu } from './DateRangeMenu'; @@ -78,7 +79,7 @@ type DateRangeMenuStoryType = StoryObj; export const Basic: DateRangeMenuStoryType = { render: args => { - const [value, setValue] = useState(null); + const [value, setValue] = useState(); const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); @@ -89,7 +90,7 @@ export const Basic: DateRangeMenuStoryType = { {...props} refEl={refEl} value={value} - onCellClick={setValue} + setValue={setValue} /> ); @@ -98,6 +99,11 @@ export const Basic: DateRangeMenuStoryType = { export const WithValue: DateRangeMenuStoryType = { render: args => { + const [value, setValue] = useState([ + newUTC(2023, Month.September, 10), + null, + ]); + const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( @@ -106,8 +112,8 @@ export const WithValue: DateRangeMenuStoryType = { {}} + value={value} + setValue={setValue} />
); diff --git a/packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts b/packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts index 13761de89e..45d71daab5 100644 --- a/packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts +++ b/packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts @@ -5,23 +5,26 @@ export const DateSegment = { } as const; export type DateSegment = (typeof DateSegment)[keyof typeof DateSegment]; -export type DateSegmentValue = number; +export type DateSegmentValue = string; export type DateSegmentsState = Record< DateSegment, DateSegmentValue | undefined >; -export function isDateSegment(str: string): str is DateSegment { +export function isDateSegment(str: any): str is DateSegment { + if (typeof str !== 'string') return false; return ['day', 'month', 'year'].includes(str); } -export type OnUpdateCallback = (value: DateSegmentsState) => void; +/** Callback passed into the hook, called when any segment updates */ +export type OnUpdateCallback = ( + value: DateSegmentsState, + previous?: DateSegmentsState, + updatedSegment?: DateSegment, +) => void; export interface UseDateSegmentsOptions { - /** A formatter used to separate the date value into segments */ - // timeZone: string; - /** A callback fired when the segment values change */ onUpdate?: OnUpdateCallback; } diff --git a/packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts b/packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts index 97b035f424..614c4c9b70 100644 --- a/packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts +++ b/packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts @@ -32,8 +32,8 @@ const dateSegmentsReducer = ( * Returned segments are relative to the formatter time zone */ export const useDateSegments = ( - /** Provided date is relative to the client's time zone */ - date: DateType, + /** Provided date is UTC */ + date: DateType = null, { onUpdate }: UseDateSegmentsOptions, ): UseDateSegmentsReturnValue => { // @@ -48,10 +48,10 @@ export const useDateSegments = ( useEffect(() => { if (date && !(prevDate && isSameDay(date, prevDate))) { const newSegments = getSegmentsFromDate(date); - onUpdate?.(newSegments); + onUpdate?.(newSegments, { ...segments }); dispatch(newSegments); } - }, [date, onUpdate, prevDate]); + }, [date, onUpdate, prevDate, segments]); /** * Custom dispatch that triggers the provided side effects, and updates state @@ -60,9 +60,11 @@ export const useDateSegments = ( // Calculate next state // then, execute any side effects based on the new state // finally, commit the new state - const nextState = dateSegmentsReducer(segments, { [segment]: value }); - onUpdate?.(nextState); - dispatch({ [segment]: value }); + + const updateObject = { [segment]: value }; + const nextState = dateSegmentsReducer(segments, updateObject); + onUpdate?.(nextState, { ...segments }, segment); + dispatch(updateObject); }; return { diff --git a/packages/date-picker/src/testUtils.ts b/packages/date-picker/src/testUtils.ts index 8e27ac3d94..a1993b97f4 100644 --- a/packages/date-picker/src/testUtils.ts +++ b/packages/date-picker/src/testUtils.ts @@ -28,3 +28,8 @@ export const tabNTimes = (count: number) => { userEvent.tab(); } }; + +export const eventContainingTargetValue = (value: any) => + expect.objectContaining({ + target: expect.objectContaining({ value }), + }); diff --git a/packages/date-picker/src/utils/getSegmentsFromDate/index.ts b/packages/date-picker/src/utils/getSegmentsFromDate/index.ts index 296efc5c04..7e1a19283b 100644 --- a/packages/date-picker/src/utils/getSegmentsFromDate/index.ts +++ b/packages/date-picker/src/utils/getSegmentsFromDate/index.ts @@ -1,4 +1,5 @@ -import { DateSegmentsState } from '../../hooks/useDateSegments/DateSegments.types'; +import { DateSegmentsState } from '../../hooks/useDateSegments'; +import { getValueFormatter } from '../getValueFormatter'; /** Returns a single object with day, month & year segments */ export const getSegmentsFromDate = (date: Date | null): DateSegmentsState => { @@ -8,3 +9,16 @@ export const getSegmentsFromDate = (date: Date | null): DateSegmentsState => { year: date ? date.getUTCFullYear() : undefined, } as DateSegmentsState; }; + +/** Returns a single object with _formatted_ day, month & year segments */ +export const getFormattedSegmentsFromDate = ( + date: Date | null, +): DateSegmentsState => { + const segments = getSegmentsFromDate(date); + + return { + day: getValueFormatter('day')(segments['day']), + month: getValueFormatter('month')(segments['month']), + year: getValueFormatter('year')(segments['year']), + }; +}; From d6695b2fd45663062feffa43c1b6cac3ca82ffec Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 11 Oct 2023 11:55:31 -0400 Subject: [PATCH 207/351] LG-3675, LG-3676: Date Picker select width and menu width (#2013) * switch to short month and add a fixed width * remove table spacing * rename var --- .../src/Calendar/CalendarGrid/CalendarGrid.styles.ts | 5 +++++ .../src/Calendar/CalendarGrid/CalendarGrid.tsx | 8 +++++++- .../DatePicker/DatePickerMenu/DatePickerMenu.styles.ts | 7 ++++++- .../DatePickerMenu/DatePickerMenuHeader/index.tsx | 5 ++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts index ed3e2b3bb1..9786b3596c 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts +++ b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts @@ -3,9 +3,14 @@ import { fontWeights } from '@leafygreen-ui/tokens'; export const calendarGridStyles = css` height: max-content; + border-collapse: collapse; `; export const calendarHeaderCellStyles = css` font-weight: ${fontWeights.regular}; text-transform: capitalize; `; + +export const calendarThStyles = css` + padding: 0; +`; diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx index 0c8de59508..5b8eea5c25 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx @@ -12,6 +12,7 @@ import { getWeeksArray } from '../../utils'; import { calendarGridStyles, calendarHeaderCellStyles, + calendarThStyles, } from './CalendarGrid.styles'; import { CalendarGridProps } from './CalendarGrid.types'; @@ -55,7 +56,12 @@ export const CalendarGrid = forwardRef( const dayIndex = (i + weekStartsOn) % daysPerWeek; const day = DaysOfWeek[dayIndex]; return ( -
+ {day.short} diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts index 731f27f83b..242ef409b5 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts @@ -3,7 +3,7 @@ import { contentClassName } from '@leafygreen-ui/popover'; import { spacing } from '@leafygreen-ui/tokens'; export const menuWrapperStyles = css` - width: 266px; // width of "September" select trigger + width: 244px; & > .${contentClassName} { width: 100%; @@ -38,3 +38,8 @@ export const menuCalendarGridStyles = css` grid-area: calendar; margin: auto; `; + +// Hardcoding the width +export const selectInputWidthStyles = css` + width: 68.5px; +`; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx index c83b70d52c..65cb038730 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx @@ -12,6 +12,7 @@ import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../utils'; import { menuHeaderSelectContainerStyles, menuHeaderStyles, + selectInputWidthStyles, } from '../DatePickerMenu.styles'; interface DatePickerMenuHeaderProps { @@ -70,10 +71,11 @@ export const DatePickerMenuHeader = forwardRef< const newMonth = setUTCMonth(month, Number(m)); updateMonth(newMonth); }} + className={selectInputWidthStyles} > {Months.map((m, i) => ( ))} @@ -85,6 +87,7 @@ export const DatePickerMenuHeader = forwardRef< const newMonth = setUTCYear(month, Number(y)); updateMonth(newMonth); }} + className={selectInputWidthStyles} > {yearOptions.map(y => ( , + props?: Omit, context?: DatePickerProviderProps, ) => { const result = render( - + {}} /> , ); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx deleted file mode 100644 index 20231bbd04..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { Month } from '../../constants'; - -import { DateRangeComponent } from '.'; - -const testToday = new Date(Date.UTC(2023, Month.September, 10)); - -describe('packages/date-picker/date-range-picker/date-range-component', () => { - describe('Rendering', () => { - test.todo('renders two calendar grids'); - test.todo('left calendar is labelled as the current month'); - test.todo('right calendar is labelled as the following month'); - test.todo('chevrons have aria labels'); - - describe('rendered cells', () => { - test.todo('have correct `aria-label`'); - }); - - describe('with quick select menu', () => { - test.todo('select menu triggers have aria labels'); - test.todo('select menus have correct values'); - test.todo('quick select buttons are rendered'); - }); - - describe('when value is updated', () => { - test.todo('grid is labelled as the current month'); - test.todo('select menus have correct values'); - }); - - test.todo('default highlight is on today'); - test.todo('highlight starts on start value when provided'); - test.todo('highlight starts on end value when only end provided'); - }); - - describe('Keyboard navigation', () => { - describe('Arrow Keys', () => { - test.todo('left arrow moves focus to the previous day'); - test.todo('right arrow moves focus to the next day'); - test.todo('up arrow moves focus to the previous week'); - test.todo('down arrow moves focus to the next week'); - - describe('when next day would be out of range', () => { - const props = { - // value: testToday, - }; - - test.todo('left arrow does nothing'); - test.todo('right arrow does nothing'); - test.todo('up arrow does nothing'); - test.todo('down arrow does nothing'); - }); - - describe('update the displayed month', () => { - test.todo('left arrow updates displayed month to previous'); - test.todo('right arrow updates displayed month to next'); - test.todo('up arrow updates displayed month to previous'); - test.todo('down arrow updates displayed month to next'); - test.todo('does not update month when month does not need to change'); - }); - - describe('when month should be updated', () => { - test.todo('left arrow focuses the previous day'); - test.todo('right arrow focuses the next day'); - test.todo('up arrow focuses the previous week'); - test.todo('down arrow focuses the next week'); - }); - }); - }); -}); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx index 4351732a0a..162064b6c4 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx @@ -1,21 +1,218 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { subDays } from 'date-fns'; +import { addDays, subDays } from 'date-fns'; -import { MIN_DATE, Month } from '../../constants'; -import { DatePickerProvider } from '../../DatePickerContext'; +import { Month } from '../../constants'; +import { + DatePickerProvider, + DatePickerProviderProps, + defaultDatePickerContext, +} from '../../DatePickerContext'; import { DateRangeType } from '../../types'; import { newUTC } from '../../utils'; import { DateRangeProvider, type DateRangeProviderProps, } from '../DateRangeContext'; +import { testToday } from '../DateRangePicker.testutils'; + +import { DateRangeMenu, type DateRangeMenuProps } from '.'; + +const renderDateRangeMenu = (args?: { + props?: DateRangeMenuProps; + rangeContext?: Partial; + datePickerContext?: Partial; +}) => { + const results = render( + + {}} + handleValidation={() => {}} + rootRef={React.createRef()} + {...args?.rangeContext} + > + + + , + ); + + const calendarCells = results.getAllByTestId('lg-date_picker-calendar_cell'); + const todayCell = calendarCells.find( + cell => cell.getAttribute('aria-current') === 'true', + ); + const getCellForDate = (date: Date) => + calendarCells.find(cell => cell.dataset.iso === date.toISOString()); + + return { ...results, calendarCells, todayCell, getCellForDate }; +}; describe('packages/date-picker/date-range-picker/menu', () => { - describe('Keyboard Interaction', () => { - describe('Arrow keys', () => { - test.todo(''); + beforeAll(() => { + // Set the current time to midnight UTC on 2023-12-26 + jest.useFakeTimers().setSystemTime(testToday); + }); + + test('initial focus is on `today`', () => { + const { todayCell } = renderDateRangeMenu(); + expect(todayCell).toHaveFocus(); + }); + + describe('Keyboard navigation', () => { + describe('Arrow Keys', () => { + const initialStart = newUTC(2023, Month.September, 14); + const value = [initialStart, null] as DateRangeType; + const rangeContext = { value }; + + describe('default behavior', () => { + test('left arrow moves focus to the previous day', () => { + const { getCellForDate } = renderDateRangeMenu({ rangeContext }); + userEvent.keyboard('{leftarrow}'); + const focusedCell = getCellForDate(subDays(initialStart, 1)); + expect(focusedCell).toHaveFocus(); + }); + test('right arrow moves focus to the next day', () => { + const { getCellForDate } = renderDateRangeMenu({ rangeContext }); + userEvent.keyboard('{rightarrow}'); + const focusedCell = getCellForDate(addDays(initialStart, 1)); + expect(focusedCell).toHaveFocus(); + }); + test('up arrow moves focus to the previous week', () => { + const { getCellForDate } = renderDateRangeMenu({ rangeContext }); + userEvent.keyboard('{uparrow}'); + const focusedCell = getCellForDate(subDays(initialStart, 7)); + expect(focusedCell).toHaveFocus(); + }); + test('down arrow moves focus to the next week', () => { + const { getCellForDate } = renderDateRangeMenu({ rangeContext }); + userEvent.keyboard('{downarrow}'); + const focusedCell = getCellForDate(addDays(initialStart, 7)); + expect(focusedCell).toHaveFocus(); + }); + }); + + describe('when next day would be out of range', () => { + test('left arrow does nothing', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext, + datePickerContext: { min: subDays(initialStart, 1) }, + }); + userEvent.keyboard('{leftarrow}'); + const focusedCell = getCellForDate(initialStart); + expect(focusedCell).toHaveFocus(); + }); + test('right arrow does nothing', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext, + datePickerContext: { max: addDays(initialStart, 1) }, + }); + userEvent.keyboard('{rightarrow}'); + const focusedCell = getCellForDate(initialStart); + expect(focusedCell).toHaveFocus(); + }); + test('up arrow does nothing', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext, + datePickerContext: { min: subDays(initialStart, 6) }, + }); + userEvent.keyboard('{uparrow}'); + const focusedCell = getCellForDate(initialStart); + expect(focusedCell).toHaveFocus(); + }); + test('down arrow does nothing', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext, + datePickerContext: { max: addDays(initialStart, 7) }, + }); + userEvent.keyboard('{downarrow}'); + const focusedCell = getCellForDate(initialStart); + expect(focusedCell).toHaveFocus(); + }); + }); + + describe('update the displayed month', () => { + test('left arrow updates displayed month to previous', () => { + const { getAllByRole } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 1), null] }, + }); + userEvent.keyboard('{leftarrow}'); + const calendarGrids = getAllByRole('grid'); + expect(calendarGrids[0]).toHaveAttribute('aria-label', 'August 2023'); + }); + test('right arrow updates displayed month to next', () => { + const { getAllByRole } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 30), null] }, + }); + userEvent.keyboard('{rightarrow}'); + const calendarGrids = getAllByRole('grid'); + expect(calendarGrids[0]).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); + test('up arrow updates displayed month to previous', () => { + const { getAllByRole } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 6), null] }, + }); + userEvent.keyboard('{uparrow}'); + const calendarGrids = getAllByRole('grid'); + expect(calendarGrids[0]).toHaveAttribute('aria-label', 'August 2023'); + }); + test('down arrow updates displayed month to next', () => { + const { getAllByRole } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 24), null] }, + }); + userEvent.keyboard('{downarrow}'); + const calendarGrids = getAllByRole('grid'); + expect(calendarGrids[0]).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); + test.todo('does not update month when month does not need to change'); + }); + + describe('when month should be updated', () => { + test('left arrow focuses the previous day', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 1), null] }, + }); + userEvent.keyboard('{leftarrow}'); + const focusedCell = getCellForDate(newUTC(2023, Month.August, 31)); + expect(focusedCell).toHaveFocus(); + }); + test('right arrow focuses the next day', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 30), null] }, + }); + userEvent.keyboard('{rightarrow}'); + const focusedCell = getCellForDate(newUTC(2023, Month.October, 1)); + expect(focusedCell).toHaveFocus(); + }); + test('up arrow focuses the previous week', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 6), null] }, + }); + userEvent.keyboard('{uparrow}'); + const focusedCell = getCellForDate(newUTC(2023, Month.August, 31)); + expect(focusedCell).toHaveFocus(); + }); + test('down arrow focuses the next week', () => { + const { getCellForDate } = renderDateRangeMenu({ + rangeContext: { value: [newUTC(2023, Month.September, 24), null] }, + }); + userEvent.keyboard('{downarrow}'); + const focusedCell = getCellForDate(newUTC(2023, Month.October, 1)); + expect(focusedCell).toHaveFocus(); + }); + }); }); }); }); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx index 388ff5bddb..d39bd7e180 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, KeyboardEventHandler, MouseEventHandler, + useLayoutEffect, useState, } from 'react'; import { addDays, isWithinInterval, subDays } from 'date-fns'; @@ -9,6 +10,7 @@ import isNull from 'lodash/isNull'; import isUndefined from 'lodash/isUndefined'; import { cx } from '@leafygreen-ui/emotion'; +import { usePrevious } from '@leafygreen-ui/hooks'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import { keyMap } from '@leafygreen-ui/lib'; @@ -47,7 +49,7 @@ export const DateRangeMenuCalendars = forwardRef< HTMLDivElement, DateRangeMenuCalendarsProps >((_, fwdRef) => { - const { isInRange, setOpen } = useDatePickerContext(); + const { isInRange, isOpen, setOpen } = useDatePickerContext(); const { refs, value, @@ -61,8 +63,17 @@ export const DateRangeMenuCalendars = forwardRef< today, } = useDateRangeContext(); + const prevOpen = usePrevious(isOpen); const [hoveredCell, setHover] = useState(null); + /** On initial open, focus the cell */ + useLayoutEffect(() => { + if (highlight && isOpen && !prevOpen) { + const highlightCellRef = refs.calendarCellRefs(highlight.toISOString()); + highlightCellRef.current?.focus(); + } + }, [highlight, isOpen, prevOpen, refs]); + /** * setDisplayMonth with side effects */ diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts index cf65c781fb..e66e2b00e5 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts @@ -1,3 +1,2 @@ - -export { DateRangeMenu } from './DateRangeMenu'; +export { DateRangeMenu } from './DateRangeMenu'; export { DateRangeMenuProps } from './DateRangeMenu.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx index 774da9ef27..a9a5ef3880 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx @@ -3,13 +3,14 @@ import { render, waitFor, waitForElementToBeRemoved, + within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import last from 'lodash/last'; import { Month } from '../constants'; import { eventContainingTargetValue, tabNTimes } from '../testUtils'; -import { newUTC, setUTCDate } from '../utils'; +import { newUTC, setUTCDate, setUTCMonth } from '../utils'; import { expectedTabStopLabels, @@ -22,7 +23,7 @@ import { DateRangePicker } from '.'; const testToday = newUTC(2023, Month.December, 26); describe('packages/date-picker/date-range-picker', () => { - beforeEach(() => { + beforeAll(() => { // Set the current time to midnight UTC on 2023-12-26 jest.useFakeTimers().setSystemTime(testToday); }); @@ -117,77 +118,192 @@ describe('packages/date-picker/date-range-picker', () => { await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); }); - test('if no value is set, menu opens to current month', () => { - const { openMenu } = renderDateRangePicker({ - // initialOpen: true, + test('renders two calendar grids', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, }); - const { calendarGrids, menuContainerEl } = openMenu(); - - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('December 2023'); - expect(headers?.[1]).toHaveTextContent('January 2024'); + const { calendarGrids } = getMenuElements(); + expect(calendarGrids).toHaveLength(2); + }); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'December 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'January 2024', - ); + test('chevrons have correct `aria-label`s', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + }); + const { leftChevron, rightChevron } = getMenuElements(); + expect(leftChevron).toHaveAttribute('aria-label', 'Previous month'); + expect(rightChevron).toHaveAttribute('aria-label', 'Next month'); }); - test('if only a start value is set, menu opens to the month of that value', () => { - const { openMenu } = renderDateRangePicker({ - value: [newUTC(2023, Month.March, 10), null], + describe('Displayed month', () => { + test('if no value is set, menu displays current month', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + }); + const { calendarGrids, menuContainerEl } = getMenuElements(); + + const headers = menuContainerEl?.querySelectorAll('h6'); + expect(headers?.[0]).toHaveTextContent('December 2023'); + expect(headers?.[1]).toHaveTextContent('January 2024'); + + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'December 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'January 2024', + ); }); - const { calendarGrids, menuContainerEl } = openMenu(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('March 2023'); - expect(headers?.[1]).toHaveTextContent('April 2023'); - expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'March 2023'); - expect(calendarGrids?.[1]).toHaveAttribute('aria-label', 'April 2023'); - }); + test('if only a start value is set, menu displays the month of that value', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + value: [newUTC(2023, Month.March, 10), null], + }); - test('if only an end value is set, menu opens to the month _before_ value', () => { - const { openMenu } = renderDateRangePicker({ - value: [null, newUTC(2023, Month.March, 10)], + const { calendarGrids, menuContainerEl } = getMenuElements(); + const headers = menuContainerEl?.querySelectorAll('h6'); + expect(headers?.[0]).toHaveTextContent('March 2023'); + expect(headers?.[1]).toHaveTextContent('April 2023'); + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'March 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'April 2023', + ); }); - const { calendarGrids, menuContainerEl } = openMenu(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('February 2023'); - expect(headers?.[1]).toHaveTextContent('March 2023'); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'February 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute('aria-label', 'March 2023'); - }); + test('if only an end value is set, menu displays the month _before_ value', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + value: [null, newUTC(2023, Month.March, 10)], + }); - test('if a full value is set, menu opens to the month of the start value', () => { - const { openMenu } = renderDateRangePicker({ - value: [newUTC(2023, Month.March, 10), newUTC(2023, Month.April, 1)], + const { calendarGrids, menuContainerEl } = getMenuElements(); + const headers = menuContainerEl?.querySelectorAll('h6'); + expect(headers?.[0]).toHaveTextContent('February 2023'); + expect(headers?.[1]).toHaveTextContent('March 2023'); + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'February 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'March 2023', + ); }); - const { calendarGrids, menuContainerEl } = openMenu(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('March 2023'); - expect(headers?.[1]).toHaveTextContent('April 2023'); - expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'March 2023'); - expect(calendarGrids?.[1]).toHaveAttribute('aria-label', 'April 2023'); + test('if a full value is set, menu displays the month of the start value', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + value: [ + newUTC(2023, Month.March, 10), + newUTC(2023, Month.April, 1), + ], + }); + + const { calendarGrids, menuContainerEl } = getMenuElements(); + const headers = menuContainerEl?.querySelectorAll('h6'); + expect(headers?.[0]).toHaveTextContent('March 2023'); + expect(headers?.[1]).toHaveTextContent('April 2023'); + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'March 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'April 2023', + ); + }); + + test('month changes when value changes', () => { + const { getMenuElements, rerenderWithProps } = renderDateRangePicker({ + initialOpen: true, + }); + + rerenderWithProps({ + value: [ + newUTC(2023, Month.July, 5), + newUTC(2023, Month.September, 10), + ], + }); + + const { calendarGrids } = getMenuElements(); + + expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'July 2023'); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'August 2023', + ); + }); }); test('renders the appropriate number of cells', () => { - const { openMenu } = renderDateRangePicker({ + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, value: [newUTC(2024, Month.February, 14), null], }); - const { calendarCells } = openMenu(); + const { calendarCells } = getMenuElements(); expect(calendarCells).toHaveLength(29 + 31); }); - describe('Quick select buttons', () => { + test('rendered cells have correct `aria-label`', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + value: [newUTC(2024, Month.February, 14), null], + }); + const { calendarCells } = getMenuElements(); + expect(calendarCells[0]).toHaveAttribute( + 'aria-label', + 'Thu Feb 01 2024', + ); + }); + + describe('Quick select menu', () => { + describe('Month/Year select', () => { + test('render the correct text', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + showQuickSelection: true, + }); + const { yearSelect, monthSelect } = getMenuElements(); + expect(monthSelect).toHaveTextContent('Dec'); + expect(yearSelect).toHaveTextContent('2023'); + }); + + test('have correct `aria-label`s', () => { + const { getMenuElements } = renderDateRangePicker({ + initialOpen: true, + showQuickSelection: true, + }); + const { yearSelect, monthSelect } = getMenuElements(); + expect(monthSelect).toHaveAttribute('aria-label', 'Select month'); + expect(yearSelect).toHaveAttribute('aria-label', 'Select year'); + }); + + test('update when the value changes', () => { + const { getMenuElements, rerenderWithProps } = + renderDateRangePicker({ + initialOpen: true, + showQuickSelection: true, + }); + + rerenderWithProps({ + value: [ + newUTC(2024, Month.July, 5), + newUTC(2024, Month.September, 10), + ], + }); + + const { yearSelect, monthSelect } = getMenuElements(); + expect(monthSelect).toHaveTextContent('July'); + expect(yearSelect).toHaveTextContent('2024'); + }); + }); + test.each(quickSelectButtonTestCases)( 'Renders correct label: $label', ({ index, label }) => { @@ -209,137 +325,220 @@ describe('packages/date-picker/date-range-picker', () => { }); }); - describe('Typing', () => { - test('opens the menu', () => { - const { inputElements, getMenuElements } = renderDateRangePicker(); - userEvent.type(inputElements[0], '1'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - describe.each([ - ['start', 0], - ['end', 3], - ])('into %p input', (input, index) => { - test('updates segment value', () => { - const { inputElements } = renderDateRangePicker(); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(element.value).toBe('1'); // Not '01' if not blurred - }); - - test('does not fire range change handler', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ onRangeChange }); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(onRangeChange).not.toHaveBeenCalled(); + describe('Interaction', () => { + describe('Typing', () => { + test('opens the menu', () => { + const { inputElements, getMenuElements } = renderDateRangePicker(); + userEvent.type(inputElements[0], '1'); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); }); - test('fires segment change handler', () => { - const onChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - onChange, + describe.each([ + ['start', 0], + ['end', 3], + ])('into %p input', (input, index) => { + test('updates segment value', () => { + const { inputElements } = renderDateRangePicker(); + const element = inputElements[index]; + userEvent.type(element, '1'); + expect(element.value).toBe('1'); // Not '01' if not blurred }); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(onChange).toHaveBeenCalledWith(eventContainingTargetValue('1')); - }); - describe('on un-focus/blur', () => { - test('does not fire a change handler if value is incomplete', () => { + test('does not fire range change handler', () => { const onRangeChange = jest.fn(); const { inputElements } = renderDateRangePicker({ onRangeChange }); const element = inputElements[index]; - userEvent.type(element, '2023'); - userEvent.tab(); + userEvent.type(element, '1'); expect(onRangeChange).not.toHaveBeenCalled(); }); - test('fires a change handler if the value is valid', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ onRangeChange }); - const year = inputElements[index]; - const month = inputElements[index + 1]; - const day = inputElements[index + 2]; - - userEvent.type(year, '2023'); - userEvent.type(month, '12'); - userEvent.type(day, '26'); - userEvent.tab(); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining(newUTC(2023, Month.December, 26)), - ]), - ); - }); - - test('fires a segment change handler', () => { + test('fires segment change handler', () => { const onChange = jest.fn(); const { inputElements } = renderDateRangePicker({ onChange, }); const element = inputElements[index]; - userEvent.type(element, '2023'); - userEvent.tab(); + userEvent.type(element, '1'); expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue('2023'), + eventContainingTargetValue('1'), ); }); - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('validation handler', () => { - test('fired when the value is first set', () => { - const handleValidation = jest.fn(); - const { inputElements } = renderDateRangePicker({ - handleValidation, - }); + describe('on un-focus/blur', () => { + test('does not fire a change handler if value is incomplete', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ onRangeChange }); + const element = inputElements[index]; + userEvent.type(element, '2023'); + userEvent.tab(); + expect(onRangeChange).not.toHaveBeenCalled(); + }); + + test('fires a change handler if the value is valid', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ onRangeChange }); const year = inputElements[index]; const month = inputElements[index + 1]; const day = inputElements[index + 2]; + userEvent.type(year, '2023'); userEvent.type(month, '12'); userEvent.type(day, '26'); userEvent.tab(); - - expect(handleValidation).toHaveBeenCalledWith( + expect(onRangeChange).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining(newUTC(2023, Month.December, 26)), ]), ); }); - test('fired when the value is updated', () => { - const initialStart = newUTC(2023, Month.March, 10); - const initialEnd = newUTC(2023, Month.December, 26); - const handleValidation = jest.fn(); + test('fires a segment change handler', () => { + const onChange = jest.fn(); const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - handleValidation, + onChange, }); - const day = inputElements[index + 2]; - userEvent.type(day, '15'); + const element = inputElements[index]; + userEvent.type(element, '2023'); + userEvent.tab(); + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2023'), + ); + }); - const expectedValue = expect.arrayContaining([ - input === 'start' ? newUTC(2023, Month.March, 15) : initialStart, - input === 'end' ? newUTC(2023, Month.December, 15) : initialEnd, - ]); + // eslint-disable-next-line jest/no-disabled-tests + describe.skip('validation handler', () => { + test('fired when the value is first set', () => { + const handleValidation = jest.fn(); + const { inputElements } = renderDateRangePicker({ + handleValidation, + }); + const year = inputElements[index]; + const month = inputElements[index + 1]; + const day = inputElements[index + 2]; + userEvent.type(year, '2023'); + userEvent.type(month, '12'); + userEvent.type(day, '26'); + userEvent.tab(); + + expect(handleValidation).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining(newUTC(2023, Month.December, 26)), + ]), + ); + }); + + test('fired when the value is updated', () => { + const initialStart = newUTC(2023, Month.March, 10); + const initialEnd = newUTC(2023, Month.December, 26); + const handleValidation = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + handleValidation, + }); + const day = inputElements[index + 2]; + userEvent.type(day, '15'); + + const expectedValue = expect.arrayContaining([ + input === 'start' + ? newUTC(2023, Month.March, 15) + : initialStart, + input === 'end' ? newUTC(2023, Month.December, 15) : initialEnd, + ]); - expect(handleValidation).toHaveBeenCalledWith(expectedValue); + expect(handleValidation).toHaveBeenCalledWith(expectedValue); + }); }); }); }); - }); - // e.g. selected range is 2023-09-10 -> 2023-10-14 - // change start date to 2024 or end date to 2023 - // should reject input and revert - test.todo( - 'Entering an invalid date into start/end rejects the input, and reverts to the previous value', - ); - }); + describe('when entering a new value', () => { + const initialStart = newUTC(2023, Month.September, 2); + const initialEnd = newUTC(2023, Month.September, 10); + + test('start date is updated', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + onRangeChange, + }); + const startMonthInput = inputElements[1]; + userEvent.type(startMonthInput, '{delete}8'); + userEvent.tab(); + expect(onRangeChange).toHaveBeenCalledWith([ + setUTCMonth(initialStart, Month.August), + initialEnd, + ]); + }); + + test('end date is updated', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + onRangeChange, + }); + const endMonthInput = inputElements[4]; + userEvent.type(endMonthInput, '{delete}10'); + userEvent.tab(); + expect(onRangeChange).toHaveBeenCalledWith([ + initialStart, + setUTCMonth(initialEnd, Month.October), + ]); + }); + + test('input is rejected if start date is invalid', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + onRangeChange, + }); + const startMonthInput = inputElements[2]; + userEvent.type(startMonthInput, '1'); + userEvent.tab(); + expect(onRangeChange).not.toHaveBeenCalled(); + }); + + test('input is rejected if end date is invalid', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + onRangeChange, + }); + const endMonthInput = inputElements[4]; + userEvent.type(endMonthInput, '1'); + userEvent.tab(); + expect(onRangeChange).not.toHaveBeenCalled(); + }); + + test('input is rejected if start date is after end date', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [initialStart, initialEnd], + onRangeChange, + }); + const startDayInput = inputElements[2]; + userEvent.type(startDayInput, '1'); + userEvent.tab(); + expect(onRangeChange).not.toHaveBeenCalled(); + }); + + test('input is rejected if end date is before start date', () => { + const onRangeChange = jest.fn(); + const { inputElements } = renderDateRangePicker({ + value: [ + newUTC(2023, Month.September, 2), + newUTC(2023, Month.September, 10), + ], + onRangeChange, + }); + const endDayInput = inputElements[5]; + userEvent.type(endDayInput, '{backspace}'); + userEvent.tab(); + expect(onRangeChange).not.toHaveBeenCalled(); + }); + }); + }); - describe('Interaction', () => { describe('Mouse interaction', () => { describe('Input', () => { describe('Clicking the input', () => { @@ -410,6 +609,33 @@ describe('packages/date-picker/date-range-picker', () => { expect(todayCell).toHaveFocus(); }); }); + + test('initial highlight on `start` value when provided', async () => { + const { calendarButton, getMenuElements } = renderDateRangePicker({ + value: [newUTC(2023, Month.September, 10), null], + }); + userEvent.click(calendarButton); + const { menuContainerEl } = getMenuElements(); + const sep10Cell = within(menuContainerEl!).getByLabelText( + 'Sun Sep 10 2023', + ); + await waitFor(() => { + expect(sep10Cell).toHaveFocus(); + }); + }); + test('initial highlight on `end` value when only end provided', async () => { + const { calendarButton, getMenuElements } = renderDateRangePicker({ + value: [null, newUTC(2023, Month.September, 10)], + }); + userEvent.click(calendarButton); + const { menuContainerEl } = getMenuElements(); + const sep10Cell = within(menuContainerEl!).getByLabelText( + 'Sun Sep 10 2023', + ); + await waitFor(() => { + expect(sep10Cell).toHaveFocus(); + }); + }); }); }); @@ -463,25 +689,20 @@ describe('packages/date-picker/date-range-picker', () => { ); }); - describe('if a full range is set', () => { - test('fires a change handler to set the start date & clear the end date', () => { - const onRangeChange = jest.fn(); - const startDate = newUTC(2023, Month.September, 10); - const endDate = newUTC(2023, Month.October, 31); - const { openMenu } = renderDateRangePicker({ - value: [startDate, endDate], - onRangeChange, - }); - const { calendarCells } = openMenu(); - const firstCell = calendarCells[0]; - userEvent.click(firstCell); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([ - newUTC(2023, Month.September, 1), - null, - ]), - ); + test('if a full range is set, fires a change handler to set the start date & clear the end date', () => { + const onRangeChange = jest.fn(); + const startDate = newUTC(2023, Month.September, 10); + const endDate = newUTC(2023, Month.October, 31); + const { openMenu } = renderDateRangePicker({ + value: [startDate, endDate], + onRangeChange, }); + const { calendarCells } = openMenu(); + const firstCell = calendarCells[0]; + userEvent.click(firstCell); + expect(onRangeChange).toHaveBeenCalledWith( + expect.arrayContaining([newUTC(2023, Month.September, 1), null]), + ); }); }); @@ -711,11 +932,28 @@ describe('packages/date-picker/date-range-picker', () => { expect(handleValidation).toHaveBeenCalledWith(expectedRange); }, ); - }); - test.todo( - 'When value changes via quick ranges, update the displayed month', - ); + test('update the displayed calendars', () => { + const { getAllByTestId, getMenuElements } = renderDateRangePicker({ + initialOpen: true, + showQuickSelection: true, + }); + const quickSelectButtons = getAllByTestId( + 'lg-date_picker-menu-quick-range-button', + ); + const { calendarGrids } = getMenuElements(); + const last90Button = quickSelectButtons[4]; + userEvent.click(last90Button); + expect(calendarGrids?.[0]).toHaveAttribute( + 'aria-label', + 'September 2023', + ); + expect(calendarGrids?.[1]).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); + }); }); describe('Backdrop', () => { @@ -740,14 +978,14 @@ describe('packages/date-picker/date-range-picker', () => { describe('Keyboard interaction', () => { describe('Tab', () => { - test('menu does not open on initial focus', () => { + test('menu does not open on initial input focus', () => { const { getMenuElements } = renderDateRangePicker(); tabNTimes(1); const { menuContainerEl } = getMenuElements(); expect(menuContainerEl).not.toBeInTheDocument(); }); - test('menu does not open on subsequent focuses', () => { + test('menu does not open on subsequent input focuses', () => { const { getMenuElements } = renderDateRangePicker(); tabNTimes(5); const { menuContainerEl } = getMenuElements(); diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx index 7fadec0635..0a5c352128 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx @@ -12,7 +12,7 @@ import { newUTC } from '../utils'; import { DateRangePicker, DateRangePickerProps } from '.'; -const testToday = newUTC(2023, Month.December, 26); +export const testToday = newUTC(2023, Month.December, 26); /** Explicit test cases for quick range buttons */ export const quickSelectButtonTestCases = [ @@ -38,7 +38,7 @@ export interface RenderDateRangePickerResult extends RenderResult { calendarButton: HTMLButtonElement; getMenuElements: () => RenderMenuResult; openMenu: () => RenderMenuResult; - rerenderWithProps: () => void; + rerenderWithProps: (p?: Partial) => void; } export interface RenderMenuResult { From fe09c751b7c180c816b7f9b6b4d3362e4c695d79 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 20 Oct 2023 18:22:09 -0400 Subject: [PATCH 258/351] Update DateRangeMenu.spec.tsx --- .../DateRangeMenu/DateRangeMenu.spec.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx index 162064b6c4..1aab587d19 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx @@ -176,7 +176,17 @@ describe('packages/date-picker/date-range-picker/menu', () => { 'October 2023', ); }); - test.todo('does not update month when month does not need to change'); + test('does not update month when month does not need to change', () => { + const { getAllByRole } = renderDateRangeMenu({ + rangeContext: { value: [testToday, null] }, + }); + userEvent.keyboard('{downarrow}'); + const calendarGrids = getAllByRole('grid'); + expect(calendarGrids[0]).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); }); describe('when month should be updated', () => { From 0874f3dbe5f3740649793e18e6aae6e236dbaed0 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 30 Oct 2023 11:19:47 -0400 Subject: [PATCH 259/351] fix build --- .../DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx | 1 + .../DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx index 3815aaa751..ef223c3d01 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx @@ -67,6 +67,7 @@ const MenuDecorator: Decorator = (Story, ctx) => { rootRef={React.createRef()} value={value} setValue={() => {}} + handleValidation={() => {}} > diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx index 7b5dc994f9..3066318169 100644 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx +++ b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx @@ -1,9 +1,9 @@ import React, { MouseEventHandler } from 'react'; import { forwardRef } from 'react'; import { addDays } from 'date-fns'; -import { isFinite } from 'lodash'; +import isFinite from 'lodash/isFinite'; import range from 'lodash/range'; -import { DateRangeType } from 'src/types'; +import { DateRangeType } from '../../../types'; import { cx } from '@leafygreen-ui/emotion'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; From 4d49addb9ca2cbb397be567d029a0a17917209eb Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:11:44 -0400 Subject: [PATCH 260/351] DatePicker prepare exports (#2051) * updates build config & exports * mv shared to shared * update imports * fix builds * lint * // eslint-disable-next-line jest/no-disabled-tests * Update DateInputBox.spec.tsx * mmv utils hooks to shared * fix builds * fix form-field dep version * rm date range picker * fix tests suites * fix filename case * fix dependencies * rm exports from package.json --- packages/date-picker/package.json | 7 +- packages/date-picker/rollup.config.mjs | 14 + packages/date-picker/src/Calendar/index.ts | 7 - .../DateInput/DateInputSegment/constants.ts | 46 - packages/date-picker/src/DateInput/index.ts | 1 - .../src/DatePicker/DatePicker.spec.tsx | 9 +- .../src/DatePicker/DatePicker.stories.tsx | 8 +- .../date-picker/src/DatePicker/DatePicker.tsx | 10 +- .../src/DatePicker/DatePicker.types.ts | 2 +- .../DatePickerComponent.tsx | 4 +- .../DatePickerComponent.types.ts | 6 +- .../DatePickerInput/DatePickerInput.spec.tsx | 6 +- .../DatePickerInput.stories.tsx | 2 +- .../DatePickerInput/DatePickerInput.tsx | 10 +- .../DatePickerInput/DatePickerInput.types.ts | 1 - .../DatePickerMenu/DatePickerMenu.spec.tsx | 9 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 10 +- .../DatePickerMenu/DatePickerMenu.tsx | 13 +- .../DatePickerMenuHeader/index.tsx | 6 +- .../utils/getNewHighlight/index.ts | 2 +- .../utils/getRelativeSegment/index.ts | 5 +- .../DatePicker/utils/getSegmentKey/index.ts | 2 +- .../index.ts | 3 +- .../getSegmentToFocus.spec.ts | 4 +- .../utils/getSegmentToFocus/index.ts | 7 +- .../DateRangeComponent.styles.ts | 3 - .../DateRangeComponent/DateRangeComponent.tsx | 41 - .../DateRangeComponent.types.ts | 14 - .../DateRangeComponent/index.ts | 2 - .../DateRangeContext/DateRangeContext.tsx | 79 -- .../DateRangeContext.types.ts | 39 - .../DateRangePicker/DateRangeContext/index.ts | 2 - .../useDateRangeComponentRefs.tsx | 36 - .../DateRangeInput/DateRangeInput.spec.tsx | 31 - .../DateRangeInput/DateRangeInput.stories.tsx | 65 - .../DateRangeInput/DateRangeInput.styles.ts | 6 - .../DateRangeInput/DateRangeInput.tsx | 206 --- .../DateRangeInput/DateRangeInput.types.ts | 11 - .../DateRangePicker/DateRangeInput/index.ts | 2 - .../DateRangeMenu/DateRangeMenu.spec.tsx | 228 ---- .../DateRangeMenu/DateRangeMenu.stories.tsx | 252 ---- .../DateRangeMenu/DateRangeMenu.styles.ts | 25 - .../DateRangeMenu/DateRangeMenu.tsx | 127 -- .../DateRangeMenu/DateRangeMenu.types.ts | 13 - .../DateRangeMenuCalendars.styles.ts | 47 - .../DateRangeMenuCalendars.tsx | 341 ----- .../DateRangeMenuCalendars.types.ts | 3 - .../DateRangeMenuCalendars/index.ts | 1 - .../DateRangeMenuFooter.styles.ts | 18 - .../DateRangeMenuFooter.tsx | 65 - .../DateRangeMenuFooter/index.ts | 1 - .../QuickRangeButton.stories.tsx | 37 - .../QuickRangeButton.styles.ts | 55 - .../QuickRangeButton/QuickRangeButton.tsx | 40 - .../QuickRangeButton/index.ts | 1 - .../QuickSelectionMenu.styles.ts | 39 - .../QuickSelectionMenu/QuickSelectionMenu.tsx | 164 --- .../DateRangeMenu/QuickSelectionMenu/index.ts | 1 - .../DateRangePicker/DateRangeMenu/index.ts | 2 - .../DateRangePicker/DateRangePicker.spec.tsx | 1205 ----------------- .../DateRangePicker.stories.tsx | 160 --- .../DateRangePicker/DateRangePicker.styles.ts | 3 - .../DateRangePicker.testutils.tsx | 295 ---- .../src/DateRangePicker/DateRangePicker.tsx | 48 - .../DateRangePicker/DateRangePicker.types.ts | 57 - .../date-picker/src/DateRangePicker/index.ts | 2 - .../utils/getInitialHighlight/index.ts | 19 - .../utils/getInitialMonth/index.ts | 19 - .../utils/getRangeSegmentToFocus/index.ts | 65 - .../utils/getRelativeRangeSegment/index.ts | 86 -- packages/date-picker/src/constants.ts | 54 - .../date-picker/src/hooks/useFormat/index.ts | 54 - packages/date-picker/src/index.ts | 7 +- .../CalendarCell/CalendarCell.spec.tsx | 0 .../CalendarCell/CalendarCell.stories.tsx | 0 .../CalendarCell/CalendarCell.styles.ts | 0 .../Calendar/CalendarCell/CalendarCell.tsx | 0 .../CalendarCell/CalendarCell.types.ts | 0 .../Calendar/CalendarCell/index.ts | 0 .../CalendarGrid/CalendarGrid.spec.tsx | 0 .../CalendarGrid/CalendarGrid.stories.tsx | 6 +- .../CalendarGrid/CalendarGrid.styles.ts | 0 .../Calendar/CalendarGrid/CalendarGrid.tsx | 4 +- .../CalendarGrid/CalendarGrid.types.ts | 0 .../Calendar/CalendarGrid/index.ts | 0 .../src/shared/components/Calendar/index.ts | 7 + .../CalendarButton/CalendarButton.styles.ts | 0 .../CalendarButton/CalendarButton.tsx | 0 .../DateInput/CalendarButton/index.ts | 0 .../DateFormField/DateFormField.spec.tsx | 0 .../DateFormField/DateFormField.stories.tsx | 2 +- .../DateFormField/DateFormField.styles.ts | 0 .../DateInput/DateFormField/DateFormField.tsx | 0 .../DateFormField/DateFormField.types.ts | 0 .../DateInput/DateFormField/index.ts | 0 .../DateInputBox/DateInputBox.spec.tsx | 11 +- .../DateInputBox/DateInputBox.stories.tsx | 4 +- .../DateInputBox/DateInputBox.styles.ts | 0 .../DateInput/DateInputBox/DateInputBox.tsx | 8 +- .../DateInputBox/DateInputBox.types.ts | 4 +- .../DateInput/DateInputBox/index.ts | 0 .../DateInputSegment.spec.tsx | 2 +- .../DateInputSegment.stories.tsx | 0 .../DateInputSegment.styles.ts | 5 +- .../DateInputSegment/DateInputSegment.tsx | 2 +- .../DateInputSegment.types.ts | 5 +- .../DateInput/DateInputSegment/index.ts | 0 .../src/shared/components/DateInput/index.ts | 4 + .../DatePickerContext.spec.tsx | 0 .../DatePickerContext/DatePickerContext.tsx | 0 .../DatePickerContext.types.ts | 2 +- .../DatePickerContext.utils.ts | 6 +- .../components}/DatePickerContext/index.ts | 5 +- .../MenuWrapper/MenuWrapper.spec.tsx | 0 .../MenuWrapper/MenuWrapper.styles.ts | 0 .../components}/MenuWrapper/MenuWrapper.tsx | 0 .../components}/MenuWrapper/index.ts | 0 .../src/shared/components/index.ts | 18 + packages/date-picker/src/shared/constants.ts | 122 ++ .../date-picker/src/shared/hooks/index.ts | 9 + .../hooks/useControlledValue/index.ts | 0 .../useControlledValue.spec.tsx | 0 .../useControlledValue/useControlledValue.ts | 0 .../useDateSegments/DateSegments.types.ts | 0 .../hooks/useDateSegments/index.ts | 4 +- .../hooks/useDateSegments/useDateSegments.ts | 0 .../hooks/useSegmentRefs/index.ts | 0 .../hooks/useSegmentRefs/segmentRefs.types.ts | 0 .../hooks/useSegmentRefs/useSegmentRefs.ts | 0 packages/date-picker/src/shared/index.ts | 5 + .../date-picker/src/{ => shared}/types.ts | 0 .../{ => shared}/utils/addMonthsUTC/index.ts | 0 .../utils/cloneReverse/cloneReverse.spec.ts | 0 .../{ => shared}/utils/cloneReverse/index.ts | 0 .../utils/doesSomeSegmentExist/index.ts | 0 .../getDaysInUTCMonth.spec.ts | 0 .../utils/getDaysInUTCMonth/index.ts | 0 .../utils/getFirstEmptySegment/index.ts | 5 +- .../getFirstOfMonth/getFirstOfMonth.spec.ts | 0 .../utils/getFirstOfMonth/index.ts | 0 .../utils/getFormatParts/index.ts | 0 .../getFullMonthLabel.spec.ts | 0 .../utils/getFullMonthLabel/index.ts | 0 .../getLastOfMonth/getLastOfMonth.spec.ts | 0 .../utils/getLastOfMonth/index.ts | 0 .../utils/getMonthName/getMonthName.spec.ts | 0 .../{ => shared}/utils/getMonthName/index.ts | 0 .../getRemainingParts.spec.ts | 0 .../utils/getRemainingParts/index.ts | 0 .../getSegmentsFromDate.spec.ts | 0 .../utils/getSegmentsFromDate/index.ts | 2 +- .../getUTCDateString/getUTCDateString.spec.ts | 0 .../utils/getUTCDateString/index.ts | 0 .../utils/getValueFormatter/index.ts | 7 +- .../getValueFormatter/valueFormatter.spec.ts | 2 +- .../utils/getWeeksArray/getWeeksArray.spec.ts | 0 .../{ => shared}/utils/getWeeksArray/index.ts | 0 .../src/{ => shared}/utils/index.ts | 0 .../utils/isCurrentUTCDay/index.ts | 0 .../src/{ => shared}/utils/isDefined/index.ts | 0 .../utils/isElementInputSegment/index.ts | 2 +- .../{ => shared}/utils/isOnOrAfter/index.ts | 0 .../{ => shared}/utils/isOnOrBefore/index.ts | 0 .../{ => shared}/utils/isSameTZDay/index.ts | 0 .../utils/isSameTZDay/isSameTZDay.spec.ts | 0 .../{ => shared}/utils/isSameUTCDay/index.ts | 0 .../utils/isSameUTCDay/isSameUTCDay.spec.ts | 0 .../utils/isSameUTCMonth/index.ts | 0 .../isSameUTCMonth/isSameUTCMonth.spec.ts | 0 .../utils/isSameUTCRange/index.ts | 0 .../src/{ => shared}/utils/isTodayTZ/index.ts | 0 .../utils/isTodayTZ/isTodayTZ.spec.ts | 0 .../{ => shared}/utils/isValidDate/index.ts | 0 .../utils/isValidDate/isValidDate.spec.ts | 0 .../{ => shared}/utils/isValidLocale/index.ts | 0 .../utils/isValidLocale/isValidLocale.spec.ts | 0 .../utils/isValidSegment/index.ts | 5 +- .../isValidSegment/isValidSegment.spec.ts | 0 .../{ => shared}/utils/isZeroLike/index.ts | 0 .../src/{ => shared}/utils/maxDate/index.ts | 0 .../src/{ => shared}/utils/minDate/index.ts | 0 .../utils/newDateFromSegments/index.ts | 2 +- .../newDateFromSegments.spec.ts | 0 .../src/{ => shared}/utils/newUTC/index.ts | 0 .../{ => shared}/utils/newUTC/newUTC.spec.ts | 0 .../{ => shared}/utils/pickAndOmit/index.ts | 0 .../utils/setToUTCMidnight/index.ts | 0 .../setToUTCMidnight/setToUTCMidnight.spec.ts | 0 .../{ => shared}/utils/setUTCDate/index.ts | 0 .../{ => shared}/utils/setUTCMonth/index.ts | 0 .../utils/setUTCMonth/setUTCMonth.spec.ts | 0 .../{ => shared}/utils/setUTCYear/index.ts | 0 .../utils/testutils.ts} | 9 +- .../src/{ => shared}/utils/toDate/index.ts | 0 .../{ => shared}/utils/toDate/toDate.spec.ts | 0 packages/hooks/package.json | 5 +- yarn.lock | 21 + 197 files changed, 321 insertions(+), 4239 deletions(-) create mode 100644 packages/date-picker/rollup.config.mjs delete mode 100644 packages/date-picker/src/Calendar/index.ts delete mode 100644 packages/date-picker/src/DateInput/DateInputSegment/constants.ts delete mode 100644 packages/date-picker/src/DateInput/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeComponent/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeContext/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeContext/useDateRangeComponentRefs.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.tsx delete mode 100644 packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts delete mode 100644 packages/date-picker/src/DateRangePicker/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/utils/getInitialHighlight/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/utils/getInitialMonth/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts delete mode 100644 packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts delete mode 100644 packages/date-picker/src/constants.ts delete mode 100644 packages/date-picker/src/hooks/useFormat/index.ts rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/CalendarCell.spec.tsx (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/CalendarCell.stories.tsx (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/CalendarCell.styles.ts (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/CalendarCell.tsx (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/CalendarCell.types.ts (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarCell/index.ts (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/CalendarGrid.spec.tsx (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/CalendarGrid.stories.tsx (94%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/CalendarGrid.styles.ts (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/CalendarGrid.tsx (95%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/CalendarGrid.types.ts (100%) rename packages/date-picker/src/{ => shared/components}/Calendar/CalendarGrid/index.ts (100%) create mode 100644 packages/date-picker/src/shared/components/Calendar/index.ts rename packages/date-picker/src/{ => shared/components}/DateInput/CalendarButton/CalendarButton.styles.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/CalendarButton/CalendarButton.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/CalendarButton/index.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/DateFormField.spec.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/DateFormField.stories.tsx (98%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/DateFormField.styles.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/DateFormField.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/DateFormField.types.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateFormField/index.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/DateInputBox.spec.tsx (96%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/DateInputBox.stories.tsx (96%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/DateInputBox.styles.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/DateInputBox.tsx (97%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/DateInputBox.types.ts (86%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputBox/index.ts (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/DateInputSegment.spec.tsx (98%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/DateInputSegment.stories.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/DateInputSegment.styles.ts (93%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/DateInputSegment.tsx (96%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/DateInputSegment.types.ts (85%) rename packages/date-picker/src/{ => shared/components}/DateInput/DateInputSegment/index.ts (100%) create mode 100644 packages/date-picker/src/shared/components/DateInput/index.ts rename packages/date-picker/src/{ => shared/components}/DatePickerContext/DatePickerContext.spec.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DatePickerContext/DatePickerContext.tsx (100%) rename packages/date-picker/src/{ => shared/components}/DatePickerContext/DatePickerContext.types.ts (95%) rename packages/date-picker/src/{ => shared/components}/DatePickerContext/DatePickerContext.utils.ts (92%) rename packages/date-picker/src/{ => shared/components}/DatePickerContext/index.ts (73%) rename packages/date-picker/src/{Calendar => shared/components}/MenuWrapper/MenuWrapper.spec.tsx (100%) rename packages/date-picker/src/{Calendar => shared/components}/MenuWrapper/MenuWrapper.styles.ts (100%) rename packages/date-picker/src/{Calendar => shared/components}/MenuWrapper/MenuWrapper.tsx (100%) rename packages/date-picker/src/{Calendar => shared/components}/MenuWrapper/index.ts (100%) create mode 100644 packages/date-picker/src/shared/components/index.ts create mode 100644 packages/date-picker/src/shared/constants.ts create mode 100644 packages/date-picker/src/shared/hooks/index.ts rename packages/date-picker/src/{ => shared}/hooks/useControlledValue/index.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useControlledValue/useControlledValue.spec.tsx (100%) rename packages/date-picker/src/{ => shared}/hooks/useControlledValue/useControlledValue.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useDateSegments/DateSegments.types.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useDateSegments/index.ts (71%) rename packages/date-picker/src/{ => shared}/hooks/useDateSegments/useDateSegments.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useSegmentRefs/index.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useSegmentRefs/segmentRefs.types.ts (100%) rename packages/date-picker/src/{ => shared}/hooks/useSegmentRefs/useSegmentRefs.ts (100%) create mode 100644 packages/date-picker/src/shared/index.ts rename packages/date-picker/src/{ => shared}/types.ts (100%) rename packages/date-picker/src/{ => shared}/utils/addMonthsUTC/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/cloneReverse/cloneReverse.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/cloneReverse/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/doesSomeSegmentExist/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getDaysInUTCMonth/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getFirstEmptySegment/index.ts (80%) rename packages/date-picker/src/{ => shared}/utils/getFirstOfMonth/getFirstOfMonth.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getFirstOfMonth/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getFormatParts/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getFullMonthLabel/getFullMonthLabel.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getFullMonthLabel/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getLastOfMonth/getLastOfMonth.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getLastOfMonth/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getMonthName/getMonthName.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getMonthName/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getRemainingParts/getRemainingParts.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getRemainingParts/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getSegmentsFromDate/index.ts (92%) rename packages/date-picker/src/{ => shared}/utils/getUTCDateString/getUTCDateString.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getUTCDateString/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getValueFormatter/index.ts (77%) rename packages/date-picker/src/{ => shared}/utils/getValueFormatter/valueFormatter.spec.ts (94%) rename packages/date-picker/src/{ => shared}/utils/getWeeksArray/getWeeksArray.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/getWeeksArray/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isCurrentUTCDay/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isDefined/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isElementInputSegment/index.ts (86%) rename packages/date-picker/src/{ => shared}/utils/isOnOrAfter/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isOnOrBefore/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameTZDay/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameTZDay/isSameTZDay.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameUTCDay/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameUTCDay/isSameUTCDay.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameUTCMonth/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameUTCMonth/isSameUTCMonth.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isSameUTCRange/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isTodayTZ/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isTodayTZ/isTodayTZ.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isValidDate/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isValidDate/isValidDate.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isValidLocale/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isValidLocale/isValidLocale.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isValidSegment/index.ts (84%) rename packages/date-picker/src/{ => shared}/utils/isValidSegment/isValidSegment.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/isZeroLike/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/maxDate/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/minDate/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/newDateFromSegments/index.ts (82%) rename packages/date-picker/src/{ => shared}/utils/newDateFromSegments/newDateFromSegments.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/newUTC/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/newUTC/newUTC.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/pickAndOmit/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setToUTCMidnight/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setToUTCMidnight/setToUTCMidnight.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setUTCDate/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setUTCMonth/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setUTCMonth/setUTCMonth.spec.ts (100%) rename packages/date-picker/src/{ => shared}/utils/setUTCYear/index.ts (100%) rename packages/date-picker/src/{testUtils.ts => shared/utils/testutils.ts} (89%) rename packages/date-picker/src/{ => shared}/utils/toDate/index.ts (100%) rename packages/date-picker/src/{ => shared}/utils/toDate/toDate.spec.ts (100%) diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 23d324fda3..28617e1468 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -2,10 +2,10 @@ "name": "@leafygreen-ui/date-picker", "version": "0.1.0", "description": "LeafyGreen UI Kit Date Picker", + "license": "Apache-2.0", "main": "./dist/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", - "license": "Apache-2.0", "scripts": { "build": "lg build-package", "tsc": "lg build-ts", @@ -15,9 +15,8 @@ "access": "public" }, "dependencies": { - "@leafygreen-ui/button": "^21.0.7", "@leafygreen-ui/emotion": "^4.0.7", - "@leafygreen-ui/form-field": "^0.1.0", + "@leafygreen-ui/form-field": "^0.2.0", "@leafygreen-ui/hooks": "^8.0.0", "@leafygreen-ui/icon": "^11.23.0", "@leafygreen-ui/icon-button": "^15.0.17", @@ -29,6 +28,7 @@ "@leafygreen-ui/typography": "^17.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", + "lodash": "^4.17.21", "polished": "^4.2.2", "weekstart": "^2.0.0" }, @@ -36,6 +36,7 @@ "@leafygreen-ui/leafygreen-provider": "^3.1.6" }, "devDependencies": { + "@leafygreen-ui/button": "^21.0.7", "mockdate": "^3.0.5", "storybook-mock-date-decorator": "^1.0.1", "timezone-mock": "^1.3.6" diff --git a/packages/date-picker/rollup.config.mjs b/packages/date-picker/rollup.config.mjs new file mode 100644 index 0000000000..f287595312 --- /dev/null +++ b/packages/date-picker/rollup.config.mjs @@ -0,0 +1,14 @@ +import { + esmConfig, + storiesConfig, + umdConfig, +} from '@lg-tools/build/config/rollup.config.mjs'; + +const sharedConfig = [esmConfig, umdConfig].map(config => ({ + ...config, + input: 'src/shared/index.ts', +})); + +const config = [esmConfig, umdConfig, ...sharedConfig]; + +export default config; diff --git a/packages/date-picker/src/Calendar/index.ts b/packages/date-picker/src/Calendar/index.ts deleted file mode 100644 index b068ea776a..0000000000 --- a/packages/date-picker/src/Calendar/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { - CalendarCell, - CalendarCellProps, - CalendarCellState, -} from './CalendarCell'; -export { CalendarGrid, CalendarGridProps } from './CalendarGrid'; -export { MenuWrapper } from './MenuWrapper'; diff --git a/packages/date-picker/src/DateInput/DateInputSegment/constants.ts b/packages/date-picker/src/DateInput/DateInputSegment/constants.ts deleted file mode 100644 index fd10be962b..0000000000 --- a/packages/date-picker/src/DateInput/DateInputSegment/constants.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MAX_DATE, MIN_DATE } from '../../constants'; - -/** The minimum number for each segment */ -export const defaultMin = { - day: 1, - month: 1, - year: MIN_DATE.getUTCFullYear(), -} as const; - -/** The maximum number for each segment */ -export const defaultMax = { - day: 31, - month: 12, - year: MAX_DATE.getUTCFullYear(), -} as const; - -/** The shorthand for each char */ -export const placeholderChar = { - day: 'D', - month: 'M', - year: 'Y', -}; - -export const charsPerSegment = { - day: 2, - month: 2, - year: 4, -}; - -const makePlaceholder = (n: number, s: string) => - new Array(n).fill(s).join('\u200B'); - -/** The default placeholders for each segment */ -export const defaultPlaceholder = { - day: makePlaceholder(charsPerSegment.day, placeholderChar.day), - month: makePlaceholder(charsPerSegment.month, placeholderChar.month), - year: makePlaceholder(charsPerSegment.year, placeholderChar.year), -} as const; - -/** The percentage of 1ch these specific characters take up */ -export const characterWidth = { - // // Standard font - D: 46 / 40, - M: 55 / 40, - Y: 50 / 40, -} as const; diff --git a/packages/date-picker/src/DateInput/index.ts b/packages/date-picker/src/DateInput/index.ts deleted file mode 100644 index eadb3a750a..0000000000 --- a/packages/date-picker/src/DateInput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DateInputBox, type DateInputBoxProps } from './DateInputBox'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 46bfee8386..2db09b0f11 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -8,9 +8,12 @@ import { import userEvent from '@testing-library/user-event'; import { range } from 'lodash'; -import { Month } from '../constants'; -import { eventContainingTargetValue, tabNTimes } from '../testUtils'; -import { newUTC } from '../utils/newUTC'; +import { Month } from '../shared/constants'; +import { newUTC } from '../shared/utils'; +import { + eventContainingTargetValue, + tabNTimes, +} from '../shared/utils/testutils'; import { renderDatePicker } from './DatePicker.testutils'; import { DatePicker } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx index 6a8d92861b..e9135e81c5 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx @@ -6,13 +6,13 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; -import { Month } from '../constants'; import { DatePickerContextProps, DatePickerProvider, -} from '../DatePickerContext'; -import { Locales, TimeZones } from '../testUtils'; -import { newUTC } from '../utils'; +} from '../shared/components/DatePickerContext'; +import { Month } from '../shared/constants'; +import { newUTC } from '../shared/utils'; +import { Locales, TimeZones } from '../shared/utils/testutils'; import { DatePicker } from './DatePicker'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index ba47423faa..a4eea83177 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -1,9 +1,11 @@ import React, { forwardRef } from 'react'; -import { DatePickerProvider } from '../DatePickerContext'; -import { contextPropNames } from '../DatePickerContext/DatePickerContext.utils'; -import { useControlledValue } from '../hooks/useControlledValue'; -import { pickAndOmit } from '../utils'; +import { + contextPropNames, + DatePickerProvider, +} from '../shared/components/DatePickerContext'; +import { useControlledValue } from '../shared/hooks'; +import { pickAndOmit } from '../shared/utils'; import { DatePickerProps } from './DatePicker.types'; import { DatePickerComponent } from './DatePickerComponent'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index a92b568f0e..d74f8799ae 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -1,6 +1,6 @@ import { ChangeEvent } from 'react'; -import { BaseDatePickerProps, DateType } from '../types'; +import { BaseDatePickerProps, DateType } from '../shared/types'; export interface DatePickerProps extends BaseDatePickerProps { /** diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index 6972f9923a..145205687c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -2,8 +2,8 @@ import React, { forwardRef, useRef } from 'react'; import { useBackdropClick, useForwardedRef } from '@leafygreen-ui/hooks'; -import { useDatePickerContext } from '../../DatePickerContext'; -import { isSameUTCDay } from '../../utils'; +import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { isSameUTCDay } from '../../shared/utils'; import { DatePickerInput } from '../DatePickerInput'; import { DatePickerMenu, DatePickerMenuProps } from '../DatePickerMenu'; diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts index c2f2975343..4f8b039c96 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts @@ -1,6 +1,6 @@ -import { ContextPropKeys } from '../../DatePickerContext/DatePickerContext.utils'; -import { useControlledValue } from '../../hooks/useControlledValue'; -import { DateType } from '../../types'; +import { ContextPropKeys } from '../../shared/components/DatePickerContext'; +import { useControlledValue } from '../../shared/hooks'; +import { DateType } from '../../shared/types'; import { DatePickerProps } from '../DatePicker.types'; /** diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index a11bd8efd2..ae3e48149a 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Month } from '../../constants'; import { DatePickerProvider, DatePickerProviderProps, -} from '../../DatePickerContext'; -import { defaultDatePickerContext } from '../../DatePickerContext/DatePickerContext.utils'; + defaultDatePickerContext, +} from '../../shared/components/DatePickerContext'; +import { Month } from '../../shared/constants'; import { DatePickerInput, DatePickerInputProps } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index 1c149d71fd..ea65d36224 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -11,7 +11,7 @@ import { Size } from '@leafygreen-ui/tokens'; import { DatePickerContextProps, DatePickerProvider, -} from '../../DatePickerContext'; +} from '../../shared/components/DatePickerContext'; import { DatePickerInput } from './DatePickerInput'; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index ebf8a426b2..5f8783d20f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -7,12 +7,10 @@ import React, { import { keyMap } from '@leafygreen-ui/lib'; -import { DateInputBox } from '../../DateInput'; -import { DateFormField } from '../../DateInput/DateFormField'; -import { useDatePickerContext } from '../../DatePickerContext'; -import { useSegmentRefs } from '../../hooks/useSegmentRefs'; -import { isZeroLike } from '../../utils'; -import { isElementInputSegment } from '../../utils/isElementInputSegment'; +import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; +import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { useSegmentRefs } from '../../shared/hooks'; +import { isElementInputSegment, isZeroLike } from '../../shared/utils'; import { getRelativeSegment } from '../utils/getRelativeSegment'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts index 706d2d999c..01bdc732e2 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts @@ -1,6 +1,5 @@ import { MouseEventHandler } from 'react'; -// import { DynamicRefGetter } from '@leafygreen-ui/hooks/src/useDynamicRefs'; import { HTMLElementProps } from '@leafygreen-ui/lib'; import { DatePickerComponentProps } from '../DatePickerComponent'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 95b89ac4ef..fddbf6a358 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -3,14 +3,13 @@ import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addDays, subDays } from 'date-fns'; -import { Month } from '../../constants'; import { DatePickerProvider, DatePickerProviderProps, -} from '../../DatePickerContext'; -import { defaultDatePickerContext } from '../../DatePickerContext/DatePickerContext.utils'; -import { newUTC } from '../../utils/newUTC'; -import { setUTCDate } from '../../utils/setUTCDate'; + defaultDatePickerContext, +} from '../../shared/components/DatePickerContext'; +import { Month } from '../../shared/constants'; +import { newUTC, setUTCDate } from '../../shared/utils'; import { DatePickerMenu, DatePickerMenuProps } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 26c738a05a..88ebc4b5a3 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -10,16 +10,14 @@ import { StoryMetaType } from '@leafygreen-ui/lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; import { InlineCode } from '@leafygreen-ui/typography'; -import { Month } from '../../constants'; import { + contextPropNames, DatePickerContextProps, DatePickerProvider, -} from '../../DatePickerContext'; -import { - contextPropNames, defaultDatePickerContext, -} from '../../DatePickerContext/DatePickerContext.utils'; -import { newUTC, pickAndOmit } from '../../utils'; +} from '../../shared/components/DatePickerContext'; +import { Month } from '../../shared/constants'; +import { newUTC, pickAndOmit } from '../../shared/utils'; import { DatePickerMenu } from './DatePickerMenu'; import { DatePickerMenuProps } from './DatePickerMenu.types'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 3f56b22663..329d5a13aa 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -14,10 +14,13 @@ import { keyMap } from '@leafygreen-ui/lib'; import { useForwardedRef } from '@leafygreen-ui/select/src/utils'; import { spacing } from '@leafygreen-ui/tokens'; -import { CalendarCell, CalendarCellState } from '../../Calendar/CalendarCell'; -import { CalendarGrid } from '../../Calendar/CalendarGrid'; -import { MenuWrapper } from '../../Calendar/MenuWrapper'; -import { useDatePickerContext } from '../../DatePickerContext'; +import { + CalendarCell, + CalendarCellState, + CalendarGrid, +} from '../../shared/components/Calendar'; +import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { MenuWrapper } from '../../shared/components/MenuWrapper'; import { getFirstOfMonth, getFullMonthLabel, @@ -25,7 +28,7 @@ import { isSameUTCDay, isSameUTCMonth, setToUTCMidnight, -} from '../../utils'; +} from '../../shared/utils'; import { getNewHighlight } from './utils/getNewHighlight'; import { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx index 65cb038730..80b4b4a4e3 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx @@ -6,9 +6,9 @@ import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import { Option, Select } from '@leafygreen-ui/select'; -import { Months, selectElementProps } from '../../../constants'; -import { useDatePickerContext } from '../../../DatePickerContext'; -import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../utils'; +import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; +import { Months, selectElementProps } from '../../../shared/constants'; +import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../shared/utils'; import { menuHeaderSelectContainerStyles, menuHeaderStyles, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts index 85e49fc7e2..1717dd6c1d 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts @@ -4,7 +4,7 @@ import { getFirstOfMonth, getLastOfMonth, isSameUTCMonth, -} from '../../../../utils'; +} from '../../../../shared/utils'; export const getNewHighlight = ( currentHighlight: Date | null, diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts index 20b34c8461..65fefd0b68 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts @@ -1,9 +1,8 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { DatePickerContextProps } from '../../../DatePickerContext'; -import { DateSegment } from '../../../hooks/useDateSegments'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { DatePickerContextProps } from '../../../shared/components/DatePickerContext'; +import { DateSegment, SegmentRefs } from '../../../shared/hooks'; type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; interface GetRelativeSegmentContext { diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts index 1be0cef9e7..f183924dac 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentKey/index.ts @@ -1,4 +1,4 @@ -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { SegmentRefs } from '../../../shared/hooks'; /** * Returns the key of a given segment ref or element diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts index c75967c46e..b651317ba0 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentRefFromDateTimeFormatPart/index.ts @@ -1,7 +1,6 @@ import isUndefined from 'lodash/isUndefined'; -import { isDateSegment } from '../../../hooks/useDateSegments'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; +import { isDateSegment, SegmentRefs } from '../../../shared/hooks'; /** * Given a {@link Intl.DateTimeFormatPart}, diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts index 47f8b944e6..142e0fab86 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts @@ -1,7 +1,7 @@ import { createRef } from 'react'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; -import { getFormatParts } from '../../../utils'; +import { SegmentRefs } from '../../../shared/hooks'; +import { getFormatParts } from '../../../shared/utils'; import { getSegmentToFocus } from '.'; diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts index b6b47d4af9..f06a7a6eca 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts @@ -1,10 +1,9 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { DatePickerContextProps } from '../../../DatePickerContext'; -import { DateSegment } from '../../../hooks/useDateSegments'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; -import { getFirstEmptySegment } from '../../../utils'; +import { DatePickerContextProps } from '../../../shared/components/DatePickerContext'; +import { DateSegment, SegmentRefs } from '../../../shared/hooks'; +import { getFirstEmptySegment } from '../../../shared/utils'; interface GetSegmentToFocusProps { target: EventTarget; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.styles.ts deleted file mode 100644 index 356c065c34..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.styles.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; - -export const baseStyles = css``; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx deleted file mode 100644 index d4a47f5cc7..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; - -import { useBackdropClick } from '@leafygreen-ui/hooks'; - -import { useDatePickerContext } from '../../DatePickerContext'; -import { useDateRangeContext } from '../DateRangeContext'; -import { DateRangeInput } from '../DateRangeInput'; -import { DateRangeMenu } from '../DateRangeMenu'; - -import { DateRangeComponentProps } from './DateRangeComponent.types'; - -export const DateRangeComponent = ({ - onCancel, - onClear, - showQuickSelection, - ...rest -}: DateRangeComponentProps) => { - const { isOpen, setOpen, menuId } = useDatePickerContext(); - - const { - refs: { formFieldRef, menuRef }, - } = useDateRangeContext(); - - useBackdropClick(() => setOpen(false), [formFieldRef, menuRef], isOpen); - - return ( - <> - - - - ); -}; - -DateRangeComponent.displayName = 'DateRangeComponent'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts deleted file mode 100644 index 7923477d99..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/DateRangeComponent.types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { DatePickerContextProps } from '../../DatePickerContext'; -import { DateRangeContextProps } from '../DateRangeContext/DateRangeContext.types'; -import { DateRangePickerProps } from '../DateRangePicker.types'; - -/** - * We pass into the component anything in - * DateRangePickerProps that is _not_ in - * DatePickerContext or DateRangeContext - */ -export interface DateRangeComponentProps - extends Omit< - DateRangePickerProps, - keyof (DatePickerContextProps & DateRangeContextProps) - > {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeComponent/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeComponent/index.ts deleted file mode 100644 index f8d2242e4d..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeComponent/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DateRangeComponent } from './DateRangeComponent'; -export { DateRangeComponentProps } from './DateRangeComponent.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.tsx b/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.tsx deleted file mode 100644 index 458ea3d5dc..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { - createContext, - PropsWithChildren, - useContext, - useMemo, - useState, -} from 'react'; - -import { DateRangeType, DateType } from '../../types'; -import { addMonthsUTC, setToUTCMidnight } from '../../utils'; -import { getInitialHighlight } from '../utils/getInitialHighlight'; -import { getInitialMonth } from '../utils/getInitialMonth'; - -import { - DateRangeContextProps, - DateRangeProviderProps, -} from './DateRangeContext.types'; -import { useDateRangeComponentRefs } from './useDateRangeComponentRefs'; - -/** A context for DateRange picker */ -const DateRangeContext = createContext( - {} as DateRangeContextProps, -); - -/** A hook to access DateRange picker context */ -export const useDateRangeContext = () => useContext(DateRangeContext); - -/** - * A provider for DateRange picker. - */ -export const DateRangeProvider = ({ - children, - value, - setValue: _setValue, - handleValidation, - rootRef, -}: PropsWithChildren) => { - const refs = useDateRangeComponentRefs(rootRef); - const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); - const [month, setMonth] = useState(getInitialMonth(value, today)); - const nextMonth = useMemo(() => addMonthsUTC(month, 1), [month]); - - /** Handle possible side effects here */ - const setValue = (newRange?: DateRangeType) => { - _setValue(newRange); - }; - - // Keep track of the element the user is highlighting with the keyboard - const [highlight, setHighlight] = useState( - getInitialHighlight(value, today, month), - ); - - const getHighlightedCell = () => { - const highlightKey = highlight?.toISOString(); - return highlightKey - ? refs.calendarCellRefs(highlightKey)?.current - : undefined; - }; - - return ( - - {children} - - ); -}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.types.ts deleted file mode 100644 index 90793f5678..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeContext/DateRangeContext.types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { DynamicRefGetter } from '@leafygreen-ui/hooks'; - -import { SegmentRefs } from '../../hooks/useSegmentRefs'; -import { DateRangeType, DateType } from '../../types'; -import { DateRangePickerProps } from '../DateRangePicker.types'; - -export interface DateRangeComponentRefs { - formFieldRef: React.RefObject; - menuRef: React.RefObject; - startSegmentRefs: SegmentRefs; - endSegmentRefs: SegmentRefs; - calendarSectionRef: React.RefObject; - chevronRefs: DynamicRefGetter; - calendarCellRefs: DynamicRefGetter; - footerButtonRefs: DynamicRefGetter; - selectRefs: DynamicRefGetter; - quickRangeButtonRefs: DynamicRefGetter; -} - -export interface DateRangeContextProps { - refs: DateRangeComponentRefs; - value: DateRangeType | undefined; - setValue: (newVal: DateRangeType | undefined) => void; - handleValidation: DateRangePickerProps['handleValidation']; - today: Date; - month: Date; - nextMonth: Date; - setMonth: React.Dispatch>; - highlight: DateType; - setHighlight: React.Dispatch>; - getHighlightedCell: () => HTMLTableCellElement | null | undefined; -} - -export interface DateRangeProviderProps { - rootRef: React.ForwardedRef; - value: DateRangeType | undefined; - setValue: (newVal: DateRangeType | undefined) => void; - handleValidation: DateRangePickerProps['handleValidation']; -} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeContext/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeContext/index.ts deleted file mode 100644 index 44e3bb3894..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeContext/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DateRangeProvider, useDateRangeContext } from './DateRangeContext'; -export { DateRangeProviderProps } from './DateRangeContext.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeContext/useDateRangeComponentRefs.tsx b/packages/date-picker/src/DateRangePicker/DateRangeContext/useDateRangeComponentRefs.tsx deleted file mode 100644 index 04ec8f80e8..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeContext/useDateRangeComponentRefs.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useRef } from 'react'; - -import { useDynamicRefs, useForwardedRef } from '@leafygreen-ui/hooks'; - -import { useSegmentRefs } from '../../hooks/useSegmentRefs'; - -import { DateRangeComponentRefs } from './DateRangeContext.types'; - -/** Creates `ref` objects for any & all relevant component elements */ -export const useDateRangeComponentRefs = ( - forwardedRef: React.ForwardedRef, -): DateRangeComponentRefs => { - const formFieldRef = useForwardedRef(forwardedRef, null); - const menuRef = useRef(null); - const startSegmentRefs = useSegmentRefs(); - const endSegmentRefs = useSegmentRefs(); - const calendarSectionRef = useRef(null); - const chevronRefs = useDynamicRefs(); - const calendarCellRefs = useDynamicRefs(); - const footerButtonRefs = useDynamicRefs(); - const selectRefs = useDynamicRefs(); - const quickRangeButtonRefs = useDynamicRefs(); - - return { - formFieldRef, - menuRef, - startSegmentRefs, - endSegmentRefs, - calendarSectionRef, - chevronRefs, - calendarCellRefs, - footerButtonRefs, - selectRefs, - quickRangeButtonRefs, - }; -}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx deleted file mode 100644 index 5451ca576f..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.spec.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { DateRangeInput } from '.'; - -describe('packages/date-picker/date-range-picker/date-range-input', () => { - describe('Keyboard interaction', () => { - // yyyy-mm-dd - describe('Left Arrow', () => { - test.todo('moves the cursor when the segment has a value'); - test.todo('focuses the previous segment when the segment is empty'); - test.todo( - 'focuses the previous segment if the cursor is at the start of the input text', - ); - }); - - describe('Right Arrow', () => { - test.todo('moves the cursor when the segment has a value'); - test.todo('focuses the next segment when the segment is empty'); - test.todo( - 'focuses the next segment if the cursor is at the end of the input text', - ); - }); - - describe('Backspace key', () => { - test.todo('deletes any value in the input'); - test.todo('deletes the whole value on multiple presses'); - test.todo('focuses the previous segment if current segment is empty'); - }); - }); -}); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx deleted file mode 100644 index 62da0f1ab9..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.stories.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from 'react'; -import { StoryFn } from '@storybook/react'; - -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { StoryMetaType } from '@leafygreen-ui/lib'; -import { Size } from '@leafygreen-ui/tokens'; - -import { - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; - -import { DateRangeInput } from './DateRangeInput'; - -const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - - - - - -); - -const meta: StoryMetaType = { - title: 'Components/DatePicker/DateRangePicker/DateRangeInput', - component: DateRangeInput, - decorators: [ProviderWrapper], - parameters: { - default: null, - controls: { - exclude: ['segmentRefs'], - }, - generate: { - combineArgs: { - darkMode: [false, true], - // value: [null, new Date('1993-12-26')], - dateFormat: ['iso8601', 'en-US', 'en-UK', 'de-DE'], - size: Object.values(Size), - }, - decorator: ProviderWrapper, - }, - }, - args: { - label: 'Label', - dateFormat: 'en-UK', - timeZone: 'Europe/London', - }, - argTypes: {}, -}; - -export default meta; - -export const Basic: StoryFn = props => { - return ( - <> - - - ); -}; - -export const Generated = () => {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.styles.ts deleted file mode 100644 index 4382dcd017..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.styles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; - -export const inputWrapperStyles = css` - display: flex; - gap: 12px; -`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx deleted file mode 100644 index 0123f3523b..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, { - FocusEventHandler, - forwardRef, - KeyboardEventHandler, - MouseEventHandler, -} from 'react'; - -import { keyMap } from '@leafygreen-ui/lib'; - -import { DateInputBox } from '../../DateInput'; -import { DateFormField } from '../../DateInput/DateFormField'; -import { useDatePickerContext } from '../../DatePickerContext'; -import { DateRangeType, DateType } from '../../types'; -import { isElementInputSegment, isSameUTCRange, isZeroLike } from '../../utils'; -import { useDateRangeContext } from '../DateRangeContext'; -import { getRangeSegmentToFocus } from '../utils/getRangeSegmentToFocus'; -import { getRelativeRangeSegment } from '../utils/getRelativeRangeSegment'; - -import { inputWrapperStyles } from './DateRangeInput.styles'; -import { DateRangeInputProps } from './DateRangeInput.types'; - -const EN_DASH = '–'; - -export const DateRangeInput = forwardRef( - ({ onChange, ...rest }: DateRangeInputProps, fwdRef) => { - const { disabled, formatParts, setOpen, isDirty, setIsDirty } = - useDatePickerContext(); - - const { - refs: { startSegmentRefs, endSegmentRefs }, - value, - setValue, - handleValidation, - getHighlightedCell, - } = useDateRangeContext(); - - /** Called when the input, or any of its children, is clicked */ - const handleInputClick: MouseEventHandler = ({ target }) => { - if (!disabled) { - setOpen(true); - } - - const segmentToFocus = getRangeSegmentToFocus({ - target, - formatParts, - segmentRefs: [startSegmentRefs, endSegmentRefs], - }); - - segmentToFocus?.focus(); - }; - - const handleKeyDown: KeyboardEventHandler = e => { - const { target: _target, key } = e; - const target = _target as HTMLElement; - const isSegment = - isElementInputSegment(target, startSegmentRefs) || - isElementInputSegment(target, endSegmentRefs); - - // if target is not a segment, do nothing - if (!isSegment) return; - - const isSegmentEmpty = isZeroLike(target.value); - const cursorPosition = target.selectionEnd; - - const ctx = { - target, - formatParts, - rangeSegmentRefs: [startSegmentRefs, endSegmentRefs], - }; - - switch (key) { - case keyMap.ArrowLeft: - if (isSegmentEmpty || cursorPosition === 0) { - const prevSegment = getRelativeRangeSegment('prev', ctx); - - prevSegment?.current?.focus(); - } - - break; - case keyMap.ArrowRight: - if (isSegmentEmpty || cursorPosition === target.value.length) { - const nextSegment = getRelativeRangeSegment('next', ctx); - - nextSegment?.current?.focus(); - } - break; - case keyMap.ArrowDown: - case keyMap.ArrowUp: - { - // default number input behavior - } - break; - - case keyMap.Backspace: { - if (isSegmentEmpty) { - const prevSegment = getRelativeRangeSegment('prev', ctx); - prevSegment?.current?.focus(); - } - break; - } - - case keyMap.Enter: - handleValidation?.(value); - break; - - case keyMap.Escape: - setOpen(false); - handleValidation?.(value); - break; - - case keyMap.Tab: - // default behavior - // focus trap handled by parent - break; - - default: - // any other keydown should open the menu - setOpen(true); - } - }; - - /** Called when any child of DatePickerInput is blurred */ - const handleInputBlur: FocusEventHandler = e => { - const nextFocus = e.relatedTarget as HTMLInputElement; - - const segmentElements = [startSegmentRefs, endSegmentRefs].flatMap(refs => - Object.values(refs).map(ref => ref.current), - ); - - const isNextFocusASegment = segmentElements.includes(nextFocus); - - // If the next focus is _not_ on a segment - if (!isNextFocusASegment) { - setIsDirty(true); - handleValidation?.(value); - } - }; - - const handleCalendarButtonClick: MouseEventHandler< - HTMLButtonElement - > = e => { - if (!disabled) { - e.stopPropagation(); - setOpen(true); - requestAnimationFrame(() => { - // once the menu is open - const highlightedCell = getHighlightedCell(); - highlightedCell?.focus(); - }); - } - }; - - /** Called when the input's start or end value has changed */ - const updateValue = (newRange?: DateRangeType) => { - if (!isSameUTCRange(value, newRange)) { - // When the value changes via the input element, - // we only trigger validation if the component is dirty - if (isDirty) { - handleValidation?.(newRange); - } - setValue(newRange); - } - }; - - const handleStartInputChange = (newStart: DateType) => { - const end = value ? value[1] : null; - updateValue([newStart, end]); - }; - - const handleEndInputChange = (newEnd: DateType) => { - const start = value ? value[0] : null; - updateValue([start, newEnd]); - }; - - return ( - -
- - {EN_DASH} - -
-
- ); - }, -); - -DateRangeInput.displayName = 'DateRangeInput'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts deleted file mode 100644 index 486b2c62cb..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/DateRangeInput.types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { HTMLElementProps } from '@leafygreen-ui/lib'; - -import { DateRangeComponentProps } from '../DateRangeComponent'; - -/** - * We pass into the Input specific properties of ComponentProps - * and any other `div` attributes - */ -export interface DateRangeInputProps - extends Pick, - Omit, 'onChange'> {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts deleted file mode 100644 index ffa12d47f9..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeInput/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DateRangeInput } from './DateRangeInput'; -export { DateRangeInputProps } from './DateRangeInput.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx deleted file mode 100644 index 1aab587d19..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.spec.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { addDays, subDays } from 'date-fns'; - -import { Month } from '../../constants'; -import { - DatePickerProvider, - DatePickerProviderProps, - defaultDatePickerContext, -} from '../../DatePickerContext'; -import { DateRangeType } from '../../types'; -import { newUTC } from '../../utils'; -import { - DateRangeProvider, - type DateRangeProviderProps, -} from '../DateRangeContext'; -import { testToday } from '../DateRangePicker.testutils'; - -import { DateRangeMenu, type DateRangeMenuProps } from '.'; - -const renderDateRangeMenu = (args?: { - props?: DateRangeMenuProps; - rangeContext?: Partial; - datePickerContext?: Partial; -}) => { - const results = render( - - {}} - handleValidation={() => {}} - rootRef={React.createRef()} - {...args?.rangeContext} - > - - - , - ); - - const calendarCells = results.getAllByTestId('lg-date_picker-calendar_cell'); - const todayCell = calendarCells.find( - cell => cell.getAttribute('aria-current') === 'true', - ); - const getCellForDate = (date: Date) => - calendarCells.find(cell => cell.dataset.iso === date.toISOString()); - - return { ...results, calendarCells, todayCell, getCellForDate }; -}; - -describe('packages/date-picker/date-range-picker/menu', () => { - beforeAll(() => { - // Set the current time to midnight UTC on 2023-12-26 - jest.useFakeTimers().setSystemTime(testToday); - }); - - test('initial focus is on `today`', () => { - const { todayCell } = renderDateRangeMenu(); - expect(todayCell).toHaveFocus(); - }); - - describe('Keyboard navigation', () => { - describe('Arrow Keys', () => { - const initialStart = newUTC(2023, Month.September, 14); - const value = [initialStart, null] as DateRangeType; - const rangeContext = { value }; - - describe('default behavior', () => { - test('left arrow moves focus to the previous day', () => { - const { getCellForDate } = renderDateRangeMenu({ rangeContext }); - userEvent.keyboard('{leftarrow}'); - const focusedCell = getCellForDate(subDays(initialStart, 1)); - expect(focusedCell).toHaveFocus(); - }); - test('right arrow moves focus to the next day', () => { - const { getCellForDate } = renderDateRangeMenu({ rangeContext }); - userEvent.keyboard('{rightarrow}'); - const focusedCell = getCellForDate(addDays(initialStart, 1)); - expect(focusedCell).toHaveFocus(); - }); - test('up arrow moves focus to the previous week', () => { - const { getCellForDate } = renderDateRangeMenu({ rangeContext }); - userEvent.keyboard('{uparrow}'); - const focusedCell = getCellForDate(subDays(initialStart, 7)); - expect(focusedCell).toHaveFocus(); - }); - test('down arrow moves focus to the next week', () => { - const { getCellForDate } = renderDateRangeMenu({ rangeContext }); - userEvent.keyboard('{downarrow}'); - const focusedCell = getCellForDate(addDays(initialStart, 7)); - expect(focusedCell).toHaveFocus(); - }); - }); - - describe('when next day would be out of range', () => { - test('left arrow does nothing', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext, - datePickerContext: { min: subDays(initialStart, 1) }, - }); - userEvent.keyboard('{leftarrow}'); - const focusedCell = getCellForDate(initialStart); - expect(focusedCell).toHaveFocus(); - }); - test('right arrow does nothing', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext, - datePickerContext: { max: addDays(initialStart, 1) }, - }); - userEvent.keyboard('{rightarrow}'); - const focusedCell = getCellForDate(initialStart); - expect(focusedCell).toHaveFocus(); - }); - test('up arrow does nothing', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext, - datePickerContext: { min: subDays(initialStart, 6) }, - }); - userEvent.keyboard('{uparrow}'); - const focusedCell = getCellForDate(initialStart); - expect(focusedCell).toHaveFocus(); - }); - test('down arrow does nothing', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext, - datePickerContext: { max: addDays(initialStart, 7) }, - }); - userEvent.keyboard('{downarrow}'); - const focusedCell = getCellForDate(initialStart); - expect(focusedCell).toHaveFocus(); - }); - }); - - describe('update the displayed month', () => { - test('left arrow updates displayed month to previous', () => { - const { getAllByRole } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 1), null] }, - }); - userEvent.keyboard('{leftarrow}'); - const calendarGrids = getAllByRole('grid'); - expect(calendarGrids[0]).toHaveAttribute('aria-label', 'August 2023'); - }); - test('right arrow updates displayed month to next', () => { - const { getAllByRole } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 30), null] }, - }); - userEvent.keyboard('{rightarrow}'); - const calendarGrids = getAllByRole('grid'); - expect(calendarGrids[0]).toHaveAttribute( - 'aria-label', - 'October 2023', - ); - }); - test('up arrow updates displayed month to previous', () => { - const { getAllByRole } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 6), null] }, - }); - userEvent.keyboard('{uparrow}'); - const calendarGrids = getAllByRole('grid'); - expect(calendarGrids[0]).toHaveAttribute('aria-label', 'August 2023'); - }); - test('down arrow updates displayed month to next', () => { - const { getAllByRole } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 24), null] }, - }); - userEvent.keyboard('{downarrow}'); - const calendarGrids = getAllByRole('grid'); - expect(calendarGrids[0]).toHaveAttribute( - 'aria-label', - 'October 2023', - ); - }); - test('does not update month when month does not need to change', () => { - const { getAllByRole } = renderDateRangeMenu({ - rangeContext: { value: [testToday, null] }, - }); - userEvent.keyboard('{downarrow}'); - const calendarGrids = getAllByRole('grid'); - expect(calendarGrids[0]).toHaveAttribute( - 'aria-label', - 'October 2023', - ); - }); - }); - - describe('when month should be updated', () => { - test('left arrow focuses the previous day', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 1), null] }, - }); - userEvent.keyboard('{leftarrow}'); - const focusedCell = getCellForDate(newUTC(2023, Month.August, 31)); - expect(focusedCell).toHaveFocus(); - }); - test('right arrow focuses the next day', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 30), null] }, - }); - userEvent.keyboard('{rightarrow}'); - const focusedCell = getCellForDate(newUTC(2023, Month.October, 1)); - expect(focusedCell).toHaveFocus(); - }); - test('up arrow focuses the previous week', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 6), null] }, - }); - userEvent.keyboard('{uparrow}'); - const focusedCell = getCellForDate(newUTC(2023, Month.August, 31)); - expect(focusedCell).toHaveFocus(); - }); - test('down arrow focuses the next week', () => { - const { getCellForDate } = renderDateRangeMenu({ - rangeContext: { value: [newUTC(2023, Month.September, 24), null] }, - }); - userEvent.keyboard('{downarrow}'); - const focusedCell = getCellForDate(newUTC(2023, Month.October, 1)); - expect(focusedCell).toHaveFocus(); - }); - }); - }); - }); -}); diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx deleted file mode 100644 index ef223c3d01..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.stories.tsx +++ /dev/null @@ -1,252 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks, react/prop-types */ -import React, { useRef } from 'react'; -import { Decorator, StoryObj } from '@storybook/react'; -import omit from 'lodash/omit'; -import MockDate from 'mockdate'; - -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { StoryMetaType } from '@leafygreen-ui/lib'; -import { transitionDuration } from '@leafygreen-ui/tokens'; -import { InlineCode } from '@leafygreen-ui/typography'; - -import { Month } from '../../constants'; -import { - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; -import { - contextPropNames, - defaultDatePickerContext, -} from '../../DatePickerContext/DatePickerContext.utils'; -import { newUTC } from '../../utils'; -import { DateRangeProvider } from '../DateRangeContext'; -import { DateRangeContextProps } from '../DateRangeContext/DateRangeContext.types'; - -import { DateRangeMenu } from './DateRangeMenu'; -import { DateRangeMenuProps } from './DateRangeMenu.types'; - -const mockToday = newUTC(2023, Month.September, 14); - -type DateRangeMenuStoryProps = DateRangeMenuProps & - DateRangeContextProps & - DatePickerContextProps; - -type DateRangeMenuStoryType = StoryObj; - -const MenuDecorator: Decorator = (Story, ctx) => { - const { - darkMode, - value, - label, - description, - dateFormat, - timeZone, - min, - max, - ...args - } = ctx.args as DateRangeMenuStoryProps; - - // Force `new Date()` to return `mockToday` - MockDate.set(mockToday); - - return ( - - - {}} - handleValidation={() => {}} - > - - - - - ); -}; - -const meta: StoryMetaType = { - title: 'Components/DatePicker/DateRangePicker/DateRangeMenu', - component: DateRangeMenu, - // @ts-expect-error - decorators: [MenuDecorator], - parameters: { - default: null, - chromatic: { - delay: transitionDuration.slower, - }, - }, - args: { - isOpen: true, - dateFormat: 'en-UK', - timeZone: 'Europe/London', - min: new Date('1996-10-14'), - max: new Date('2026-10-14'), - showQuickSelection: true, - }, - argTypes: {}, -}; - -export default meta; - -export const Basic: DateRangeMenuStoryType = { - render: (args: DateRangeMenuProps) => { - const props = omit(args, [...contextPropNames, 'isOpen']); - const refEl = useRef(null); - return ( - <> - refEl - - - ); - }, -}; - -export const WithValue: DateRangeMenuStoryType = { - args: { - value: [newUTC(2023, Month.September, 14), null], - }, - render: (args: DateRangeMenuProps) => { - const props = omit(args, [...contextPropNames, 'isOpen']); - const refEl = useRef(null); - return ( -
- refEl - -
- ); - }, -}; - -export const DarkMode: DateRangeMenuStoryType = { - ...WithValue, - args: { - darkMode: true, - }, -}; - -/** - * Chromatic Interaction tests - */ -// TODO: - -// type DateRangeMenuInteractionTestType = Omit & -// Required>; - -// export const InitialFocusToday: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// const { findByRole } = within(ctx.canvasElement.parentElement!); -// await findByRole('listbox'); -// userEvent.tab(); -// }, -// }; -// export const InitialFocusValue: DateRangeMenuInteractionTestType = { -// ...WithValue, -// play: async ctx => { -// const { findByRole } = within(ctx.canvasElement.parentElement!); -// await findByRole('listbox'); -// userEvent.tab(); -// }, -// }; - -// export const LeftArrowKey: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowleft}'); -// }, -// }; - -// export const RightArrowKey: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowright}'); -// }, -// }; - -// export const UpArrowKey: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowup}'); -// }, -// }; - -// export const DownArrowKey: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowdown}'); -// }, -// }; - -// export const UpToPrevMonth: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowup}{arrowup}'); -// }, -// }; - -// export const DownToNextMonth: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await InitialFocusToday.play(ctx); -// userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}'); -// }, -// }; - -// export const OpenMonthMenu: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// const canvas = within(ctx.canvasElement.parentElement!); -// await canvas.findByRole('listbox'); -// const monthMenu = await canvas.findByLabelText('Select month'); -// userEvent.click(monthMenu); -// }, -// }; - -// export const SelectJanuary: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await OpenMonthMenu.play(ctx); -// const { findAllByRole } = within(ctx.canvasElement.parentElement!); -// const options = await findAllByRole('option'); -// const Jan = options[0]; -// userEvent.click(Jan); -// }, -// }; - -// export const OpenYearMenu: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// const canvas = within(ctx.canvasElement.parentElement!); -// await canvas.findByRole('listbox'); -// const monthMenu = await canvas.findByLabelText('Select year'); -// userEvent.click(monthMenu); -// }, -// }; - -// export const Select2026: DateRangeMenuInteractionTestType = { -// ...Basic, -// play: async ctx => { -// await OpenYearMenu.play(ctx); -// const { findAllByRole } = within(ctx.canvasElement.parentElement!); -// const options = await findAllByRole('option'); -// const _2026 = last(options); -// userEvent.click(_2026!); -// }, -// }; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts deleted file mode 100644 index 5ef89b0e6b..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.styles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; - -import { calendarsClassName } from './DateRangeMenuCalendars/DateRangeMenuCalendars.styles'; -import { quickSelectionClassName } from './QuickSelectionMenu/QuickSelectionMenu.styles'; - -export const rangeMenuWrapperStyles = css` - padding: 0; // needs to be set by inner content - z-index: 1; -`; - -export const menuContentStyles = css` - display: grid; - grid-auto-flow: column; - grid-template-columns: max-content auto; - grid-template-areas: 'quick-select calendars'; - z-index: 1; - - & > .${quickSelectionClassName} { - grid-area: quick-select; - } - - & > .${calendarsClassName} { - grid-area: calendars; - } -`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx deleted file mode 100644 index 8ba9e4a283..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { - FocusEventHandler, - forwardRef, - KeyboardEventHandler, - MouseEventHandler, - useEffect, -} from 'react'; - -import { keyMap } from '@leafygreen-ui/lib'; -import { spacing } from '@leafygreen-ui/tokens'; - -import { MenuWrapper } from '../../Calendar/MenuWrapper'; -import { useDatePickerContext } from '../../DatePickerContext'; -import { DateRangeType } from '../../types'; -import { useDateRangeContext } from '../DateRangeContext'; - -import { - menuContentStyles, - rangeMenuWrapperStyles, -} from './DateRangeMenu.styles'; -import { DateRangeMenuProps } from './DateRangeMenu.types'; -import { DateRangeMenuCalendars } from './DateRangeMenuCalendars'; -import { DateRangeMenuFooter } from './DateRangeMenuFooter'; -import { QuickSelectionMenu } from './QuickSelectionMenu'; - -export const DateRangeMenu = forwardRef( - ( - { showQuickSelection, onCancel, onClear, ...rest }: DateRangeMenuProps, - fwdRef, - ) => { - // const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); - const { isOpen, setOpen, setIsDirty } = useDatePickerContext(); - const { value, setValue, getHighlightedCell, refs } = useDateRangeContext(); - - useEffect(() => { - // Once the menu opens, mark the input as dirty - if (isOpen) { - setIsDirty(true); - } - }, [setIsDirty, isOpen]); - - /** Called when any calendar cell is clicked */ - const updateValue = (newRange?: DateRangeType) => { - // TODO: more logic here - setValue(newRange); - }; - - /** Triggered any time an element in the menu is focused */ - const handleMenuFocus: FocusEventHandler = e => { - const element = e.target; - const previousFocus = e.relatedTarget; - - const isInitialMenuFocus = - refs.menuRef.current?.contains(element) && - !refs.menuRef.current.contains(previousFocus); - - if (isInitialMenuFocus) { - const highlightedCell = getHighlightedCell(); - highlightedCell?.focus(); - } - }; - - /** Triggered on any key down event */ - const handleKeyDown: KeyboardEventHandler = e => { - if (e.key === keyMap.Tab) { - const currentFocus = document.activeElement; - - // Focus trap: - // if focus is on the "Apply" button, move focus to either - // left chevron or month select menu - if (currentFocus === refs.footerButtonRefs('apply').current) { - const elementToFocus = showQuickSelection - ? refs.selectRefs('month').current?.querySelector('button') - : refs.chevronRefs('left').current; - - e.preventDefault(); - elementToFocus?.focus(); - } - } - }; - - /** Triggered when the Apply button is clicked */ - const handleApply: MouseEventHandler = _ => { - updateValue(value); - setOpen(false); - }; - - /** Triggered when the cancel button is clicked */ - const handleCancel: MouseEventHandler = e => { - updateValue(value); - setOpen(false); - onCancel?.(e); - }; - - /** Triggered when the clear button is clicked */ - const handleClear: MouseEventHandler = e => { - updateValue([null, null]); - onClear?.(e); - }; - - return ( - -
- {showQuickSelection && } - -
- -
- ); - }, -); - -DateRangeMenu.displayName = 'DateRangeMenu'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts deleted file mode 100644 index bbfef38409..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenu.types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HTMLElementProps } from '@leafygreen-ui/lib'; -import { PopoverProps, PortalControlProps } from '@leafygreen-ui/popover'; - -import { DateRangeComponentProps } from '../DateRangeComponent'; - -/** - * Pass into the menu specific properties of ComponentProps - * and any other `div` attributes - */ -export type DateRangeMenuProps = PortalControlProps & - Pick & - Pick & - HTMLElementProps<'div'> & {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts deleted file mode 100644 index d1ca8acd63..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.styles.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; -import { createUniqueClassName } from '@leafygreen-ui/lib'; -import { spacing, typeScales } from '@leafygreen-ui/tokens'; - -const calendarGapX = spacing[4] * 2; - -export const calendarsClassName = createUniqueClassName( - 'date-range-menu-calendars', -); - -export const calendarsFrameStyles = css` - display: grid; - grid-template-rows: 28px auto; // Size of icon-button - grid-template-areas: 'header' 'calendars'; - gap: ${spacing[3]}px; - padding: ${spacing[4]}px; - /* padding-top: ${spacing[6]}px; */ -`; - -export const calendarsContainerStyles = css` - grid-area: calendars; - display: flex; - gap: ${calendarGapX}px; - align-items: start; - height: fit-content; -`; - -export const calendarHeadersContainerStyle = css` - grid-area: header; - display: flex; - gap: ${calendarGapX}px; - align-items: center; -`; - -export const calendarHeaderStyles = css` - display: flex; - - width: 100%; - align-items: center; - - h6 { - font-size: ${typeScales.body2.fontSize}px; - line-height: ${typeScales.body2.lineHeight}px; - width: 100%; - text-align: center; - } -`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx deleted file mode 100644 index d39bd7e180..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import React, { - forwardRef, - KeyboardEventHandler, - MouseEventHandler, - useLayoutEffect, - useState, -} from 'react'; -import { addDays, isWithinInterval, subDays } from 'date-fns'; -import isNull from 'lodash/isNull'; -import isUndefined from 'lodash/isUndefined'; - -import { cx } from '@leafygreen-ui/emotion'; -import { usePrevious } from '@leafygreen-ui/hooks'; -import Icon from '@leafygreen-ui/icon'; -import IconButton from '@leafygreen-ui/icon-button'; -import { keyMap } from '@leafygreen-ui/lib'; -import { Subtitle } from '@leafygreen-ui/typography'; - -import { - CalendarCell, - CalendarCellState, - CalendarGrid, -} from '../../../Calendar'; -import { CalendarCellRangeState } from '../../../Calendar/CalendarCell'; -import { useDatePickerContext } from '../../../DatePickerContext'; -import { DateRangeType, DateType } from '../../../types'; -import { - getFullMonthLabel, - getUTCDateString, - isDefined, - isSameUTCDay, - isSameUTCMonth, - maxDate, - minDate, - setUTCMonth, -} from '../../../utils'; -import { useDateRangeContext } from '../../DateRangeContext'; - -import { - calendarHeadersContainerStyle, - calendarHeaderStyles, - calendarsClassName, - calendarsContainerStyles, - calendarsFrameStyles, -} from './DateRangeMenuCalendars.styles'; -import { DateRangeMenuCalendarsProps } from './DateRangeMenuCalendars.types'; - -export const DateRangeMenuCalendars = forwardRef< - HTMLDivElement, - DateRangeMenuCalendarsProps ->((_, fwdRef) => { - const { isInRange, isOpen, setOpen } = useDatePickerContext(); - const { - refs, - value, - setValue, - handleValidation, - highlight, - setHighlight, - month, - nextMonth, - setMonth: setDisplayMonth, - today, - } = useDateRangeContext(); - - const prevOpen = usePrevious(isOpen); - const [hoveredCell, setHover] = useState(null); - - /** On initial open, focus the cell */ - useLayoutEffect(() => { - if (highlight && isOpen && !prevOpen) { - const highlightCellRef = refs.calendarCellRefs(highlight.toISOString()); - highlightCellRef.current?.focus(); - } - }, [highlight, isOpen, prevOpen, refs]); - - /** - * setDisplayMonth with side effects - */ - const updateMonth = (newMonth: Date) => { - if (isSameUTCMonth(newMonth, month)) { - return; - } - - // const newHighlight = getNewHighlight(highlight, month, newMonth); - // const shouldUpdateHighlight = !isSameUTCDay(highlight, newHighlight); - - // if (newHighlight && shouldUpdateHighlight) { - // setHighlight(newHighlight); - // } - - setDisplayMonth(newMonth); - }; - - /** - * setHighlight with side effects - */ - const updateHighlight = (newHighlight: Date) => { - // change month if nextHighlight is different than `month` or `nextMonth` - if ( - !isSameUTCMonth(month, newHighlight) && - !isSameUTCMonth(nextMonth, newHighlight) - ) { - setDisplayMonth(newHighlight); - } - - // keep track of the highlighted cell - - if (!isSameUTCDay(newHighlight, highlight)) { - setHighlight(newHighlight); - - // After the DOM changes, focus the relevant cell - requestAnimationFrame(() => { - const highlightCellRef = refs.calendarCellRefs( - newHighlight.toISOString(), - ); - highlightCellRef.current?.focus(); - }); - } - }; - - /** - * Creates a click handler for a specific cell date - */ - const cellClickHandlerForDay = - (day: Date): MouseEventHandler => - () => { - if (isInRange(day)) { - // if no value is set, set the start date - if (!value || value.every(isNull)) { - setValue([day, null]); - } else if (value.some(isNull)) { - // if only one date is set, set both dates - const newRange: DateRangeType = [ - minDate([...value, day]) ?? day, - maxDate([...value, day]) ?? day, - ]; - setValue(newRange); - } else if (value.every(isDefined)) { - // if both values are set, set the start date & clear the end date - setValue([day, null]); - } - } - }; - - /** Returns the current state of the cell */ - const getCellState = (cellDay: Date): CalendarCellState => { - if (!isInRange(cellDay)) { - return CalendarCellState.Disabled; - } - - // if at least the start/end date is defined... - if (value && value.some(isDefined)) { - if ( - isSameUTCDay(cellDay, minDate(value)) || - isSameUTCDay(cellDay, maxDate(value)) - ) { - return CalendarCellState.Active; - } - } - - return CalendarCellState.Default; - }; - - const getCellRangeState = (cellDay: Date): CalendarCellRangeState => { - // if at least the start/end date is defined... - if (value && value.some(isDefined)) { - // for the purposes of visualizing the calendar, - // the range start is the earliest/latest date of the start/end value or hovered cell, - const rangeStart = minDate([...value, hoveredCell]); - const rangeEnd = maxDate([...value, hoveredCell]); - - if (isSameUTCDay(cellDay, rangeStart)) { - return CalendarCellRangeState.Start; - } - - if (isSameUTCDay(cellDay, rangeEnd)) { - return CalendarCellRangeState.End; - } - - // otherwise if the current cell is within the range of start & end - if ( - !isUndefined(rangeStart) && - !isUndefined(rangeEnd) && - isWithinInterval(cellDay, { start: rangeStart, end: rangeEnd }) - ) { - return CalendarCellRangeState.Range; - } - } - - return CalendarCellRangeState.None; - }; - - /** Called on any keydown within the menu element */ - const handleCalendarKeyDown: KeyboardEventHandler< - HTMLTableElement - > = event => { - const { key } = event; - const highlightStart = highlight || value?.[0] || value?.[1] || today; - let nextHighlight = highlightStart; - - switch (key) { - case keyMap.ArrowLeft: { - nextHighlight = subDays(highlightStart, 1); - break; - } - - case keyMap.ArrowRight: { - nextHighlight = addDays(highlightStart, 1); - break; - } - - case keyMap.ArrowUp: { - nextHighlight = subDays(highlightStart, 7); - break; - } - - case keyMap.ArrowDown: { - nextHighlight = addDays(highlightStart, 7); - break; - } - - case keyMap.Escape: - setOpen(false); - handleValidation?.(value); - break; - - default: - break; - } - - // if nextHighlight is in range - if (isInRange(nextHighlight)) { - updateHighlight(nextHighlight); - - // Prevent the parent keydown handler from being called - event.stopPropagation(); - } - }; - - const handleCellHover = - (day: Date): MouseEventHandler => - _ => { - setHover(day); - }; - - const handleCalendarMouseOut: MouseEventHandler = e => { - // TODO: improve this logic - if (e.target === e.currentTarget) { - // ... if the calendar container is the event's target - setHover(null); - } - }; - - const handleChevronClick = - (dir: 'left' | 'right'): MouseEventHandler => - () => { - const increment = dir === 'left' ? -1 : 1; - const newMonthIndex = month.getUTCMonth() + increment; - const newMonth = setUTCMonth(month, newMonthIndex); - updateMonth(newMonth); - }; - - return ( -
-
-
- - - - {getFullMonthLabel(month)} -
- -
- {getFullMonthLabel(nextMonth)} - - - -
-
- - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/mouse-events-have-key-events */} -
- - {(day, i) => ( - - {day.getUTCDate()} - - )} - - - - {(day, i) => ( - - {day.getUTCDate()} - - )} - -
-
- ); -}); - -DateRangeMenuCalendars.displayName = 'DateRangeMenuCalendars'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.types.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.types.ts deleted file mode 100644 index 095e9ca085..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/DateRangeMenuCalendars.types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { HTMLElementProps } from '@leafygreen-ui/lib'; - -export interface DateRangeMenuCalendarsProps extends HTMLElementProps<'div'> {} diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts deleted file mode 100644 index ac8f9eed41..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuCalendars/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DateRangeMenuCalendars } from './DateRangeMenuCalendars'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts deleted file mode 100644 index 0337561a15..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.styles.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; -import { palette } from '@leafygreen-ui/palette'; -import { spacing } from '@leafygreen-ui/tokens'; - -export const footerStyles = css` - display: flex; - width: 100%; - justify-content: space-between; - padding: ${spacing[2] + spacing[1]}px; - border-block-start: 1px solid ${palette.gray.light2}; -`; - -export const clearButtonStyles = css` - outline: none; - border: none; - background-color: unset; - z-index: 0; -`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx deleted file mode 100644 index 77b0237fba..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/DateRangeMenuFooter.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { forwardRef, MouseEventHandler } from 'react'; - -import Button, { Size, Variant } from '@leafygreen-ui/button'; -import { Link } from '@leafygreen-ui/typography'; - -import { useDateRangeContext } from '../../../DateRangePicker/DateRangeContext'; - -import { clearButtonStyles, footerStyles } from './DateRangeMenuFooter.styles'; - -interface DateRangeMenuFooterProps { - onApply: MouseEventHandler; - onCancel: MouseEventHandler; - onClear: MouseEventHandler; -} - -export const DateRangeMenuFooter = forwardRef< - HTMLDivElement, - DateRangeMenuFooterProps ->(({ onApply, onCancel, onClear }: DateRangeMenuFooterProps, fwdRef) => { - const { - refs: { footerButtonRefs }, - } = useDateRangeContext(); - - return ( -
- - Clear - -
- - -
-
- ); -}); - -DateRangeMenuFooter.displayName = 'DateRangeMenuFooter'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts deleted file mode 100644 index 4744d49ce2..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/DateRangeMenuFooter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DateRangeMenuFooter } from './DateRangeMenuFooter'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx deleted file mode 100644 index a4a3a56576..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { StoryMetaType, StoryType } from '@leafygreen-ui/lib'; - -import { QuickRangeButton } from './QuickRangeButton'; - -const meta: StoryMetaType = { - title: 'Components/DatePicker/DateRangePicker/QuickRangeButton', - component: QuickRangeButton, - parameters: { - default: null, - generate: { - combineArgs: { - darkMode: [false, true], - 'data-hover': [false, true], - 'data-focus': [false, true], - }, - decorator: (Instance, ctx) => ( - - - - ), - args: { - children: 'Last 12 months', - }, - }, - }, -}; - -export default meta; - -export const Basic: StoryType = () => { - return ; -}; - -export const Generated = () => <>; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts deleted file mode 100644 index 44bc927ef8..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.styles.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; -import { Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { - fontFamilies, - fontWeights, - spacing, - typeScales, -} from '@leafygreen-ui/tokens'; - -export const baseQuickRangeButtonStyles = css` - border: none; - outline: none; - background-color: unset; - display: block; - font-family: ${fontFamilies.default}; - font-size: ${typeScales.body1.fontSize}px; - line-height: ${typeScales.body1.lineHeight}px; - font-weight: ${fontWeights.regular}; - padding: 2px ${spacing[1]}px; - border-radius: ${spacing[2]}px; - cursor: pointer; -`; - -const hoverSelector = `&:hover, &[data-hover="true"]`; -const focusSelector = `&:focus-visible, &[data-focus="true"]`; - -export const baseQuickRangeButtonThemeStyles: Record = { - [Theme.Light]: css` - color: ${palette.black}; - - ${hoverSelector} { - outline: 2px solid ${palette.gray.light2}; - } - - ${focusSelector} { - color: ${palette.blue.dark1}; - outline: 2px solid ${palette.blue.light1}; - font-weight: ${fontWeights.bold}; - } - `, - [Theme.Dark]: css` - color: ${palette.white}; - - ${hoverSelector} { - outline: 2px solid ${palette.gray.dark2}; - } - - ${focusSelector} { - color: ${palette.blue.light1}; - outline: 2px solid ${palette.blue.light1}; - font-weight: ${fontWeights.bold}; - } - `, -}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx deleted file mode 100644 index f5508d68f8..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/QuickRangeButton.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { forwardRef } from 'react'; - -import { cx } from '@leafygreen-ui/emotion'; -import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { getNodeTextContent, HTMLElementProps } from '@leafygreen-ui/lib'; - -import { - baseQuickRangeButtonStyles, - baseQuickRangeButtonThemeStyles, -} from './QuickRangeButton.styles'; - -interface QuickRangeButtonProps - extends Omit, 'children'> { - label: string; -} - -export const QuickRangeButton = forwardRef< - HTMLButtonElement, - QuickRangeButtonProps ->(({ label, className, ...rest }, fwdRef) => { - const { theme } = useDarkMode(); - return ( - - ); -}); - -QuickRangeButton.displayName = 'QuickRangeButton'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts deleted file mode 100644 index 52c37b06e4..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickRangeButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { QuickRangeButton } from './QuickRangeButton'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts deleted file mode 100644 index 0f0f2d9653..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.styles.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; -import { createUniqueClassName, Theme } from '@leafygreen-ui/lib'; -import { palette } from '@leafygreen-ui/palette'; -import { spacing } from '@leafygreen-ui/tokens'; - -export const quickSelectionClassName = createUniqueClassName( - 'date-range-quick-selection', -); - -export const quickSelectMenuStyles = css` - display: flex; - flex-direction: column; - gap: ${spacing[4]}px; - padding: ${spacing[4]}px; - // TODO: Fix the menu vs clear button z-index -`; - -export const quickSelectMenuThemeStyles: Record = { - [Theme.Light]: css` - background-color: ${palette.gray.light3}; - border-inline-end: 1px solid ${palette.gray.light2}; - `, - [Theme.Dark]: css` - // TODO: - `, -}; - -export const quickSelectMenuMonthSelectContainerStyles = css` - display: flex; - flex-direction: column; - gap: ${spacing[2]}px; -`; - -export const quickSelectMenuSelectionsContainerStyles = css` - display: flex; - flex-direction: column; - align-items: start; - gap: ${spacing[2]}px; -`; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx deleted file mode 100644 index 3066318169..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/QuickSelectionMenu.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { MouseEventHandler } from 'react'; -import { forwardRef } from 'react'; -import { addDays } from 'date-fns'; -import isFinite from 'lodash/isFinite'; -import range from 'lodash/range'; -import { DateRangeType } from '../../../types'; - -import { cx } from '@leafygreen-ui/emotion'; -import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { Option, Select } from '@leafygreen-ui/select'; -import { Overline } from '@leafygreen-ui/typography'; - -import { - MAX_DATE, - MIN_DATE, - Months, - selectElementProps, -} from '../../../constants'; -import { useDatePickerContext } from '../../../DatePickerContext'; -import { - isSameUTCMonth, - setToUTCMidnight, - setUTCMonth, - setUTCYear, -} from '../../../utils'; -import { useDateRangeContext } from '../../DateRangeContext'; - -import { QuickRangeButton } from './QuickRangeButton'; -import { - quickSelectionClassName, - quickSelectMenuMonthSelectContainerStyles, - quickSelectMenuSelectionsContainerStyles, - quickSelectMenuStyles, - quickSelectMenuThemeStyles, -} from './QuickSelectionMenu.styles'; - -interface QuickRangeConfigObject { - id: string; - label: string; - relativeRange: [number, number]; -} - -const quickRangeButtonsConfig: Array = [ - { id: 'today', label: 'Today', relativeRange: [0, 0] }, - { id: 'yesterday', label: 'Yesterday', relativeRange: [-1, -1] }, - { id: 'last7', label: 'Last 7 days', relativeRange: [-7, 0] }, - { id: 'last30', label: 'Last 30 days', relativeRange: [-30, 0] }, - { id: 'last90', label: 'Last 90 days', relativeRange: [-90, 0] }, - { id: 'last12', label: 'Last 12 months', relativeRange: [-365, 0] }, - { id: 'all-time', label: 'All time', relativeRange: [-Infinity, 0] }, -]; - -interface QuickSelectionMenuProps {} - -export const QuickSelectionMenu = forwardRef< - HTMLDivElement, - QuickSelectionMenuProps ->((_, fwdRef) => { - const { theme } = useDarkMode(); - const { setOpen, min, max, isInRange } = useDatePickerContext(); - const { setValue, handleValidation, month, setMonth, refs, today } = - useDateRangeContext(); - - // TODO: is this the right logic? - const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); - - // TODO: refine this logic - const updateMonth = (newMonth: Date) => { - if (isInRange(newMonth)) { - setMonth(newMonth); - } - }; - - const updateValue = (range?: DateRangeType) => { - // When the value changes, - if (range && range[0] && !isSameUTCMonth(range[0], month)) { - updateMonth(setToUTCMidnight(range[0])); - } - - handleValidation?.(range); - setValue(range); - }; - - const quickRangeButtonHandler = - (id: string): MouseEventHandler => - () => { - const quickRange = quickRangeButtonsConfig.find(r => r.id === id); - - if (quickRange) { - const relativeStart = quickRange.relativeRange[0]; - const relativeEnd = quickRange.relativeRange[1]; - - const start = isFinite(relativeStart) - ? addDays(today, relativeStart) - : MIN_DATE; - const end = isFinite(relativeEnd) - ? addDays(today, relativeEnd) - : MAX_DATE; - - updateValue([start, end]); - setOpen(false); - } - }; - - return ( -
-
- - -
-
- Quick Ranges: - {quickRangeButtonsConfig.map(({ id, label }) => ( - - ))} -
-
- ); -}); - -QuickSelectionMenu.displayName = 'QuickSelectionMenu'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts deleted file mode 100644 index 980234417b..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/QuickSelectionMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { QuickSelectionMenu } from './QuickSelectionMenu'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts b/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts deleted file mode 100644 index e66e2b00e5..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangeMenu/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DateRangeMenu } from './DateRangeMenu'; -export { DateRangeMenuProps } from './DateRangeMenu.types'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx deleted file mode 100644 index a9a5ef3880..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.spec.tsx +++ /dev/null @@ -1,1205 +0,0 @@ -import React from 'react'; -import { - render, - waitFor, - waitForElementToBeRemoved, - within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import last from 'lodash/last'; - -import { Month } from '../constants'; -import { eventContainingTargetValue, tabNTimes } from '../testUtils'; -import { newUTC, setUTCDate, setUTCMonth } from '../utils'; - -import { - expectedTabStopLabels, - getTabStopElementMap, - quickSelectButtonTestCases, - renderDateRangePicker, -} from './DateRangePicker.testutils'; -import { DateRangePicker } from '.'; - -const testToday = newUTC(2023, Month.December, 26); - -describe('packages/date-picker/date-range-picker', () => { - beforeAll(() => { - // Set the current time to midnight UTC on 2023-12-26 - jest.useFakeTimers().setSystemTime(testToday); - }); - - describe('Rendering', () => { - /// Note: Many rendering tests should be handled by Chromatic - - describe('Input', () => { - test('renders label', () => { - const { getByText } = render(); - const label = getByText('Label'); - expect(label).toBeInTheDocument(); - }); - - test('renders description', () => { - const { getByText } = render( - , - ); - const description = getByText('Description'); - expect(description).toBeInTheDocument(); - }); - - test('spreads rest to formField', () => { - const { getByTestId } = render( - , - ); - const formField = getByTestId('lg-date-range-picker'); - expect(formField).toBeInTheDocument(); - }); - - test('formField contains label & input elements', () => { - const { getByTestId, getByRole } = render( - , - ); - const formField = getByTestId('lg-date-range-picker'); - const inputContainer = getByRole('combobox'); - expect(formField.querySelector('label')).toBeInTheDocument(); - expect(formField.querySelector('label')).toHaveTextContent('Label'); - expect(inputContainer).toBeInTheDocument(); - }); - - test('renders 6 inputs', () => { - const { inputElements } = renderDateRangePicker(); - expect(inputElements).toHaveLength(6); - }); - - test('renders `start` & `end` prop', () => { - const { inputElements } = renderDateRangePicker({ - value: [ - newUTC(2023, Month.January, 5), - newUTC(2023, Month.February, 14), - ], - }); - expect(inputElements[0].value).toEqual('2023'); - expect(inputElements[1].value).toEqual('01'); - expect(inputElements[2].value).toEqual('05'); - - expect(inputElements[3].value).toEqual('2023'); - expect(inputElements[4].value).toEqual('02'); - expect(inputElements[5].value).toEqual('14'); - }); - - test('renders `initialStart` & `initialEnd` prop', () => { - const { inputElements } = renderDateRangePicker({ - initialValue: [ - newUTC(2023, Month.July, 5), - newUTC(2023, Month.August, 10), - ], - }); - expect(inputElements[0].value).toEqual('2023'); - expect(inputElements[1].value).toEqual('07'); - expect(inputElements[2].value).toEqual('05'); - - expect(inputElements[3].value).toEqual('2023'); - expect(inputElements[4].value).toEqual('08'); - expect(inputElements[5].value).toEqual('10'); - }); - }); - - describe('Menu', () => { - test('menu is initially closed', () => { - const { getMenuElements } = renderDateRangePicker(); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('menu is initially open when rendered with `initialOpen`', async () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - }); - const { menuContainerEl } = getMenuElements(); - await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); - }); - - test('renders two calendar grids', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - }); - const { calendarGrids } = getMenuElements(); - expect(calendarGrids).toHaveLength(2); - }); - - test('chevrons have correct `aria-label`s', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - }); - const { leftChevron, rightChevron } = getMenuElements(); - expect(leftChevron).toHaveAttribute('aria-label', 'Previous month'); - expect(rightChevron).toHaveAttribute('aria-label', 'Next month'); - }); - - describe('Displayed month', () => { - test('if no value is set, menu displays current month', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - }); - const { calendarGrids, menuContainerEl } = getMenuElements(); - - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('December 2023'); - expect(headers?.[1]).toHaveTextContent('January 2024'); - - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'December 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'January 2024', - ); - }); - - test('if only a start value is set, menu displays the month of that value', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - value: [newUTC(2023, Month.March, 10), null], - }); - - const { calendarGrids, menuContainerEl } = getMenuElements(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('March 2023'); - expect(headers?.[1]).toHaveTextContent('April 2023'); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'March 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'April 2023', - ); - }); - - test('if only an end value is set, menu displays the month _before_ value', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - value: [null, newUTC(2023, Month.March, 10)], - }); - - const { calendarGrids, menuContainerEl } = getMenuElements(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('February 2023'); - expect(headers?.[1]).toHaveTextContent('March 2023'); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'February 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'March 2023', - ); - }); - - test('if a full value is set, menu displays the month of the start value', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - value: [ - newUTC(2023, Month.March, 10), - newUTC(2023, Month.April, 1), - ], - }); - - const { calendarGrids, menuContainerEl } = getMenuElements(); - const headers = menuContainerEl?.querySelectorAll('h6'); - expect(headers?.[0]).toHaveTextContent('March 2023'); - expect(headers?.[1]).toHaveTextContent('April 2023'); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'March 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'April 2023', - ); - }); - - test('month changes when value changes', () => { - const { getMenuElements, rerenderWithProps } = renderDateRangePicker({ - initialOpen: true, - }); - - rerenderWithProps({ - value: [ - newUTC(2023, Month.July, 5), - newUTC(2023, Month.September, 10), - ], - }); - - const { calendarGrids } = getMenuElements(); - - expect(calendarGrids?.[0]).toHaveAttribute('aria-label', 'July 2023'); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'August 2023', - ); - }); - }); - - test('renders the appropriate number of cells', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - value: [newUTC(2024, Month.February, 14), null], - }); - const { calendarCells } = getMenuElements(); - expect(calendarCells).toHaveLength(29 + 31); - }); - - test('rendered cells have correct `aria-label`', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - value: [newUTC(2024, Month.February, 14), null], - }); - const { calendarCells } = getMenuElements(); - expect(calendarCells[0]).toHaveAttribute( - 'aria-label', - 'Thu Feb 01 2024', - ); - }); - - describe('Quick select menu', () => { - describe('Month/Year select', () => { - test('render the correct text', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - }); - const { yearSelect, monthSelect } = getMenuElements(); - expect(monthSelect).toHaveTextContent('Dec'); - expect(yearSelect).toHaveTextContent('2023'); - }); - - test('have correct `aria-label`s', () => { - const { getMenuElements } = renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - }); - const { yearSelect, monthSelect } = getMenuElements(); - expect(monthSelect).toHaveAttribute('aria-label', 'Select month'); - expect(yearSelect).toHaveAttribute('aria-label', 'Select year'); - }); - - test('update when the value changes', () => { - const { getMenuElements, rerenderWithProps } = - renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - }); - - rerenderWithProps({ - value: [ - newUTC(2024, Month.July, 5), - newUTC(2024, Month.September, 10), - ], - }); - - const { yearSelect, monthSelect } = getMenuElements(); - expect(monthSelect).toHaveTextContent('July'); - expect(yearSelect).toHaveTextContent('2024'); - }); - }); - - test.each(quickSelectButtonTestCases)( - 'Renders correct label: $label', - ({ index, label }) => { - const { getAllByTestId } = renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - }); - const quickSelectButtons = getAllByTestId( - 'lg-date_picker-menu-quick-range-button', - ); - - expect(quickSelectButtons[index]).toHaveAttribute( - 'aria-label', - label, - ); - }, - ); - }); - }); - }); - - describe('Interaction', () => { - describe('Typing', () => { - test('opens the menu', () => { - const { inputElements, getMenuElements } = renderDateRangePicker(); - userEvent.type(inputElements[0], '1'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - describe.each([ - ['start', 0], - ['end', 3], - ])('into %p input', (input, index) => { - test('updates segment value', () => { - const { inputElements } = renderDateRangePicker(); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(element.value).toBe('1'); // Not '01' if not blurred - }); - - test('does not fire range change handler', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ onRangeChange }); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('fires segment change handler', () => { - const onChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - onChange, - }); - const element = inputElements[index]; - userEvent.type(element, '1'); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue('1'), - ); - }); - - describe('on un-focus/blur', () => { - test('does not fire a change handler if value is incomplete', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ onRangeChange }); - const element = inputElements[index]; - userEvent.type(element, '2023'); - userEvent.tab(); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('fires a change handler if the value is valid', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ onRangeChange }); - const year = inputElements[index]; - const month = inputElements[index + 1]; - const day = inputElements[index + 2]; - - userEvent.type(year, '2023'); - userEvent.type(month, '12'); - userEvent.type(day, '26'); - userEvent.tab(); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining(newUTC(2023, Month.December, 26)), - ]), - ); - }); - - test('fires a segment change handler', () => { - const onChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - onChange, - }); - const element = inputElements[index]; - userEvent.type(element, '2023'); - userEvent.tab(); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue('2023'), - ); - }); - - // eslint-disable-next-line jest/no-disabled-tests - describe.skip('validation handler', () => { - test('fired when the value is first set', () => { - const handleValidation = jest.fn(); - const { inputElements } = renderDateRangePicker({ - handleValidation, - }); - const year = inputElements[index]; - const month = inputElements[index + 1]; - const day = inputElements[index + 2]; - userEvent.type(year, '2023'); - userEvent.type(month, '12'); - userEvent.type(day, '26'); - userEvent.tab(); - - expect(handleValidation).toHaveBeenCalledWith( - expect.arrayContaining([ - expect.objectContaining(newUTC(2023, Month.December, 26)), - ]), - ); - }); - - test('fired when the value is updated', () => { - const initialStart = newUTC(2023, Month.March, 10); - const initialEnd = newUTC(2023, Month.December, 26); - const handleValidation = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - handleValidation, - }); - const day = inputElements[index + 2]; - userEvent.type(day, '15'); - - const expectedValue = expect.arrayContaining([ - input === 'start' - ? newUTC(2023, Month.March, 15) - : initialStart, - input === 'end' ? newUTC(2023, Month.December, 15) : initialEnd, - ]); - - expect(handleValidation).toHaveBeenCalledWith(expectedValue); - }); - }); - }); - }); - - describe('when entering a new value', () => { - const initialStart = newUTC(2023, Month.September, 2); - const initialEnd = newUTC(2023, Month.September, 10); - - test('start date is updated', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - onRangeChange, - }); - const startMonthInput = inputElements[1]; - userEvent.type(startMonthInput, '{delete}8'); - userEvent.tab(); - expect(onRangeChange).toHaveBeenCalledWith([ - setUTCMonth(initialStart, Month.August), - initialEnd, - ]); - }); - - test('end date is updated', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - onRangeChange, - }); - const endMonthInput = inputElements[4]; - userEvent.type(endMonthInput, '{delete}10'); - userEvent.tab(); - expect(onRangeChange).toHaveBeenCalledWith([ - initialStart, - setUTCMonth(initialEnd, Month.October), - ]); - }); - - test('input is rejected if start date is invalid', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - onRangeChange, - }); - const startMonthInput = inputElements[2]; - userEvent.type(startMonthInput, '1'); - userEvent.tab(); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('input is rejected if end date is invalid', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - onRangeChange, - }); - const endMonthInput = inputElements[4]; - userEvent.type(endMonthInput, '1'); - userEvent.tab(); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('input is rejected if start date is after end date', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [initialStart, initialEnd], - onRangeChange, - }); - const startDayInput = inputElements[2]; - userEvent.type(startDayInput, '1'); - userEvent.tab(); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('input is rejected if end date is before start date', () => { - const onRangeChange = jest.fn(); - const { inputElements } = renderDateRangePicker({ - value: [ - newUTC(2023, Month.September, 2), - newUTC(2023, Month.September, 10), - ], - onRangeChange, - }); - const endDayInput = inputElements[5]; - userEvent.type(endDayInput, '{backspace}'); - userEvent.tab(); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - }); - }); - - describe('Mouse interaction', () => { - describe('Input', () => { - describe('Clicking the input', () => { - test('opens the menu', () => { - const { inputContainer, getMenuElements } = renderDateRangePicker(); - userEvent.click(inputContainer); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('focuses the clicked segment', () => { - const { inputElements } = renderDateRangePicker(); - userEvent.click(inputElements[2]); - expect(document.activeElement).toBe(inputElements[2]); - }); - - test('focuses the first segment when all are empty (`undefined`)', () => { - const { inputContainer, inputElements } = renderDateRangePicker(); - userEvent.click(inputContainer); - expect(document.activeElement).toBe(inputElements[0]); - }); - - test('focuses the first segment when all are empty (`[null, null]`)', () => { - const { inputContainer, inputElements } = renderDateRangePicker({ - value: [null, null], - }); - userEvent.click(inputContainer); - expect(document.activeElement).toBe(inputElements[0]); - }); - - test('focuses the first empty segment in start input', () => { - const { inputContainer, inputElements } = renderDateRangePicker(); - userEvent.type(inputElements[0], '01'); - userEvent.click(inputContainer); - expect(document.activeElement).toBe(inputElements[1]); - }); - - test('focuses the first empty segment in end input when start value is set', () => { - const { inputContainer, inputElements } = renderDateRangePicker({ - value: [newUTC(2023, 1, 1), null], - }); - userEvent.click(inputContainer); - expect(document.activeElement).toBe(inputElements[3]); - }); - - test('focuses the last segment when all are filled', () => { - const { inputContainer, inputElements } = renderDateRangePicker({ - value: [newUTC(2023, 1, 1), newUTC(2023, 1, 14)], - }); - userEvent.click(inputContainer); - expect(document.activeElement).toBe(inputElements[5]); - }); - }); - - describe('Clicking the Calendar button', () => { - test('opens the menu', () => { - const { calendarButton, getMenuElements } = renderDateRangePicker(); - userEvent.click(calendarButton); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('focuses on the highlighted cell', async () => { - const { calendarButton, getMenuElements } = renderDateRangePicker(); - userEvent.click(calendarButton); - const { todayCell } = getMenuElements(); - await waitFor(() => { - expect(todayCell).toHaveFocus(); - }); - }); - - test('initial highlight on `start` value when provided', async () => { - const { calendarButton, getMenuElements } = renderDateRangePicker({ - value: [newUTC(2023, Month.September, 10), null], - }); - userEvent.click(calendarButton); - const { menuContainerEl } = getMenuElements(); - const sep10Cell = within(menuContainerEl!).getByLabelText( - 'Sun Sep 10 2023', - ); - await waitFor(() => { - expect(sep10Cell).toHaveFocus(); - }); - }); - test('initial highlight on `end` value when only end provided', async () => { - const { calendarButton, getMenuElements } = renderDateRangePicker({ - value: [null, newUTC(2023, Month.September, 10)], - }); - userEvent.click(calendarButton); - const { menuContainerEl } = getMenuElements(); - const sep10Cell = within(menuContainerEl!).getByLabelText( - 'Sun Sep 10 2023', - ); - await waitFor(() => { - expect(sep10Cell).toHaveFocus(); - }); - }); - }); - }); - - describe('Basic menu', () => { - describe('Clicking a calendar cell', () => { - test('if no value is set, fires a change handler to set the start date', () => { - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ - value: [null, null], - onRangeChange, - }); - const { calendarCells } = openMenu(); - const firstCell = calendarCells[0]; - userEvent.click(firstCell); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([setUTCDate(testToday, 1), null]), - ); - }); - - test('if only start value is set, fires change handler to set the end date', () => { - const onRangeChange = jest.fn(); - const startDate = newUTC(2023, Month.September, 10); - const { openMenu } = renderDateRangePicker({ - value: [startDate, null], - onRangeChange, - }); - const { calendarCells } = openMenu(); - const lastCell = last(calendarCells); - userEvent.click(lastCell!); - - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([ - startDate, - newUTC(2023, Month.October, 31), - ]), - ); - }); - - test('if only end value is set, fires change handler to set the start date', () => { - const onRangeChange = jest.fn(); - const endDate = newUTC(2023, Month.September, 10); - const { openMenu } = renderDateRangePicker({ - value: [null, endDate], - onRangeChange, - }); - const { calendarCells } = openMenu(); - const firstCell = calendarCells[0]; - userEvent.click(firstCell); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([newUTC(2023, Month.August, 1), endDate]), - ); - }); - - test('if a full range is set, fires a change handler to set the start date & clear the end date', () => { - const onRangeChange = jest.fn(); - const startDate = newUTC(2023, Month.September, 10); - const endDate = newUTC(2023, Month.October, 31); - const { openMenu } = renderDateRangePicker({ - value: [startDate, endDate], - onRangeChange, - }); - const { calendarCells } = openMenu(); - const firstCell = calendarCells[0]; - userEvent.click(firstCell); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([newUTC(2023, Month.September, 1), null]), - ); - }); - }); - - describe('Clicking a Chevron', () => { - describe('Left', () => { - test('does not close the menu', () => { - const { openMenu } = renderDateRangePicker(); - const { leftChevron, menuContainerEl } = openMenu(); - userEvent.click(leftChevron!); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('updates the displayed month to the previous', () => { - const { openMenu } = renderDateRangePicker(); - const { leftChevron, menuContainerEl } = openMenu(); - userEvent.click(leftChevron!); - const monthHeaders = menuContainerEl?.querySelectorAll('h6'); - expect(monthHeaders?.[0]).toHaveTextContent('November 2023'); - expect(monthHeaders?.[1]).toHaveTextContent('December 2023'); - }); - }); - - describe('Right', () => { - test('does not close the menu', () => { - const { openMenu } = renderDateRangePicker(); - const { rightChevron, menuContainerEl } = openMenu(); - userEvent.click(rightChevron!); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('updates the displayed month to the next', () => { - const { openMenu } = renderDateRangePicker(); - const { rightChevron, menuContainerEl } = openMenu(); - userEvent.click(rightChevron!); - const monthHeaders = menuContainerEl?.querySelectorAll('h6'); - expect(monthHeaders?.[0]).toHaveTextContent('January 2024'); - expect(monthHeaders?.[1]).toHaveTextContent('February 2024'); - }); - }); - }); - - describe('Clicking the Apply button', () => { - test('fires a change handler with the current input value', () => { - const start = newUTC(2023, Month.April, 1); - const end = newUTC(2023, Month.July, 5); - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ - onRangeChange, - value: [start, end], - }); - const { applyButton } = openMenu(); - userEvent.click(applyButton!); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([start, end]), - ); - }); - - test('closes menu', async () => { - const { openMenu } = renderDateRangePicker(); - const { applyButton, menuContainerEl } = openMenu(); - userEvent.click(applyButton!); - await waitForElementToBeRemoved(menuContainerEl); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - }); - - describe('Clicking the Cancel button', () => { - test('fires an onCancel handler', () => { - const start = newUTC(2023, Month.April, 1); - const end = newUTC(2023, Month.July, 5); - const onCancel = jest.fn(); - const { inputElements, getMenuElements } = renderDateRangePicker({ - onCancel, - value: [start, end], - }); - userEvent.type(inputElements[2], '5'); - const { cancelButton } = getMenuElements(); - userEvent.click(cancelButton!); - expect(onCancel).toHaveBeenCalled(); - }); - - test('fires a change handler with the previous input value', () => { - const start = newUTC(2023, Month.April, 1); - const end = newUTC(2023, Month.July, 5); - const onRangeChange = jest.fn(); - const { inputElements, getMenuElements } = renderDateRangePicker({ - onRangeChange, - value: [start, end], - }); - userEvent.type(inputElements[2], '5'); - const { cancelButton } = getMenuElements(); - userEvent.click(cancelButton!); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([start, end]), - ); - }); - - test('closes menu', async () => { - const { openMenu } = renderDateRangePicker(); - const { cancelButton, menuContainerEl } = openMenu(); - userEvent.click(cancelButton!); - await waitForElementToBeRemoved(menuContainerEl); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - }); - - describe('Clicking the Clear button', () => { - test('fires an onClear handler', () => { - const start = newUTC(2023, Month.April, 1); - const end = newUTC(2023, Month.July, 5); - const onClear = jest.fn(); - const { openMenu } = renderDateRangePicker({ - onClear, - value: [start, end], - }); - const { clearButton } = openMenu(); - userEvent.click(clearButton!); - expect(onClear).toHaveBeenCalled(); - }); - - test('fires a change handler with the to clear the range values', () => { - const start = newUTC(2023, Month.April, 1); - const end = newUTC(2023, Month.July, 5); - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ - onRangeChange, - value: [start, end], - }); - const { clearButton } = openMenu(); - userEvent.click(clearButton!); - expect(onRangeChange).toHaveBeenCalledWith( - expect.arrayContaining([null, null]), - ); - }); - - test('does not close the menu', () => { - const { openMenu } = renderDateRangePicker(); - const { clearButton, menuContainerEl } = openMenu(); - userEvent.click(clearButton!); - expect(menuContainerEl).toBeInTheDocument(); - }); - }); - }); - - describe('Quick Select menu', () => { - describe('Month select menu', () => { - test('menu opens over the calendar menu', () => { - const { openMenu, queryAllByRole } = renderDateRangePicker({ - showQuickSelection: true, - }); - const { monthSelect, menuContainerEl } = openMenu(); - userEvent.click(monthSelect!); - expect(menuContainerEl).toBeInTheDocument(); - const listBoxes = queryAllByRole('listbox'); - expect(listBoxes).toHaveLength(2); - }); - - test('selecting the month updates the calendar', async () => { - const { openMenu, findAllByRole } = renderDateRangePicker({ - showQuickSelection: true, - }); - const { monthSelect, calendarGrids } = openMenu(); - userEvent.click(monthSelect!); - const options = await findAllByRole('option'); - const Jan = options[0]; - userEvent.click(Jan); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'January 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'February 2023', - ); - }); - }); - - describe('Year select menu', () => { - test('menu opens over the calendar menu', () => { - const { openMenu, queryAllByRole } = renderDateRangePicker({ - showQuickSelection: true, - }); - const { yearSelect, menuContainerEl } = openMenu(); - userEvent.click(yearSelect!); - expect(menuContainerEl).toBeInTheDocument(); - const listBoxes = queryAllByRole('listbox'); - expect(listBoxes).toHaveLength(2); - }); - - test('selecting the year updates the calendar', async () => { - const { openMenu, findAllByRole } = renderDateRangePicker({ - showQuickSelection: true, - }); - const { yearSelect, calendarGrids } = openMenu(); - userEvent.click(yearSelect!); - const options = await findAllByRole('option'); - const _1970 = options[0]; - userEvent.click(_1970); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'December 1970', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'January 1971', - ); - }); - }); - - describe('Quick range buttons', () => { - test.each(quickSelectButtonTestCases)( - '$label button fires the correct change & validation handlers', - ({ index, expectedRange }) => { - const onRangeChange = jest.fn(); - const handleValidation = jest.fn(); - const { getAllByTestId } = renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - onRangeChange, - handleValidation, - }); - const quickSelectButtons = getAllByTestId( - 'lg-date_picker-menu-quick-range-button', - ); - userEvent.click(quickSelectButtons[index]); - expect(onRangeChange).toHaveBeenCalledWith(expectedRange); - expect(handleValidation).toHaveBeenCalledWith(expectedRange); - }, - ); - - test('update the displayed calendars', () => { - const { getAllByTestId, getMenuElements } = renderDateRangePicker({ - initialOpen: true, - showQuickSelection: true, - }); - const quickSelectButtons = getAllByTestId( - 'lg-date_picker-menu-quick-range-button', - ); - const { calendarGrids } = getMenuElements(); - const last90Button = quickSelectButtons[4]; - userEvent.click(last90Button); - expect(calendarGrids?.[0]).toHaveAttribute( - 'aria-label', - 'September 2023', - ); - expect(calendarGrids?.[1]).toHaveAttribute( - 'aria-label', - 'October 2023', - ); - }); - }); - }); - - describe('Backdrop', () => { - test('closes the menu', async () => { - const { openMenu, container } = renderDateRangePicker(); - const { menuContainerEl } = openMenu(); - userEvent.click(container.parentElement!); - await waitForElementToBeRemoved(menuContainerEl); - }); - - test('does not fire a change handler', () => { - const onRangeChange = jest.fn(); - const { openMenu, container } = renderDateRangePicker({ - onRangeChange, - }); - openMenu(); - userEvent.click(container.parentElement!); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - }); - }); - - describe('Keyboard interaction', () => { - describe('Tab', () => { - test('menu does not open on initial input focus', () => { - const { getMenuElements } = renderDateRangePicker(); - tabNTimes(1); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('menu does not open on subsequent input focuses', () => { - const { getMenuElements } = renderDateRangePicker(); - tabNTimes(5); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('calls validation handler when last segment is unfocused', () => { - const handleValidation = jest.fn(); - renderDateRangePicker({ handleValidation }); - tabNTimes(8); - expect(handleValidation).toHaveBeenCalled(); - }); - - test('does not call validation handler when changing segment', () => { - const handleValidation = jest.fn(); - renderDateRangePicker({ handleValidation }); - tabNTimes(1); - expect(handleValidation).not.toHaveBeenCalled(); - }); - - // TODO: Repeat these tests for values: - // `undefined`, `[null, null]` & `[Date, Date]` - describe('Tab order', () => { - describe('when menu is closed', () => { - const tabStops = expectedTabStopLabels['closed']; - - test('Tab order proceeds as expected', () => { - const renderResult = renderDateRangePicker(); - - for (const label of tabStops) { - const element = getTabStopElementMap(renderResult)[label]; - - if (element !== null) { - expect(element).toHaveFocus(); - } else { - expect( - renderResult.inputContainer.contains( - document.activeElement, - ), - ).toBeFalsy(); - } - - userEvent.tab(); - } - }); - }); - - describe('when basic menu is open', () => { - const tabStops = expectedTabStopLabels['basic']; // array of label strings - - test(`Tab order proceeds as expected`, () => { - const renderResult = renderDateRangePicker({ - showQuickSelection: false, - }); - renderResult.openMenu(); - - for (const label of tabStops) { - const element = getTabStopElementMap(renderResult)[label]; - expect(element).toHaveFocus(); - userEvent.tab(); - } - }); - }); - - describe('when quick-select menu is open', () => { - const tabStops = expectedTabStopLabels['quick-select']; - - test(`Tab order proceeds as expected`, () => { - const renderResult = renderDateRangePicker({ - showQuickSelection: true, - }); - renderResult.openMenu(); - - for (const label of tabStops) { - const element = getTabStopElementMap(renderResult)[label]; - expect(element).toHaveFocus(); - userEvent.tab(); - } - }); - }); - }); - }); - - describe('Enter key', () => { - test('if menu is closed, does not open the menu', () => { - const { getMenuElements } = renderDateRangePicker(); - userEvent.tab(); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('opens menu if calendar button is focused', () => { - const { getMenuElements } = renderDateRangePicker(); - tabNTimes(7); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('calls validation handler', () => { - const handleValidation = jest.fn(); - renderDateRangePicker({ - handleValidation, - }); - tabNTimes(2); - userEvent.keyboard('{enter}'); - expect(handleValidation).toHaveBeenCalled(); - }); - - test('if a cell is focused, fires a change handler', () => { - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ onRangeChange }); - const { todayCell } = openMenu(); - tabNTimes(7); - expect(todayCell).toHaveFocus(); - userEvent.keyboard('{enter}'); - expect(onRangeChange).toHaveBeenCalled(); - }); - - test('if a cell is focused, closes the menu', async () => { - const { openMenu } = renderDateRangePicker(); - const { todayCell, menuContainerEl } = openMenu(); - tabNTimes(7); - expect(todayCell).toHaveFocus(); - userEvent.keyboard('{enter}'); - await waitForElementToBeRemoved(menuContainerEl); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('if a Chevron is focused, updates the displayed month', () => { - const { openMenu } = renderDateRangePicker(); - const { leftChevron, menuContainerEl } = openMenu(); - tabNTimes(11); - expect(leftChevron).toHaveFocus(); - userEvent.keyboard('{enter}'); - const monthHeaders = menuContainerEl?.querySelectorAll('h6'); - expect(monthHeaders?.[0]).toHaveTextContent('November 2023'); - expect(monthHeaders?.[1]).toHaveTextContent('December 2023'); - }); - - test('if Quick Select button is clicked, fires change handler', () => { - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ - showQuickSelection: true, - onRangeChange, - }); - const { quickRangeButtons } = openMenu(); - tabNTimes(13); - expect(quickRangeButtons?.[0]).toHaveFocus(); - userEvent.keyboard('{enter}'); - expect(onRangeChange).toHaveBeenCalled(); // TODO: with - }); - - test('if month/year select is focused, opens the select menu', () => { - const { openMenu, queryAllByRole } = renderDateRangePicker({ - showQuickSelection: true, - }); - const { monthSelect } = openMenu(); - tabNTimes(11); - expect(monthSelect).toHaveFocus(); - userEvent.keyboard('{enter}'); - const listBoxes = queryAllByRole('listbox'); - expect(listBoxes).toHaveLength(2); - }); - }); - - describe('Escape key', () => { - test('closes the menu', async () => { - const { openMenu } = renderDateRangePicker(); - const { menuContainerEl } = openMenu(); - userEvent.keyboard('{escape}'); - await waitForElementToBeRemoved(menuContainerEl); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('does not fire a change handler', () => { - const onRangeChange = jest.fn(); - const { openMenu } = renderDateRangePicker({ onRangeChange }); - openMenu(); - userEvent.keyboard('{escape}'); - expect(onRangeChange).not.toHaveBeenCalled(); - }); - - test('fires a validation handler', async () => { - const handleValidation = jest.fn(); - const { openMenu } = renderDateRangePicker({ - handleValidation, - value: [null, null], - }); - openMenu(); - userEvent.tab(); - userEvent.keyboard('{escape}'); - await waitFor(() => { - expect(handleValidation).toHaveBeenCalledWith([null, null]); - }); - }); - - test('focus remains in the input element', () => { - const handleValidation = jest.fn(); - const { openMenu, inputContainer } = renderDateRangePicker({ - handleValidation, - }); - openMenu(); - userEvent.keyboard('{escape}'); - expect(inputContainer.contains(document.activeElement)).toBeTruthy(); - }); - }); - - /** - * Arrow Keys: - * Since arrow key behavior changes based on whether the input or menu is focused, - * many of these tests exist in the "DatePickerInput" and "DatePickerMenu" components - */ - }); - }); -}); diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx deleted file mode 100644 index a82b7502fd..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.stories.tsx +++ /dev/null @@ -1,160 +0,0 @@ -/* eslint-disable no-console */ -/* eslint-disable react/prop-types */ -import React, { useState } from 'react'; -import { Decorator, StoryFn } from '@storybook/react'; -import { mockDateDecorator } from 'storybook-mock-date-decorator'; - -import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { StoryMetaType } from '@leafygreen-ui/lib'; -import { Size } from '@leafygreen-ui/tokens'; - -import { Month } from '../constants'; -import { - DatePickerContextProps, - DatePickerProvider, -} from '../DatePickerContext'; -import { - getProviderPropsFromStoryContext, - Locales, - TimeZones, -} from '../testUtils'; -import { DatePickerState, DateRangeType } from '../types'; -import { newUTC } from '../utils'; - -import { DateRangePicker } from './DateRangePicker'; - -const ProviderWrapper: Decorator = (Story, ctx) => { - const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = - getProviderPropsFromStoryContext(ctx); - - return ( - - - - - - ); -}; - -const mockToday = newUTC(2023, Month.September, 14); - -const meta: StoryMetaType = { - title: 'Components/DatePicker/DateRangePicker', - component: DateRangePicker, - decorators: [mockDateDecorator, ProviderWrapper], - parameters: { - default: null, - date: mockToday, - controls: { - exclude: [ - 'handleValidation', - 'initialValue', - 'onChange', - 'onRangeChange', - 'onCancel', - 'onClear', - 'value', - ], - }, - }, - args: { - dateFormat: 'en-US', - label: 'Pick a Range', - description: 'Coordinated Universal Time', - min: newUTC(1996, Month.October, 14), - max: newUTC(2026, Month.October, 14), - size: Size.Default, - timeZone: 'America/New_York', - }, - argTypes: { - baseFontSize: { control: 'select' }, - dateFormat: { control: 'select', options: Locales }, - description: { control: 'text' }, - label: { control: 'text' }, - min: { control: 'date' }, - max: { control: 'date' }, - size: { control: 'select' }, - state: { control: 'select' }, - timeZone: { control: 'select', options: TimeZones }, - }, -}; - -export default meta; - -export const Basic: StoryFn = props => { - const [range, setRange] = useState([ - newUTC(2023, Month.October, 14), - newUTC(2023, Month.December, 26), - ]); - - const handleRangeChange = (range?: DateRangeType) => { - console.log('Storybook: Range changed', range); - setRange(range); - }; - - return ( - - ); -}; - -export const WithQuickSelection: StoryFn = props => { - const [range, setRange] = useState([ - newUTC(2023, Month.October, 14), - newUTC(2023, Month.December, 26), - ]); - - return ( - - ); -}; - -export const Uncontrolled: StoryFn = props => { - return ; -}; - -export const WithValidation: StoryFn = props => { - const expectedDate = newUTC(2023, Month.December, 26); - - const [state, setState] = useState(DatePickerState.None); - const [range, setRange] = useState([null, null]); - - const handleRangeChange = (range?: DateRangeType) => { - console.log('Storybook: Range changed', range); - setRange(range); - }; - - const handleValidation = (range?: DateRangeType) => { - console.log('Storybook: Handling validation', range); - - if (range && range[0] === expectedDate) { - setState(DatePickerState.None); - } - setState(DatePickerState.Error); - }; - - return ( - - ); -}; - -// export const Generated = () => {}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts b/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts deleted file mode 100644 index 356c065c34..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.styles.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { css } from '@leafygreen-ui/emotion'; - -export const baseStyles = css``; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx deleted file mode 100644 index 0a5c352128..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.testutils.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import React from 'react'; -import { - getByRole as globalGetByRole, - render, - RenderResult, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { subDays } from 'date-fns'; - -import { MIN_DATE, Month } from '../constants'; -import { newUTC } from '../utils'; - -import { DateRangePicker, DateRangePickerProps } from '.'; - -export const testToday = newUTC(2023, Month.December, 26); - -/** Explicit test cases for quick range buttons */ -export const quickSelectButtonTestCases = [ - { label: 'Today', expectedRange: [testToday, testToday] }, - { - label: 'Yesterday', - expectedRange: [subDays(testToday, 1), subDays(testToday, 1)], - }, - { label: 'Last 7 days', expectedRange: [subDays(testToday, 7), testToday] }, - { label: 'Last 30 days', expectedRange: [subDays(testToday, 30), testToday] }, - { label: 'Last 90 days', expectedRange: [subDays(testToday, 90), testToday] }, - { - label: 'Last 12 months', - expectedRange: [subDays(testToday, 365), testToday], - }, - { label: 'All time', expectedRange: [MIN_DATE, testToday] }, -].map((arr, index) => ({ index, ...arr })); - -export interface RenderDateRangePickerResult extends RenderResult { - formField: HTMLElement; - inputContainer: HTMLElement; - inputElements: Array; - calendarButton: HTMLButtonElement; - getMenuElements: () => RenderMenuResult; - openMenu: () => RenderMenuResult; - rerenderWithProps: (p?: Partial) => void; -} - -export interface RenderMenuResult { - menuContainerEl: HTMLElement | null; - leftChevron: HTMLButtonElement | null; - rightChevron: HTMLButtonElement | null; - calendarGrids: Array | null; - calendarCells: Array; - todayCell: HTMLTableCellElement | null; - menuFooter: HTMLDivElement | null; - clearButton: HTMLButtonElement | null; - cancelButton: HTMLButtonElement | null; - applyButton: HTMLButtonElement | null; - quickSelectMenu: HTMLDivElement | null; - monthSelect: HTMLButtonElement | null; - yearSelect: HTMLButtonElement | null; - quickRangeButtons: Array; -} - -/** - * Renders a date picker for jest environments - */ -export const renderDateRangePicker = ( - props?: Partial, -): RenderDateRangePickerResult => { - const defaultProps = { label: '' }; - const result = render( - , - ); - - function rerenderWithProps(newProps?: Partial) { - return result?.rerender( - , - ); - } - - const formField = result.getByTestId('lg-date-picker'); - const inputContainer = result.getByRole('combobox'); - - const inputElements = Array.from(inputContainer.querySelectorAll('input')); - - const calendarButton = globalGetByRole( - inputContainer, - 'button', - ) as HTMLButtonElement; - - /** - * Returns relevant menu elements. - * Call this after the menu has been opened - */ - function getMenuElements(): RenderMenuResult { - const menuContainerEl = result.queryByRole('listbox'); - - const calendarGrids = result.queryAllByRole( - 'grid', - ) as Array; - - const calendarCells = result.queryAllByRole( - 'gridcell', - ) as Array; - - // label text is tested in DatePickerMenu.spec - const leftChevron = result.queryByLabelText( - 'Previous month', - ) as HTMLButtonElement; - const rightChevron = result.queryByLabelText( - 'Next month', - ) as HTMLButtonElement; - const todayCell = menuContainerEl?.querySelector( - '[aria-current="true"]', - ) as HTMLTableCellElement; - - // Footer - const menuFooter = menuContainerEl?.querySelector( - '[data-testid="lg-date_picker-menu-footer"]', - ) as HTMLDivElement | null; - const clearButton = result.queryByLabelText( - 'Clear selection', - ) as HTMLButtonElement | null; - const cancelButton = result.queryByLabelText( - 'Cancel selection', - ) as HTMLButtonElement | null; - const applyButton = result.queryByLabelText( - 'Apply selection', - ) as HTMLButtonElement | null; - - // Quick select menu - const quickSelectMenu = menuContainerEl?.querySelector( - '[data-testid="lg-date_picker-menu-quick_select"]', - ) as HTMLDivElement | null; - const monthSelect = result.queryByLabelText( - 'Select month', - ) as HTMLButtonElement | null; - const yearSelect = result.queryByLabelText( - 'Select year', - ) as HTMLButtonElement | null; - - const quickRangeButtons = Array.from( - quickSelectMenu?.querySelectorAll( - '[data-testid="lg-date_picker-menu-quick-range-button"]', - ) || [null], - ) as Array; - - return { - menuContainerEl, - calendarGrids, - calendarCells, - todayCell, - leftChevron, - rightChevron, - menuFooter, - clearButton, - cancelButton, - applyButton, - quickSelectMenu, - monthSelect, - yearSelect, - quickRangeButtons, - }; - } - - function openMenu(): RenderMenuResult { - userEvent.click(inputContainer); - return getMenuElements(); - } - - return { - ...result, - rerenderWithProps, - formField, - inputContainer, - inputElements, - calendarButton, - getMenuElements, - openMenu, - }; -}; - -export interface ExpectedTabStop { - name: string; - selector: string; -} - -/** - * Returns the elements we expect to have focus after pressing `Tab` N times - */ -export const expectedTabStopLabels = { - closed: [ - 'none', - 'input > start date > year segment', - 'input > start date > month segment', - 'input > start date > day segment', - 'input > end date > year segment', - 'input > end date > month segment', - 'input > end date > day segment', - 'input > open menu button', - 'none', - ], - basic: [ - 'input > start date > year segment', - 'input > start date > month segment', - 'input > start date > day segment', - 'input > end date > year segment', - 'input > end date > month segment', - 'input > end date > day segment', - 'input > open menu button', - `menu > today cell`, - 'menu > footer > clear button', - 'menu > footer > cancel button', - 'menu > footer > apply button', - 'menu > left chevron', - 'menu > right chevron', - `menu > today cell`, - ], - 'quick-select': [ - 'input > start date > year segment', - 'input > start date > month segment', - 'input > start date > day segment', - 'input > end date > year segment', - 'input > end date > month segment', - 'input > end date > day segment', - 'input > open menu button', - `menu > today cell`, - 'menu > footer > clear button', - 'menu > footer > cancel button', - 'menu > footer > apply button', - 'menu > quick select > month select', - 'menu > quick select > year select', - 'menu > quick select > quick range > Today', - 'menu > quick select > quick range > Yesterday', - 'menu > quick select > quick range > Last 7 days', - 'menu > quick select > quick range > Last 30 days', - 'menu > quick select > quick range > Last 90 days', - 'menu > quick select > quick range > Last 12 months', - 'menu > quick select > quick range > All time', - 'menu > left chevron', - 'menu > right chevron', - `menu > today cell`, - ], -} as const; - -type TabStopLabel = - (typeof expectedTabStopLabels)[keyof typeof expectedTabStopLabels][number]; - -export const getTabStopElementMap = ( - renderResult: RenderDateRangePickerResult, -): Record => { - const { inputElements, calendarButton, getMenuElements } = renderResult; - const { - todayCell, - clearButton, - cancelButton, - applyButton, - monthSelect, - yearSelect, - leftChevron, - rightChevron, - quickRangeButtons, - } = getMenuElements(); - return { - none: null, - 'input > start date > year segment': inputElements[0], - 'input > start date > month segment': inputElements[1], - 'input > start date > day segment': inputElements[2], - 'input > end date > year segment': inputElements[3], - 'input > end date > month segment': inputElements[4], - 'input > end date > day segment': inputElements[5], - 'input > open menu button': calendarButton, - 'menu > today cell': todayCell, - 'menu > footer > clear button': clearButton, - 'menu > footer > cancel button': cancelButton, - 'menu > footer > apply button': applyButton, - 'menu > quick select > month select': monthSelect, - 'menu > quick select > year select': yearSelect, - 'menu > quick select > quick range > Today': quickRangeButtons[0], - 'menu > quick select > quick range > Yesterday': quickRangeButtons[1], - 'menu > quick select > quick range > Last 7 days': quickRangeButtons[2], - 'menu > quick select > quick range > Last 30 days': quickRangeButtons[3], - 'menu > quick select > quick range > Last 90 days': quickRangeButtons[4], - 'menu > quick select > quick range > Last 12 months': quickRangeButtons[5], - 'menu > quick select > quick range > All time': quickRangeButtons[6], - 'menu > left chevron': leftChevron, - 'menu > right chevron': rightChevron, - }; -}; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx b/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx deleted file mode 100644 index 476faf472f..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { forwardRef } from 'react'; - -import { contextPropNames, DatePickerProvider } from '../DatePickerContext'; -import { useControlledValue } from '../hooks/useControlledValue'; -import { pickAndOmit } from '../utils'; - -import { DateRangeComponent } from './DateRangeComponent'; -import { DateRangeProvider } from './DateRangeContext'; -import { DateRangePickerProps } from './DateRangePicker.types'; - -export const DateRangePicker = forwardRef( - ( - { - value: rangeProp, - initialValue: initialProp, - onRangeChange: onChangeProp, - handleValidation, - ...props - }: DateRangePickerProps, - fwdRef, - ) => { - const [datePickerContextProps, restProps] = pickAndOmit( - props, - contextPropNames, - ); - - const { value, setValue } = useControlledValue( - rangeProp, - onChangeProp, - initialProp, - ); - - return ( - - - - - - ); - }, -); - -DateRangePicker.displayName = 'DateRangePicker'; diff --git a/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts b/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts deleted file mode 100644 index dcb1b700df..0000000000 --- a/packages/date-picker/src/DateRangePicker/DateRangePicker.types.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ChangeEvent, MouseEventHandler } from 'react'; - -import { BaseDatePickerProps, DateRangeType } from '../types'; - -export interface DateRangePickerProps extends BaseDatePickerProps { - /** - * The selected start & end date - */ - value?: DateRangeType; - - /** - * Callback fired when the “Apply” button is clicked, or when either the start or end date changes. If either start or end is unset, then that value will be null. - * - * Parameter `range[0]` represents the start date, - * and `range[1]` represents the end date. - * - * Callback date arguments will be in Date objects in UTC time, or null - */ - onRangeChange?: (range?: DateRangeType) => void; - - /** - * Callback fired when any segment changes, (but not necessarily a full value) - */ - onChange?: (event: ChangeEvent) => void; - - /** - * The initial selected start & end dates. - * - * A given initial index is ignored if `range[x]` is defined - */ - initialValue?: DateRangeType; - - /** - * A callback fired when validation should run, based on our form validation guidelines. - * Use this callback to compute the correct `state` value. - * - * Parameter `range[0]` represents the start date, and `range[1]` represents the end date. - * - * Callback date arguments will be in Date objects in UTC time, or null - */ - handleValidation?: (range?: DateRangeType) => void; - - /** - * Callback fired when the “clear” button is clicked. - */ - onClear?: MouseEventHandler; - - /** - * Callback fired when the “cancel” button is clicked. - */ - onCancel?: MouseEventHandler; - - /** - * Whether or not to show the Quick Range Selection menu - */ - showQuickSelection?: boolean; -} diff --git a/packages/date-picker/src/DateRangePicker/index.ts b/packages/date-picker/src/DateRangePicker/index.ts deleted file mode 100644 index 4b0004f344..0000000000 --- a/packages/date-picker/src/DateRangePicker/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { DateRangePicker } from './DateRangePicker'; -export { DateRangePickerProps } from './DateRangePicker.types'; diff --git a/packages/date-picker/src/DateRangePicker/utils/getInitialHighlight/index.ts b/packages/date-picker/src/DateRangePicker/utils/getInitialHighlight/index.ts deleted file mode 100644 index dadb7ef97e..0000000000 --- a/packages/date-picker/src/DateRangePicker/utils/getInitialHighlight/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DateRangeType } from '../../../types'; -import { isSameUTCMonth } from '../../../utils'; - -export const getInitialHighlight = ( - value: DateRangeType | undefined, - today: Date, - month: Date, -): Date => { - if (value) { - const [start, end] = value; - // Could use ternary, but that's hard to read - if (start) return start; - if (end) return end; - if (isSameUTCMonth(today, month)) return today; - return month; - } - - return today; -}; diff --git a/packages/date-picker/src/DateRangePicker/utils/getInitialMonth/index.ts b/packages/date-picker/src/DateRangePicker/utils/getInitialMonth/index.ts deleted file mode 100644 index 0d2bec068a..0000000000 --- a/packages/date-picker/src/DateRangePicker/utils/getInitialMonth/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DateRangeType } from '../../../types'; -import { addMonthsUTC, getFirstOfMonth, isDefined } from '../../../utils'; - -export const getInitialMonth = ( - value: DateRangeType | undefined, - today: Date, -): Date => { - if (isDefined(value)) { - const [start, end] = value; - // `start` if it exists, - // otherwise one month before `end` if it exists, - // fallback to today - const initial = start ? start : end ? addMonthsUTC(end, -1) : today; - - return getFirstOfMonth(initial); - } - - return getFirstOfMonth(today); -}; diff --git a/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts b/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts deleted file mode 100644 index 3b9497002b..0000000000 --- a/packages/date-picker/src/DateRangePicker/utils/getRangeSegmentToFocus/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import isUndefined from 'lodash/isUndefined'; -import last from 'lodash/last'; -import { DatePickerContextProps } from 'src/DatePickerContext'; - -import { DateSegment } from '../../../hooks/useDateSegments'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; -import { getFirstEmptySegment } from '../../../utils'; - -interface GetRangeSegmentToFocusProps { - target: EventTarget; - formatParts: DatePickerContextProps['formatParts']; - segmentRefs: [SegmentRefs, SegmentRefs]; -} - -/** Returns the range segment to focus on when the input is clicked */ -export const getRangeSegmentToFocus = ({ - target, - formatParts, - segmentRefs: [startSegmentRefs, endSegmentRefs], -}: GetRangeSegmentToFocusProps): HTMLElement | undefined | null => { - if ( - isUndefined(target) || - isUndefined(formatParts) || - isUndefined(startSegmentRefs) || - isUndefined(endSegmentRefs) - ) { - return; - } - - const startSegmentsArray = Object.values(startSegmentRefs).map( - r => r.current, - ); - const endSegmentsArray = Object.values(endSegmentRefs).map(r => r.current); - const segmentRefsArray = [...startSegmentsArray, ...endSegmentsArray]; - - const isTargetASegment = segmentRefsArray.includes( - target as HTMLInputElement, - ); - - if (!isTargetASegment) { - const formatSegments = formatParts.filter(part => part.type !== 'literal'); - const allSegmentsFilled = segmentRefsArray.every(el => el?.value); - - if (allSegmentsFilled) { - const lastSegmentPart = last(formatSegments) as Intl.DateTimeFormatPart; - const keyOfLastSegment = lastSegmentPart.type as DateSegment; - const lastSegmentRef = endSegmentRefs[keyOfLastSegment]; - return lastSegmentRef.current; - } else { - const allStartSegmentsFilled = startSegmentsArray.every(el => el?.value); - - if (allStartSegmentsFilled) { - return getFirstEmptySegment({ - formatParts, - segmentRefs: endSegmentRefs, - }); - } else { - return getFirstEmptySegment({ - formatParts, - segmentRefs: startSegmentRefs, - }); - } - } - } -}; diff --git a/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts b/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts deleted file mode 100644 index 13f1bc40f0..0000000000 --- a/packages/date-picker/src/DateRangePicker/utils/getRelativeRangeSegment/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; -import isUndefined from 'lodash/isUndefined'; -import last from 'lodash/last'; -import { DateSegment } from 'src/hooks/useDateSegments'; - -import { DatePickerContextProps } from '../../../DatePickerContext'; -import { SegmentRefs } from '../../../hooks/useSegmentRefs'; - -type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; -interface GetRelativeSegmentContext { - target: HTMLInputElement | React.RefObject; - formatParts: DatePickerContextProps['formatParts']; - rangeSegmentRefs: Array; -} - -export const getRelativeRangeSegment = ( - direction: RelativeDirection, - { target, formatParts, rangeSegmentRefs }: GetRelativeSegmentContext, -): React.RefObject | undefined => { - if ( - isUndefined(direction) || - isUndefined(target) || - isUndefined(formatParts) || - isUndefined(rangeSegmentRefs) - ) { - return; - } - - // only the relevant segments, not separators - const formatSegments = formatParts.filter(part => part.type !== 'literal'); - - const orderedSegmentRefs = rangeSegmentRefs.flatMap(segmentRefs => - formatSegments.map(({ type }) => segmentRefs[type as DateSegment]), - ); - - const currentSegmentIndex: number | undefined = orderedSegmentRefs.findIndex( - ref => ref.current === target, - ); - - switch (direction) { - case 'first': { - const firstSegmentRef = orderedSegmentRefs[0]; - return firstSegmentRef; - } - - case 'last': { - const lastSegmentRef = last(orderedSegmentRefs); - - if (lastSegmentRef) { - return lastSegmentRef; - } - - break; - } - - case 'next': { - if (!isUndefined(currentSegmentIndex)) { - const nextSegmentIndex = Math.min( - currentSegmentIndex + 1, - orderedSegmentRefs.length - 1, - ); - - const nextSegmentRef = orderedSegmentRefs[nextSegmentIndex]; - - return nextSegmentRef; - } - - break; - } - - case 'prev': { - if (!isUndefined(currentSegmentIndex)) { - const prevSegmentIndex = Math.max(currentSegmentIndex - 1, 0); - - const prevSegmentRef = orderedSegmentRefs[prevSegmentIndex]; - - return prevSegmentRef; - } - - break; - } - - default: - break; - } -}; diff --git a/packages/date-picker/src/constants.ts b/packages/date-picker/src/constants.ts deleted file mode 100644 index 5b6103ea3c..0000000000 --- a/packages/date-picker/src/constants.ts +++ /dev/null @@ -1,54 +0,0 @@ -import range from 'lodash/range'; - -import { DropdownWidthBasis } from '@leafygreen-ui/select'; - -import { getMonthName } from './utils/getMonthName'; - -// Compute the long & short form of each month index -export const Months: Array<{ - long: string; - short: string; -}> = range(12).map(m => getMonthName(m)); - -/** Maps the month name to its index */ -export enum Month { - January, - February, - March, - April, - May, - June, - July, - August, - September, - October, - November, - December, -} - -export const daysPerWeek = 7 as const; - -export const MIN_DATE = new Date(Date.UTC(1970, Month.January, 1)); -export const MAX_DATE = new Date(Date.UTC(2038, Month.January, 19)); - -export const DaysOfWeek = [ - { long: 'Sunday', short: 'su' }, - { long: 'Monday', short: 'mo' }, - { long: 'Tuesday', short: 'tu' }, - { long: 'Wednesday', short: 'we' }, - { long: 'Thursday', short: 'th' }, - { long: 'Friday', short: 'fr' }, - { long: 'Saturday', short: 'sa' }, -] as const; - -export type DaysOfWeek = (typeof DaysOfWeek)[number]; - -/** Default props for the month & year select menus */ -export const selectElementProps = { - size: 'xsmall', - allowDeselect: false, - dropdownWidthBasis: DropdownWidthBasis.Option, - // using no portal so the select menus are included in the backdrop "foreground" - // there is currently no way to pass a ref into the Select portal to use in backdrop "foreground" - usePortal: false, -} as const; diff --git a/packages/date-picker/src/hooks/useFormat/index.ts b/packages/date-picker/src/hooks/useFormat/index.ts deleted file mode 100644 index 29860ed9a2..0000000000 --- a/packages/date-picker/src/hooks/useFormat/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; - -import { consoleOnce } from '@leafygreen-ui/lib'; - -import { isValidLocale } from '../../utils'; - -const now = new Date(); -const ISO = 'iso8601'; -const IsoFormatParts: Array = [ - { type: 'year', value: '' }, - { type: 'literal', value: '-' }, - { type: 'month', value: '' }, - { type: 'literal', value: '-' }, - { type: 'day', value: '' }, -]; - -/** - * Hook that returns an Intl.DateTimeFormat object for the provided format string - * @deprecated - */ -export const useFormatter = (format: string) => { - const [formatter, setFormatter] = useState( - isValidLocale(format) ? Intl.DateTimeFormat(format) : undefined, - ); - - useEffect(() => { - if (isValidLocale(format)) { - setFormatter(Intl.DateTimeFormat(format)); - } else { - if (format !== ISO) { - consoleOnce.error('Invalid dateFormat', format); - } - } - }, [format]); - - return formatter; -}; - -/** - * @deprecated - */ -export const useFormatParts = ( - format: string, -): Array | undefined => { - const formatter = useFormatter(format); - - const formatParts = useMemo(() => formatter?.formatToParts(now), [formatter]); - - if (format === ISO) { - return IsoFormatParts; - } - - return formatParts; -}; diff --git a/packages/date-picker/src/index.ts b/packages/date-picker/src/index.ts index fda2104d4f..d63a67e382 100644 --- a/packages/date-picker/src/index.ts +++ b/packages/date-picker/src/index.ts @@ -1,3 +1,6 @@ export { DatePicker, type DatePickerProps } from './DatePicker'; -export { DateRangePicker, type DateRangePickerProps } from './DateRangePicker'; -export { type DateRangeType, type DateType } from './types'; +export * from './shared/components'; +export * from './shared/constants'; +export * from './shared/hooks'; +export * from './shared/types'; +export * from './shared/utils'; diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.spec.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/CalendarCell.spec.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/CalendarCell.stories.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.styles.ts b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/CalendarCell.styles.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/CalendarCell.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx diff --git a/packages/date-picker/src/Calendar/CalendarCell/CalendarCell.types.ts b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.types.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/CalendarCell.types.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.types.ts diff --git a/packages/date-picker/src/Calendar/CalendarCell/index.ts b/packages/date-picker/src/shared/components/Calendar/CalendarCell/index.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarCell/index.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarCell/index.ts diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.spec.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.spec.tsx similarity index 100% rename from packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.spec.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.spec.tsx diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx similarity index 94% rename from packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.stories.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index fecdace289..e00c7f61bc 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -5,14 +5,14 @@ import { StoryFn } from '@storybook/react'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; -import { Month } from '../../constants'; +import { Month } from '../../../constants'; +import { isTodayTZ, newUTC } from '../../../utils'; +import { Locales, TimeZones } from '../../../utils/testutils'; import { DatePickerContextProps, DatePickerProvider, useDatePickerContext, } from '../../DatePickerContext'; -import { Locales, TimeZones } from '../../testUtils'; -import { isTodayTZ, newUTC } from '../../utils'; import { CalendarCell } from '../CalendarCell/CalendarCell'; import { CalendarGrid } from './CalendarGrid'; diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.styles.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.styles.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.styles.ts diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx similarity index 95% rename from packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx index 5b8eea5c25..4f54b4f513 100644 --- a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx @@ -5,9 +5,9 @@ import { getWeekStartByLocale } from 'weekstart'; import { cx } from '@leafygreen-ui/emotion'; import { Disclaimer } from '@leafygreen-ui/typography'; -import { DaysOfWeek, daysPerWeek } from '../../constants'; +import { DaysOfWeek, daysPerWeek } from '../../../constants'; +import { getWeeksArray } from '../../../utils'; import { useDatePickerContext } from '../../DatePickerContext'; -import { getWeeksArray } from '../../utils'; import { calendarGridStyles, diff --git a/packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.types.ts b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.types.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarGrid/CalendarGrid.types.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.types.ts diff --git a/packages/date-picker/src/Calendar/CalendarGrid/index.ts b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/index.ts similarity index 100% rename from packages/date-picker/src/Calendar/CalendarGrid/index.ts rename to packages/date-picker/src/shared/components/Calendar/CalendarGrid/index.ts diff --git a/packages/date-picker/src/shared/components/Calendar/index.ts b/packages/date-picker/src/shared/components/Calendar/index.ts new file mode 100644 index 0000000000..0150c00993 --- /dev/null +++ b/packages/date-picker/src/shared/components/Calendar/index.ts @@ -0,0 +1,7 @@ +export { + CalendarCell, + type CalendarCellProps, + CalendarCellRangeState, + CalendarCellState, +} from './CalendarCell'; +export { CalendarGrid, type CalendarGridProps } from './CalendarGrid'; diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts b/packages/date-picker/src/shared/components/DateInput/CalendarButton/CalendarButton.styles.ts similarity index 100% rename from packages/date-picker/src/DateInput/CalendarButton/CalendarButton.styles.ts rename to packages/date-picker/src/shared/components/DateInput/CalendarButton/CalendarButton.styles.ts diff --git a/packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx b/packages/date-picker/src/shared/components/DateInput/CalendarButton/CalendarButton.tsx similarity index 100% rename from packages/date-picker/src/DateInput/CalendarButton/CalendarButton.tsx rename to packages/date-picker/src/shared/components/DateInput/CalendarButton/CalendarButton.tsx diff --git a/packages/date-picker/src/DateInput/CalendarButton/index.ts b/packages/date-picker/src/shared/components/DateInput/CalendarButton/index.ts similarity index 100% rename from packages/date-picker/src/DateInput/CalendarButton/index.ts rename to packages/date-picker/src/shared/components/DateInput/CalendarButton/index.ts diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx similarity index 100% rename from packages/date-picker/src/DateInput/DateFormField/DateFormField.spec.tsx rename to packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx similarity index 98% rename from packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx rename to packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx index 0125736668..d7540c2d24 100644 --- a/packages/date-picker/src/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx @@ -6,11 +6,11 @@ import { css } from '@leafygreen-ui/emotion'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; +import { DatePickerState } from '../../../types'; import { DatePickerContextProps, DatePickerProvider, } from '../../DatePickerContext'; -import { DatePickerState } from '../../types'; import { DateFormField } from './DateFormField'; diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.styles.ts b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateFormField/DateFormField.styles.ts rename to packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx similarity index 100% rename from packages/date-picker/src/DateInput/DateFormField/DateFormField.tsx rename to packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx diff --git a/packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateFormField/DateFormField.types.ts rename to packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts diff --git a/packages/date-picker/src/DateInput/DateFormField/index.ts b/packages/date-picker/src/shared/components/DateInput/DateFormField/index.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateFormField/index.ts rename to packages/date-picker/src/shared/components/DateInput/DateFormField/index.ts diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx similarity index 96% rename from packages/date-picker/src/DateInput/DateInputBox/DateInputBox.spec.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index d9203441d4..90e5ac653d 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Month } from '../../constants'; +import { Month } from '../../../constants'; +import { SegmentRefs } from '../../../hooks'; +import { newUTC } from '../../../utils'; +import { eventContainingTargetValue } from '../../../utils/testutils'; import { DatePickerProvider, DatePickerProviderProps, + defaultDatePickerContext, } from '../../DatePickerContext'; -import { defaultDatePickerContext } from '../../DatePickerContext/DatePickerContext.utils'; -import { SegmentRefs } from '../../hooks/useSegmentRefs'; -import { eventContainingTargetValue } from '../../testUtils'; -import { newUTC } from '../../utils'; import { DateInputBox, type DateInputBoxProps } from '.'; @@ -194,6 +194,7 @@ describe('packages/date-picker/shared/date-input-box', () => { }); // TODO: + // eslint-disable-next-line jest/no-disabled-tests describe.skip('Auto-focus', () => { test('typing a complete segment value focuses the next segment', () => { const { yearInput, monthInput } = renderDateInputBox( diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx similarity index 96% rename from packages/date-picker/src/DateInput/DateInputBox/DateInputBox.stories.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index bdf4c923a5..6349f2350c 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -6,12 +6,12 @@ import { isValid } from 'date-fns'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType, StoryType } from '@leafygreen-ui/lib'; -import { Month } from '../../constants'; +import { Month } from '../../../constants'; +import { newUTC } from '../../../utils'; import { DatePickerContextProps, DatePickerProvider, } from '../../DatePickerContext'; -import { newUTC } from '../../utils'; import { DateInputBox } from './DateInputBox'; diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.styles.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.styles.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateInputBox/DateInputBox.styles.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.styles.ts diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx similarity index 97% rename from packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index b54c76725f..b669254953 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -11,18 +11,18 @@ import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { createSyntheticEvent } from '@leafygreen-ui/lib'; -import { useDatePickerContext } from '../../DatePickerContext'; import { DateSegment, DateSegmentsState, isDateSegment, -} from '../../hooks/useDateSegments/DateSegments.types'; -import { useDateSegments } from '../../hooks/useDateSegments/useDateSegments'; + useDateSegments, +} from '../../../hooks'; import { doesSomeSegmentExist, getValueFormatter, newDateFromSegments, -} from '../../utils'; +} from '../../../utils'; +import { useDatePickerContext } from '../../DatePickerContext'; import { DateInputSegment } from '../DateInputSegment'; import { diff --git a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts similarity index 86% rename from packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts index 4ead06f630..e6d02353d8 100644 --- a/packages/date-picker/src/DateInput/DateInputBox/DateInputBox.types.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts @@ -2,8 +2,8 @@ import { ChangeEventHandler } from 'react'; import { HTMLElementProps } from '@leafygreen-ui/lib'; -import { SegmentRefs } from '../../hooks/useSegmentRefs'; -import { DateType } from '../../types'; +import { SegmentRefs } from '../../../hooks'; +import { DateType } from '../../../types'; export interface DateInputBoxProps extends Omit, 'onChange'> { diff --git a/packages/date-picker/src/DateInput/DateInputBox/index.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateInputBox/index.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx similarity index 98% rename from packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx index 11e270392a..c237ea1fc8 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { eventContainingTargetValue } from '../../testUtils'; +import { eventContainingTargetValue } from '../../../utils/testutils'; import { DateInputSegment, type DateInputSegmentProps } from '.'; diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx similarity index 100% rename from packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.stories.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.styles.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.styles.ts similarity index 93% rename from packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.styles.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.styles.ts index 64feeb79d8..0709d7be20 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.styles.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.styles.ts @@ -8,9 +8,8 @@ import { typeScales, } from '@leafygreen-ui/tokens'; -import { DateSegment } from '../../hooks/useDateSegments/DateSegments.types'; - -import { characterWidth, charsPerSegment } from './constants'; +import { characterWidth, charsPerSegment } from '../../../constants'; +import { DateSegment } from '../../../hooks'; export const baseStyles = css` font-family: ${fontFamilies.default}; diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx similarity index 96% rename from packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index 26e0f5e3f9..7b2505cddb 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -6,9 +6,9 @@ import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { Size } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; +import { defaultMax, defaultMin, defaultPlaceholder } from '../../../constants'; import { useDatePickerContext } from '../../DatePickerContext'; -import { defaultMax, defaultMin, defaultPlaceholder } from './constants'; import { baseStyles, fontSizeStyles, diff --git a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.types.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.types.ts similarity index 85% rename from packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.types.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.types.ts index 6fe0a09502..6c9ecdd40c 100644 --- a/packages/date-picker/src/DateInput/DateInputSegment/DateInputSegment.types.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.types.ts @@ -1,9 +1,6 @@ import { DarkModeProps, HTMLElementProps } from '@leafygreen-ui/lib'; -import { - DateSegment, - DateSegmentValue, -} from '../../hooks/useDateSegments/DateSegments.types'; +import { DateSegment, DateSegmentValue } from '../../../hooks'; export interface DateInputSegmentProps extends DarkModeProps, diff --git a/packages/date-picker/src/DateInput/DateInputSegment/index.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/index.ts similarity index 100% rename from packages/date-picker/src/DateInput/DateInputSegment/index.ts rename to packages/date-picker/src/shared/components/DateInput/DateInputSegment/index.ts diff --git a/packages/date-picker/src/shared/components/DateInput/index.ts b/packages/date-picker/src/shared/components/DateInput/index.ts new file mode 100644 index 0000000000..74de023c3c --- /dev/null +++ b/packages/date-picker/src/shared/components/DateInput/index.ts @@ -0,0 +1,4 @@ +export { CalendarButton } from './CalendarButton'; +export { DateFormField } from './DateFormField'; +export { DateInputBox, type DateInputBoxProps } from './DateInputBox'; +export { DateInputSegment } from './DateInputSegment'; diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.spec.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx similarity index 100% rename from packages/date-picker/src/DatePickerContext/DatePickerContext.spec.tsx rename to packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx similarity index 100% rename from packages/date-picker/src/DatePickerContext/DatePickerContext.tsx rename to packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.types.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts similarity index 95% rename from packages/date-picker/src/DatePickerContext/DatePickerContext.types.ts rename to packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts index 1c8f64f3ab..2e56146a7b 100644 --- a/packages/date-picker/src/DatePickerContext/DatePickerContext.types.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts @@ -1,4 +1,4 @@ -import { BaseDatePickerProps } from '../types'; +import { BaseDatePickerProps } from '../../types'; /** The props expected to pass int the provider */ export interface DatePickerProviderProps extends BaseDatePickerProps {} diff --git a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts similarity index 92% rename from packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts rename to packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts index fd03e8264f..fb5f4b0cfb 100644 --- a/packages/date-picker/src/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts @@ -4,9 +4,9 @@ import defaultTo from 'lodash/defaultTo'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; -import { MAX_DATE, MIN_DATE } from '../constants'; -import { BaseDatePickerProps, DatePickerState } from '../types'; -import { getFormatParts, toDate } from '../utils'; +import { MAX_DATE, MIN_DATE } from '../../constants'; +import { BaseDatePickerProps, DatePickerState } from '../../types'; +import { getFormatParts, toDate } from '../../utils'; import { DatePickerContextProps, diff --git a/packages/date-picker/src/DatePickerContext/index.ts b/packages/date-picker/src/shared/components/DatePickerContext/index.ts similarity index 73% rename from packages/date-picker/src/DatePickerContext/index.ts rename to packages/date-picker/src/shared/components/DatePickerContext/index.ts index 5bb1570bc0..08fe7b4bed 100644 --- a/packages/date-picker/src/DatePickerContext/index.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/index.ts @@ -4,10 +4,11 @@ export { useDatePickerContext, } from './DatePickerContext'; export { - DatePickerContextProps, - DatePickerProviderProps, + type DatePickerContextProps, + type DatePickerProviderProps, } from './DatePickerContext.types'; export { + type ContextPropKeys, contextPropNames, defaultDatePickerContext, } from './DatePickerContext.utils'; diff --git a/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.spec.tsx b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.spec.tsx similarity index 100% rename from packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.spec.tsx rename to packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.spec.tsx diff --git a/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.styles.ts b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.styles.ts similarity index 100% rename from packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.styles.ts rename to packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.styles.ts diff --git a/packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx similarity index 100% rename from packages/date-picker/src/Calendar/MenuWrapper/MenuWrapper.tsx rename to packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx diff --git a/packages/date-picker/src/Calendar/MenuWrapper/index.ts b/packages/date-picker/src/shared/components/MenuWrapper/index.ts similarity index 100% rename from packages/date-picker/src/Calendar/MenuWrapper/index.ts rename to packages/date-picker/src/shared/components/MenuWrapper/index.ts diff --git a/packages/date-picker/src/shared/components/index.ts b/packages/date-picker/src/shared/components/index.ts new file mode 100644 index 0000000000..009742c5d1 --- /dev/null +++ b/packages/date-picker/src/shared/components/index.ts @@ -0,0 +1,18 @@ +export { + CalendarCell, + type CalendarCellProps, + CalendarCellState, + CalendarGrid, + type CalendarGridProps, +} from './Calendar'; +export { DateInputBox, type DateInputBoxProps } from './DateInput'; +export { + contextPropNames, + DatePickerContext, + type DatePickerContextProps, + DatePickerProvider, + type DatePickerProviderProps, + defaultDatePickerContext, + useDatePickerContext, +} from './DatePickerContext'; +export { MenuWrapper } from './MenuWrapper'; diff --git a/packages/date-picker/src/shared/constants.ts b/packages/date-picker/src/shared/constants.ts new file mode 100644 index 0000000000..8f991397e4 --- /dev/null +++ b/packages/date-picker/src/shared/constants.ts @@ -0,0 +1,122 @@ +import padStart from 'lodash/padStart'; +import range from 'lodash/range'; + +import { DropdownWidthBasis } from '@leafygreen-ui/select'; + +/** Days in a week */ +export const daysPerWeek = 7 as const; + +/** Enumerates month names to the 0-indexed value */ +export enum Month { + January, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, +} + +/** The default earliest selectable date */ +export const MIN_DATE = new Date(Date.UTC(1970, Month.January, 1)); + +/** The default latest selectable date */ +export const MAX_DATE = new Date(Date.UTC(2038, Month.January, 19)); + +/** + * Long & short form of each month index + */ +export const Months: Array<{ + long: string; + short: string; +}> = range(12).map((monthIndex: number) => { + const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; + const month = new Date(str); + return { + long: month.toLocaleString('default', { month: 'long' }), + short: month.toLocaleString('default', { month: 'short' }), + }; +}); + +/** Long & short form for each Day of the week */ +export const DaysOfWeek = [ + { long: 'Sunday', short: 'su' }, + { long: 'Monday', short: 'mo' }, + { long: 'Tuesday', short: 'tu' }, + { long: 'Wednesday', short: 'we' }, + { long: 'Thursday', short: 'th' }, + { long: 'Friday', short: 'fr' }, + { long: 'Saturday', short: 'sa' }, +] as const; +export type DaysOfWeek = (typeof DaysOfWeek)[number]; + +/** + * The minimum number for each segment + */ +export const defaultMin = { + day: 1, + month: 1, + year: MIN_DATE.getUTCFullYear(), +} as const; + +/** + * The maximum number for each segment + */ +export const defaultMax = { + day: 31, + month: 12, + year: MAX_DATE.getUTCFullYear(), +} as const; + +/** + * The shorthand for each char + */ +export const placeholderChar = { + day: 'D', + month: 'M', + year: 'Y', +}; + +/** + * The number of characters per input segment + */ +export const charsPerSegment = { + day: 2, + month: 2, + year: 4, +}; + +const _makePlaceholder = (n: number, s: string) => + new Array(n).fill(s).join('\u200B'); + +/** + * The default placeholders for each segment + */ +export const defaultPlaceholder = { + day: _makePlaceholder(charsPerSegment.day, placeholderChar.day), + month: _makePlaceholder(charsPerSegment.month, placeholderChar.month), + year: _makePlaceholder(charsPerSegment.year, placeholderChar.year), +} as const; + +/** The percentage of 1ch these specific characters take up */ +export const characterWidth = { + // // Standard font + D: 46 / 40, + M: 55 / 40, + Y: 50 / 40, +} as const; + +/** Default props for the month & year select menus */ +export const selectElementProps = { + size: 'xsmall', + allowDeselect: false, + dropdownWidthBasis: DropdownWidthBasis.Option, + // using no portal so the select menus are included in the backdrop "foreground" + // there is currently no way to pass a ref into the Select portal to use in backdrop "foreground" + usePortal: false, +} as const; diff --git a/packages/date-picker/src/shared/hooks/index.ts b/packages/date-picker/src/shared/hooks/index.ts new file mode 100644 index 0000000000..0b3c3e87f0 --- /dev/null +++ b/packages/date-picker/src/shared/hooks/index.ts @@ -0,0 +1,9 @@ +export { useControlledValue } from './useControlledValue'; +export { + DateSegment, + type DateSegmentsState, + type DateSegmentValue, + isDateSegment, + useDateSegments, +} from './useDateSegments'; +export { type SegmentRefs, useSegmentRefs } from './useSegmentRefs'; diff --git a/packages/date-picker/src/hooks/useControlledValue/index.ts b/packages/date-picker/src/shared/hooks/useControlledValue/index.ts similarity index 100% rename from packages/date-picker/src/hooks/useControlledValue/index.ts rename to packages/date-picker/src/shared/hooks/useControlledValue/index.ts diff --git a/packages/date-picker/src/hooks/useControlledValue/useControlledValue.spec.tsx b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx similarity index 100% rename from packages/date-picker/src/hooks/useControlledValue/useControlledValue.spec.tsx rename to packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx diff --git a/packages/date-picker/src/hooks/useControlledValue/useControlledValue.ts b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts similarity index 100% rename from packages/date-picker/src/hooks/useControlledValue/useControlledValue.ts rename to packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts diff --git a/packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts b/packages/date-picker/src/shared/hooks/useDateSegments/DateSegments.types.ts similarity index 100% rename from packages/date-picker/src/hooks/useDateSegments/DateSegments.types.ts rename to packages/date-picker/src/shared/hooks/useDateSegments/DateSegments.types.ts diff --git a/packages/date-picker/src/hooks/useDateSegments/index.ts b/packages/date-picker/src/shared/hooks/useDateSegments/index.ts similarity index 71% rename from packages/date-picker/src/hooks/useDateSegments/index.ts rename to packages/date-picker/src/shared/hooks/useDateSegments/index.ts index 516d0812d4..8caa6ce048 100644 --- a/packages/date-picker/src/hooks/useDateSegments/index.ts +++ b/packages/date-picker/src/shared/hooks/useDateSegments/index.ts @@ -1,7 +1,7 @@ export { DateSegment, - DateSegmentsState, - DateSegmentValue, + type DateSegmentsState, + type DateSegmentValue, isDateSegment, } from './DateSegments.types'; export { useDateSegments } from './useDateSegments'; diff --git a/packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts similarity index 100% rename from packages/date-picker/src/hooks/useDateSegments/useDateSegments.ts rename to packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts diff --git a/packages/date-picker/src/hooks/useSegmentRefs/index.ts b/packages/date-picker/src/shared/hooks/useSegmentRefs/index.ts similarity index 100% rename from packages/date-picker/src/hooks/useSegmentRefs/index.ts rename to packages/date-picker/src/shared/hooks/useSegmentRefs/index.ts diff --git a/packages/date-picker/src/hooks/useSegmentRefs/segmentRefs.types.ts b/packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts similarity index 100% rename from packages/date-picker/src/hooks/useSegmentRefs/segmentRefs.types.ts rename to packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts diff --git a/packages/date-picker/src/hooks/useSegmentRefs/useSegmentRefs.ts b/packages/date-picker/src/shared/hooks/useSegmentRefs/useSegmentRefs.ts similarity index 100% rename from packages/date-picker/src/hooks/useSegmentRefs/useSegmentRefs.ts rename to packages/date-picker/src/shared/hooks/useSegmentRefs/useSegmentRefs.ts diff --git a/packages/date-picker/src/shared/index.ts b/packages/date-picker/src/shared/index.ts new file mode 100644 index 0000000000..e7f8eabcbf --- /dev/null +++ b/packages/date-picker/src/shared/index.ts @@ -0,0 +1,5 @@ +export * from './components'; +export * from './constants'; +export * from './hooks'; +export * from './types'; +export * from './utils'; diff --git a/packages/date-picker/src/types.ts b/packages/date-picker/src/shared/types.ts similarity index 100% rename from packages/date-picker/src/types.ts rename to packages/date-picker/src/shared/types.ts diff --git a/packages/date-picker/src/utils/addMonthsUTC/index.ts b/packages/date-picker/src/shared/utils/addMonthsUTC/index.ts similarity index 100% rename from packages/date-picker/src/utils/addMonthsUTC/index.ts rename to packages/date-picker/src/shared/utils/addMonthsUTC/index.ts diff --git a/packages/date-picker/src/utils/cloneReverse/cloneReverse.spec.ts b/packages/date-picker/src/shared/utils/cloneReverse/cloneReverse.spec.ts similarity index 100% rename from packages/date-picker/src/utils/cloneReverse/cloneReverse.spec.ts rename to packages/date-picker/src/shared/utils/cloneReverse/cloneReverse.spec.ts diff --git a/packages/date-picker/src/utils/cloneReverse/index.ts b/packages/date-picker/src/shared/utils/cloneReverse/index.ts similarity index 100% rename from packages/date-picker/src/utils/cloneReverse/index.ts rename to packages/date-picker/src/shared/utils/cloneReverse/index.ts diff --git a/packages/date-picker/src/utils/doesSomeSegmentExist/index.ts b/packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts similarity index 100% rename from packages/date-picker/src/utils/doesSomeSegmentExist/index.ts rename to packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts diff --git a/packages/date-picker/src/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts b/packages/date-picker/src/shared/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts rename to packages/date-picker/src/shared/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts diff --git a/packages/date-picker/src/utils/getDaysInUTCMonth/index.ts b/packages/date-picker/src/shared/utils/getDaysInUTCMonth/index.ts similarity index 100% rename from packages/date-picker/src/utils/getDaysInUTCMonth/index.ts rename to packages/date-picker/src/shared/utils/getDaysInUTCMonth/index.ts diff --git a/packages/date-picker/src/utils/getFirstEmptySegment/index.ts b/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts similarity index 80% rename from packages/date-picker/src/utils/getFirstEmptySegment/index.ts rename to packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts index 1f95d6a2ac..1ca88b7eb9 100644 --- a/packages/date-picker/src/utils/getFirstEmptySegment/index.ts +++ b/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts @@ -1,6 +1,5 @@ -import { DatePickerContextProps } from '../../DatePickerContext'; -import { DateSegment } from '../../hooks/useDateSegments'; -import { SegmentRefs } from '../../hooks/useSegmentRefs'; +import { DatePickerContextProps } from '../../components/DatePickerContext'; +import { DateSegment, SegmentRefs } from '../../hooks'; /** * diff --git a/packages/date-picker/src/utils/getFirstOfMonth/getFirstOfMonth.spec.ts b/packages/date-picker/src/shared/utils/getFirstOfMonth/getFirstOfMonth.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getFirstOfMonth/getFirstOfMonth.spec.ts rename to packages/date-picker/src/shared/utils/getFirstOfMonth/getFirstOfMonth.spec.ts diff --git a/packages/date-picker/src/utils/getFirstOfMonth/index.ts b/packages/date-picker/src/shared/utils/getFirstOfMonth/index.ts similarity index 100% rename from packages/date-picker/src/utils/getFirstOfMonth/index.ts rename to packages/date-picker/src/shared/utils/getFirstOfMonth/index.ts diff --git a/packages/date-picker/src/utils/getFormatParts/index.ts b/packages/date-picker/src/shared/utils/getFormatParts/index.ts similarity index 100% rename from packages/date-picker/src/utils/getFormatParts/index.ts rename to packages/date-picker/src/shared/utils/getFormatParts/index.ts diff --git a/packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts b/packages/date-picker/src/shared/utils/getFullMonthLabel/getFullMonthLabel.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getFullMonthLabel/getFullMonthLabel.spec.ts rename to packages/date-picker/src/shared/utils/getFullMonthLabel/getFullMonthLabel.spec.ts diff --git a/packages/date-picker/src/utils/getFullMonthLabel/index.ts b/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts similarity index 100% rename from packages/date-picker/src/utils/getFullMonthLabel/index.ts rename to packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts diff --git a/packages/date-picker/src/utils/getLastOfMonth/getLastOfMonth.spec.ts b/packages/date-picker/src/shared/utils/getLastOfMonth/getLastOfMonth.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getLastOfMonth/getLastOfMonth.spec.ts rename to packages/date-picker/src/shared/utils/getLastOfMonth/getLastOfMonth.spec.ts diff --git a/packages/date-picker/src/utils/getLastOfMonth/index.ts b/packages/date-picker/src/shared/utils/getLastOfMonth/index.ts similarity index 100% rename from packages/date-picker/src/utils/getLastOfMonth/index.ts rename to packages/date-picker/src/shared/utils/getLastOfMonth/index.ts diff --git a/packages/date-picker/src/utils/getMonthName/getMonthName.spec.ts b/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getMonthName/getMonthName.spec.ts rename to packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts diff --git a/packages/date-picker/src/utils/getMonthName/index.ts b/packages/date-picker/src/shared/utils/getMonthName/index.ts similarity index 100% rename from packages/date-picker/src/utils/getMonthName/index.ts rename to packages/date-picker/src/shared/utils/getMonthName/index.ts diff --git a/packages/date-picker/src/utils/getRemainingParts/getRemainingParts.spec.ts b/packages/date-picker/src/shared/utils/getRemainingParts/getRemainingParts.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getRemainingParts/getRemainingParts.spec.ts rename to packages/date-picker/src/shared/utils/getRemainingParts/getRemainingParts.spec.ts diff --git a/packages/date-picker/src/utils/getRemainingParts/index.ts b/packages/date-picker/src/shared/utils/getRemainingParts/index.ts similarity index 100% rename from packages/date-picker/src/utils/getRemainingParts/index.ts rename to packages/date-picker/src/shared/utils/getRemainingParts/index.ts diff --git a/packages/date-picker/src/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts rename to packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts diff --git a/packages/date-picker/src/utils/getSegmentsFromDate/index.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts similarity index 92% rename from packages/date-picker/src/utils/getSegmentsFromDate/index.ts rename to packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts index 7e1a19283b..fc3d1a65f5 100644 --- a/packages/date-picker/src/utils/getSegmentsFromDate/index.ts +++ b/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts @@ -1,4 +1,4 @@ -import { DateSegmentsState } from '../../hooks/useDateSegments'; +import { DateSegmentsState } from '../../hooks'; import { getValueFormatter } from '../getValueFormatter'; /** Returns a single object with day, month & year segments */ diff --git a/packages/date-picker/src/utils/getUTCDateString/getUTCDateString.spec.ts b/packages/date-picker/src/shared/utils/getUTCDateString/getUTCDateString.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getUTCDateString/getUTCDateString.spec.ts rename to packages/date-picker/src/shared/utils/getUTCDateString/getUTCDateString.spec.ts diff --git a/packages/date-picker/src/utils/getUTCDateString/index.ts b/packages/date-picker/src/shared/utils/getUTCDateString/index.ts similarity index 100% rename from packages/date-picker/src/utils/getUTCDateString/index.ts rename to packages/date-picker/src/shared/utils/getUTCDateString/index.ts diff --git a/packages/date-picker/src/utils/getValueFormatter/index.ts b/packages/date-picker/src/shared/utils/getValueFormatter/index.ts similarity index 77% rename from packages/date-picker/src/utils/getValueFormatter/index.ts rename to packages/date-picker/src/shared/utils/getValueFormatter/index.ts index d2ee3a7432..f0facacedb 100644 --- a/packages/date-picker/src/utils/getValueFormatter/index.ts +++ b/packages/date-picker/src/shared/utils/getValueFormatter/index.ts @@ -1,9 +1,12 @@ import padStart from 'lodash/padStart'; -import { charsPerSegment } from '../../DateInput/DateInputSegment/constants'; -import { DateSegment } from '../../hooks/useDateSegments/DateSegments.types'; +import { charsPerSegment } from '../../constants'; +import { DateSegment } from '../../hooks'; import { isZeroLike } from '../isZeroLike'; +/** + * @returns a value formatter function for the provided date segment + */ export const getValueFormatter = (segment: DateSegment) => (val: string | number | undefined) => { // If the value is any form of zero, we set it to an empty string diff --git a/packages/date-picker/src/utils/getValueFormatter/valueFormatter.spec.ts b/packages/date-picker/src/shared/utils/getValueFormatter/valueFormatter.spec.ts similarity index 94% rename from packages/date-picker/src/utils/getValueFormatter/valueFormatter.spec.ts rename to packages/date-picker/src/shared/utils/getValueFormatter/valueFormatter.spec.ts index 461e531f34..0dd09ce2a2 100644 --- a/packages/date-picker/src/utils/getValueFormatter/valueFormatter.spec.ts +++ b/packages/date-picker/src/shared/utils/getValueFormatter/valueFormatter.spec.ts @@ -1,4 +1,4 @@ -import { DateSegment } from '../../hooks/useDateSegments/DateSegments.types'; +import { DateSegment } from '../../shared/hooks/useDateSegments/DateSegments.types'; import { getValueFormatter } from '.'; diff --git a/packages/date-picker/src/utils/getWeeksArray/getWeeksArray.spec.ts b/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts similarity index 100% rename from packages/date-picker/src/utils/getWeeksArray/getWeeksArray.spec.ts rename to packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts diff --git a/packages/date-picker/src/utils/getWeeksArray/index.ts b/packages/date-picker/src/shared/utils/getWeeksArray/index.ts similarity index 100% rename from packages/date-picker/src/utils/getWeeksArray/index.ts rename to packages/date-picker/src/shared/utils/getWeeksArray/index.ts diff --git a/packages/date-picker/src/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts similarity index 100% rename from packages/date-picker/src/utils/index.ts rename to packages/date-picker/src/shared/utils/index.ts diff --git a/packages/date-picker/src/utils/isCurrentUTCDay/index.ts b/packages/date-picker/src/shared/utils/isCurrentUTCDay/index.ts similarity index 100% rename from packages/date-picker/src/utils/isCurrentUTCDay/index.ts rename to packages/date-picker/src/shared/utils/isCurrentUTCDay/index.ts diff --git a/packages/date-picker/src/utils/isDefined/index.ts b/packages/date-picker/src/shared/utils/isDefined/index.ts similarity index 100% rename from packages/date-picker/src/utils/isDefined/index.ts rename to packages/date-picker/src/shared/utils/isDefined/index.ts diff --git a/packages/date-picker/src/utils/isElementInputSegment/index.ts b/packages/date-picker/src/shared/utils/isElementInputSegment/index.ts similarity index 86% rename from packages/date-picker/src/utils/isElementInputSegment/index.ts rename to packages/date-picker/src/shared/utils/isElementInputSegment/index.ts index 28ac985fb7..4bacd83464 100644 --- a/packages/date-picker/src/utils/isElementInputSegment/index.ts +++ b/packages/date-picker/src/shared/utils/isElementInputSegment/index.ts @@ -1,4 +1,4 @@ -import { SegmentRefs } from '../../hooks/useSegmentRefs'; +import { SegmentRefs } from '../../hooks'; /** * Returns whether the given element is a segment diff --git a/packages/date-picker/src/utils/isOnOrAfter/index.ts b/packages/date-picker/src/shared/utils/isOnOrAfter/index.ts similarity index 100% rename from packages/date-picker/src/utils/isOnOrAfter/index.ts rename to packages/date-picker/src/shared/utils/isOnOrAfter/index.ts diff --git a/packages/date-picker/src/utils/isOnOrBefore/index.ts b/packages/date-picker/src/shared/utils/isOnOrBefore/index.ts similarity index 100% rename from packages/date-picker/src/utils/isOnOrBefore/index.ts rename to packages/date-picker/src/shared/utils/isOnOrBefore/index.ts diff --git a/packages/date-picker/src/utils/isSameTZDay/index.ts b/packages/date-picker/src/shared/utils/isSameTZDay/index.ts similarity index 100% rename from packages/date-picker/src/utils/isSameTZDay/index.ts rename to packages/date-picker/src/shared/utils/isSameTZDay/index.ts diff --git a/packages/date-picker/src/utils/isSameTZDay/isSameTZDay.spec.ts b/packages/date-picker/src/shared/utils/isSameTZDay/isSameTZDay.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isSameTZDay/isSameTZDay.spec.ts rename to packages/date-picker/src/shared/utils/isSameTZDay/isSameTZDay.spec.ts diff --git a/packages/date-picker/src/utils/isSameUTCDay/index.ts b/packages/date-picker/src/shared/utils/isSameUTCDay/index.ts similarity index 100% rename from packages/date-picker/src/utils/isSameUTCDay/index.ts rename to packages/date-picker/src/shared/utils/isSameUTCDay/index.ts diff --git a/packages/date-picker/src/utils/isSameUTCDay/isSameUTCDay.spec.ts b/packages/date-picker/src/shared/utils/isSameUTCDay/isSameUTCDay.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isSameUTCDay/isSameUTCDay.spec.ts rename to packages/date-picker/src/shared/utils/isSameUTCDay/isSameUTCDay.spec.ts diff --git a/packages/date-picker/src/utils/isSameUTCMonth/index.ts b/packages/date-picker/src/shared/utils/isSameUTCMonth/index.ts similarity index 100% rename from packages/date-picker/src/utils/isSameUTCMonth/index.ts rename to packages/date-picker/src/shared/utils/isSameUTCMonth/index.ts diff --git a/packages/date-picker/src/utils/isSameUTCMonth/isSameUTCMonth.spec.ts b/packages/date-picker/src/shared/utils/isSameUTCMonth/isSameUTCMonth.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isSameUTCMonth/isSameUTCMonth.spec.ts rename to packages/date-picker/src/shared/utils/isSameUTCMonth/isSameUTCMonth.spec.ts diff --git a/packages/date-picker/src/utils/isSameUTCRange/index.ts b/packages/date-picker/src/shared/utils/isSameUTCRange/index.ts similarity index 100% rename from packages/date-picker/src/utils/isSameUTCRange/index.ts rename to packages/date-picker/src/shared/utils/isSameUTCRange/index.ts diff --git a/packages/date-picker/src/utils/isTodayTZ/index.ts b/packages/date-picker/src/shared/utils/isTodayTZ/index.ts similarity index 100% rename from packages/date-picker/src/utils/isTodayTZ/index.ts rename to packages/date-picker/src/shared/utils/isTodayTZ/index.ts diff --git a/packages/date-picker/src/utils/isTodayTZ/isTodayTZ.spec.ts b/packages/date-picker/src/shared/utils/isTodayTZ/isTodayTZ.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isTodayTZ/isTodayTZ.spec.ts rename to packages/date-picker/src/shared/utils/isTodayTZ/isTodayTZ.spec.ts diff --git a/packages/date-picker/src/utils/isValidDate/index.ts b/packages/date-picker/src/shared/utils/isValidDate/index.ts similarity index 100% rename from packages/date-picker/src/utils/isValidDate/index.ts rename to packages/date-picker/src/shared/utils/isValidDate/index.ts diff --git a/packages/date-picker/src/utils/isValidDate/isValidDate.spec.ts b/packages/date-picker/src/shared/utils/isValidDate/isValidDate.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isValidDate/isValidDate.spec.ts rename to packages/date-picker/src/shared/utils/isValidDate/isValidDate.spec.ts diff --git a/packages/date-picker/src/utils/isValidLocale/index.ts b/packages/date-picker/src/shared/utils/isValidLocale/index.ts similarity index 100% rename from packages/date-picker/src/utils/isValidLocale/index.ts rename to packages/date-picker/src/shared/utils/isValidLocale/index.ts diff --git a/packages/date-picker/src/utils/isValidLocale/isValidLocale.spec.ts b/packages/date-picker/src/shared/utils/isValidLocale/isValidLocale.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isValidLocale/isValidLocale.spec.ts rename to packages/date-picker/src/shared/utils/isValidLocale/isValidLocale.spec.ts diff --git a/packages/date-picker/src/utils/isValidSegment/index.ts b/packages/date-picker/src/shared/utils/isValidSegment/index.ts similarity index 84% rename from packages/date-picker/src/utils/isValidSegment/index.ts rename to packages/date-picker/src/shared/utils/isValidSegment/index.ts index 87e6ea5e5e..4892c42716 100644 --- a/packages/date-picker/src/utils/isValidSegment/index.ts +++ b/packages/date-picker/src/shared/utils/isValidSegment/index.ts @@ -1,9 +1,6 @@ import isUndefined from 'lodash/isUndefined'; -import { - DateSegment, - DateSegmentValue, -} from '../../hooks/useDateSegments/DateSegments.types'; +import { DateSegment, DateSegmentValue } from '../../hooks'; /** * Returns whether a given value is a valid segment value diff --git a/packages/date-picker/src/utils/isValidSegment/isValidSegment.spec.ts b/packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts similarity index 100% rename from packages/date-picker/src/utils/isValidSegment/isValidSegment.spec.ts rename to packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts diff --git a/packages/date-picker/src/utils/isZeroLike/index.ts b/packages/date-picker/src/shared/utils/isZeroLike/index.ts similarity index 100% rename from packages/date-picker/src/utils/isZeroLike/index.ts rename to packages/date-picker/src/shared/utils/isZeroLike/index.ts diff --git a/packages/date-picker/src/utils/maxDate/index.ts b/packages/date-picker/src/shared/utils/maxDate/index.ts similarity index 100% rename from packages/date-picker/src/utils/maxDate/index.ts rename to packages/date-picker/src/shared/utils/maxDate/index.ts diff --git a/packages/date-picker/src/utils/minDate/index.ts b/packages/date-picker/src/shared/utils/minDate/index.ts similarity index 100% rename from packages/date-picker/src/utils/minDate/index.ts rename to packages/date-picker/src/shared/utils/minDate/index.ts diff --git a/packages/date-picker/src/utils/newDateFromSegments/index.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts similarity index 82% rename from packages/date-picker/src/utils/newDateFromSegments/index.ts rename to packages/date-picker/src/shared/utils/newDateFromSegments/index.ts index 6a4192e64d..d0b18a86de 100644 --- a/packages/date-picker/src/utils/newDateFromSegments/index.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts @@ -1,4 +1,4 @@ -import { DateSegmentsState } from '../../hooks/useDateSegments/DateSegments.types'; +import { DateSegmentsState } from '../../hooks'; import { isValidSegment } from '../isValidSegment'; /** Constructs a date object in UTC from day, month, year segments */ diff --git a/packages/date-picker/src/utils/newDateFromSegments/newDateFromSegments.spec.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts similarity index 100% rename from packages/date-picker/src/utils/newDateFromSegments/newDateFromSegments.spec.ts rename to packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts diff --git a/packages/date-picker/src/utils/newUTC/index.ts b/packages/date-picker/src/shared/utils/newUTC/index.ts similarity index 100% rename from packages/date-picker/src/utils/newUTC/index.ts rename to packages/date-picker/src/shared/utils/newUTC/index.ts diff --git a/packages/date-picker/src/utils/newUTC/newUTC.spec.ts b/packages/date-picker/src/shared/utils/newUTC/newUTC.spec.ts similarity index 100% rename from packages/date-picker/src/utils/newUTC/newUTC.spec.ts rename to packages/date-picker/src/shared/utils/newUTC/newUTC.spec.ts diff --git a/packages/date-picker/src/utils/pickAndOmit/index.ts b/packages/date-picker/src/shared/utils/pickAndOmit/index.ts similarity index 100% rename from packages/date-picker/src/utils/pickAndOmit/index.ts rename to packages/date-picker/src/shared/utils/pickAndOmit/index.ts diff --git a/packages/date-picker/src/utils/setToUTCMidnight/index.ts b/packages/date-picker/src/shared/utils/setToUTCMidnight/index.ts similarity index 100% rename from packages/date-picker/src/utils/setToUTCMidnight/index.ts rename to packages/date-picker/src/shared/utils/setToUTCMidnight/index.ts diff --git a/packages/date-picker/src/utils/setToUTCMidnight/setToUTCMidnight.spec.ts b/packages/date-picker/src/shared/utils/setToUTCMidnight/setToUTCMidnight.spec.ts similarity index 100% rename from packages/date-picker/src/utils/setToUTCMidnight/setToUTCMidnight.spec.ts rename to packages/date-picker/src/shared/utils/setToUTCMidnight/setToUTCMidnight.spec.ts diff --git a/packages/date-picker/src/utils/setUTCDate/index.ts b/packages/date-picker/src/shared/utils/setUTCDate/index.ts similarity index 100% rename from packages/date-picker/src/utils/setUTCDate/index.ts rename to packages/date-picker/src/shared/utils/setUTCDate/index.ts diff --git a/packages/date-picker/src/utils/setUTCMonth/index.ts b/packages/date-picker/src/shared/utils/setUTCMonth/index.ts similarity index 100% rename from packages/date-picker/src/utils/setUTCMonth/index.ts rename to packages/date-picker/src/shared/utils/setUTCMonth/index.ts diff --git a/packages/date-picker/src/utils/setUTCMonth/setUTCMonth.spec.ts b/packages/date-picker/src/shared/utils/setUTCMonth/setUTCMonth.spec.ts similarity index 100% rename from packages/date-picker/src/utils/setUTCMonth/setUTCMonth.spec.ts rename to packages/date-picker/src/shared/utils/setUTCMonth/setUTCMonth.spec.ts diff --git a/packages/date-picker/src/utils/setUTCYear/index.ts b/packages/date-picker/src/shared/utils/setUTCYear/index.ts similarity index 100% rename from packages/date-picker/src/utils/setUTCYear/index.ts rename to packages/date-picker/src/shared/utils/setUTCYear/index.ts diff --git a/packages/date-picker/src/testUtils.ts b/packages/date-picker/src/shared/utils/testutils.ts similarity index 89% rename from packages/date-picker/src/testUtils.ts rename to packages/date-picker/src/shared/utils/testutils.ts index 3e9275dc2b..1fa0c249ca 100644 --- a/packages/date-picker/src/testUtils.ts +++ b/packages/date-picker/src/shared/utils/testutils.ts @@ -6,11 +6,12 @@ import { LeafyGreenProviderProps } from '@leafygreen-ui/leafygreen-provider'; import { ContextPropKeys, + contextPropNames, + DatePickerProviderProps, defaultDatePickerContext, -} from './DatePickerContext/DatePickerContext.utils'; -import { contextPropNames, DatePickerProviderProps } from './DatePickerContext'; -import { BaseDatePickerProps } from './types'; -import { pickAndOmit } from './utils'; +} from '../components/DatePickerContext'; +import { BaseDatePickerProps } from '../types'; +import { pickAndOmit } from '../utils'; /** Time zones used to test with */ export const TimeZones = [ diff --git a/packages/date-picker/src/utils/toDate/index.ts b/packages/date-picker/src/shared/utils/toDate/index.ts similarity index 100% rename from packages/date-picker/src/utils/toDate/index.ts rename to packages/date-picker/src/shared/utils/toDate/index.ts diff --git a/packages/date-picker/src/utils/toDate/toDate.spec.ts b/packages/date-picker/src/shared/utils/toDate/toDate.spec.ts similarity index 100% rename from packages/date-picker/src/utils/toDate/toDate.spec.ts rename to packages/date-picker/src/shared/utils/toDate/toDate.spec.ts diff --git a/packages/hooks/package.json b/packages/hooks/package.json index e751be0f90..737920190b 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -22,7 +22,7 @@ "access": "public" }, "dependencies": { - "@leafygreen-ui/lib": "^11.0.0", + "@leafygreen-ui/lib": "^13.0.0", "lodash": "^4.17.21" }, "gitHead": "dd71a2d404218ccec2e657df9c0263dc1c15b9e0", @@ -35,7 +35,6 @@ "url": "https://jira.mongodb.org/projects/PD/summary" }, "devDependencies": { - "@leafygreen-ui/emotion": "^4.0.7", - "@leafygreen-ui/lib": "^13.0.0" + "@leafygreen-ui/emotion": "^4.0.7" } } diff --git a/yarn.lock b/yarn.lock index 1507f85828..c0eba1ac83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,6 +2274,27 @@ lodash "^4.17.21" prop-types "^15.7.2" +"@leafygreen-ui/lib@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@leafygreen-ui/lib/-/lib-12.0.0.tgz#1ab22c541435e6c0060e21d7097dd65892da24a1" + integrity sha512-nhaxi4oBesnizxO0YK7XwcmiLL9U5QuN7lkZdWGDdmoJgNNL+aRju4W5vmZc7vcazSHfr3gAL+NFAGaAuopyRA== + dependencies: + "@storybook/csf" "^0.1.0" + lodash "^4.17.21" + prop-types "^15.7.2" + +"@leafygreen-ui/typography@^17.0.0": + version "17.0.2" + resolved "https://registry.yarnpkg.com/@leafygreen-ui/typography/-/typography-17.0.2.tgz#39b984d6725dbe38e191ac3cf383e77fd7cd2501" + integrity sha512-wx+kk5VNMOCTenrIG2AcgAKHt9TiLhSTirZARv0J6l4VOgnO+Mkbh3sqd4mk0EOBCAFmsBR3WqwQh00JXU8Htw== + dependencies: + "@leafygreen-ui/emotion" "^4.0.7" + "@leafygreen-ui/icon" "^11.22.2" + "@leafygreen-ui/lib" "^12.0.0" + "@leafygreen-ui/palette" "^4.0.7" + "@leafygreen-ui/polymorphic" "^1.3.6" + "@leafygreen-ui/tokens" "^2.1.4" + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" From f1e2076bc8c1ac773a55d9151c216230ae066cad Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:59:59 -0400 Subject: [PATCH 261/351] Adam/date picker/input auto focus (#2057) * cleanup * fixes date picker input auto-advance * fixes getRelativeSegment tests --- packages/date-picker/package.json | 4 +- .../src/DatePicker/DatePicker.spec.tsx | 77 +++++++++++++ .../DatePickerInput/DatePickerInput.tsx | 53 ++++++++- .../getRelativeSegment.spec.tsx | 101 ++++++++++++++++++ .../utils/getRelativeSegment/index.ts | 34 +++--- .../DateInputSegment/DateInputSegment.tsx | 1 + packages/date-picker/src/shared/types.ts | 7 +- .../date-picker/src/shared/utils/index.ts | 4 +- .../utils/isExplicitSegmentValue/index.ts | 27 +++++ .../isExplicitSegmentValue.spec.ts | 26 +++++ .../src/shared/utils/isValidSegment/index.ts | 2 +- .../isValidSegment/isValidSegment.spec.ts | 20 ++-- .../utils/isValidValueForSegment/index.ts | 15 +++ .../isValidValueForSegment.spec.ts | 31 ++++++ .../shared/utils/newDateFromSegments/index.ts | 13 ++- .../newDateFromSegments.spec.ts | 10 +- yarn.lock | 21 ---- 17 files changed, 384 insertions(+), 62 deletions(-) create mode 100644 packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx create mode 100644 packages/date-picker/src/shared/utils/isExplicitSegmentValue/index.ts create mode 100644 packages/date-picker/src/shared/utils/isExplicitSegmentValue/isExplicitSegmentValue.spec.ts create mode 100644 packages/date-picker/src/shared/utils/isValidValueForSegment/index.ts create mode 100644 packages/date-picker/src/shared/utils/isValidValueForSegment/isValidValueForSegment.spec.ts diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 28617e1468..dcb02652f7 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -20,12 +20,12 @@ "@leafygreen-ui/hooks": "^8.0.0", "@leafygreen-ui/icon": "^11.23.0", "@leafygreen-ui/icon-button": "^15.0.17", - "@leafygreen-ui/lib": "^12.0.0", + "@leafygreen-ui/lib": "^13.0.0", "@leafygreen-ui/palette": "^4.0.7", "@leafygreen-ui/popover": "^11.1.0", "@leafygreen-ui/select": "^11.0.0", "@leafygreen-ui/tokens": "^2.2.0", - "@leafygreen-ui/typography": "^17.0.0", + "@leafygreen-ui/typography": "^18.0.0", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "lodash": "^4.17.21", diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 2db09b0f11..800f43fe4a 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -747,6 +747,83 @@ describe('packages/date-picker', () => { eventContainingTargetValue('2023'), ); }); + + describe('auto-advance focus', () => { + describe('ISO format', () => { + const dateFormat = 'iso8601'; + test('when year value is explicit, focus advances to month', () => { + const { yearInput, monthInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(yearInput, '1999'); + expect(monthInput).toHaveFocus(); + }); + test('when year value is ambiguous, focus does not advance', () => { + const { yearInput } = renderDatePicker({ dateFormat }); + userEvent.type(yearInput, '2'); + expect(yearInput).toHaveFocus(); + }); + test('when year value is out-of-range, focus does not advance', () => { + const { yearInput } = renderDatePicker({ dateFormat }); + userEvent.type(yearInput, '1945'); + expect(yearInput).toHaveFocus(); + }); + + test('when month value is explicit, focus advances to day', () => { + const { monthInput, dayInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(monthInput, '5'); + expect(dayInput).toHaveFocus(); + }); + test('when month value is ambiguous, focus does not advance', () => { + const { monthInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(monthInput, '1'); + expect(monthInput).toHaveFocus(); + }); + }); + + describe('en-US format', () => { + const dateFormat = 'en-US'; + test('when month value is explicit, focus advances to day', () => { + const { monthInput, dayInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(monthInput, '5'); + expect(dayInput).toHaveFocus(); + }); + test('when month value is ambiguous, focus does not advance', () => { + const { monthInput } = renderDatePicker({ dateFormat }); + userEvent.type(monthInput, '1'); + expect(monthInput).toHaveFocus(); + }); + + test('when day value is explicit, focus advances to year', () => { + const { dayInput, yearInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(dayInput, '5'); + expect(yearInput).toHaveFocus(); + }); + test('when day value is ambiguous, focus does not advance', () => { + const { dayInput } = renderDatePicker({ dateFormat }); + userEvent.type(dayInput, '2'); + expect(dayInput).toHaveFocus(); + }); + + test('when year value is ambiguous, focus does not update', () => { + const { monthInput, dayInput, yearInput } = renderDatePicker({ + dateFormat, + }); + userEvent.type(monthInput, '5'); + userEvent.type(dayInput, '5'); + userEvent.type(yearInput, '2'); + expect(yearInput).toHaveFocus(); + }); + }); + }); }); describe('on un-focus/blur', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 5f8783d20f..1ff9e60d91 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -1,4 +1,5 @@ import React, { + ChangeEventHandler, FocusEventHandler, forwardRef, KeyboardEventHandler, @@ -10,7 +11,13 @@ import { keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; import { useSegmentRefs } from '../../shared/hooks'; -import { isElementInputSegment, isZeroLike } from '../../shared/utils'; +import { + isElementInputSegment, + isExplicitSegmentValue, + isValidSegmentName, + isValidValueForSegment, + isZeroLike, +} from '../../shared/utils'; import { getRelativeSegment } from '../utils/getRelativeSegment'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; @@ -29,11 +36,14 @@ export const DatePickerInput = forwardRef( }: DatePickerInputProps, fwdRef, ) => { - const { formatParts, disabled, setOpen, setIsDirty } = + const { formatParts, disabled, setOpen, isDirty, setIsDirty } = useDatePickerContext(); const segmentRefs = useSegmentRefs(); - /** Called when the input, or any of its children, is clicked */ + /** + * Called when the input, or any of its children, is clicked. + * Opens the menu and focuses the appropriate segment + */ const handleInputClick: MouseEventHandler = ({ target }) => { if (!disabled) { setOpen(true); @@ -48,6 +58,10 @@ export const DatePickerInput = forwardRef( } }; + /** + * Called when the calendar button is clicked. + * Opens the menu + */ const handleIconButtonClick: MouseEventHandler = e => { // Prevent the parent click handler from being called since clicks on the parent always opens the dropdown e.stopPropagation(); @@ -146,7 +160,10 @@ export const DatePickerInput = forwardRef( onKeyDown?.(e); }; - /** Called when any child of DatePickerInput is blurred */ + /** + * Called when any child of DatePickerInput is blurred. + * Calls the validation handler. + */ const handleInputBlur: FocusEventHandler = e => { const nextFocus = e.relatedTarget as HTMLInputElement; @@ -161,6 +178,32 @@ export const DatePickerInput = forwardRef( } }; + /** + * Called when any segment changes + */ + const handleSegmentChange: ChangeEventHandler = e => { + const segment = e.target.dataset['segment']; + const segmentValue = e.target.value; + + if (isValidSegmentName(segment)) { + if ( + isValidValueForSegment(segment, segmentValue) && + isExplicitSegmentValue(segment, segmentValue) + ) { + const nextSegment = getRelativeSegment('next', { + segment: segmentRefs[segment], + formatParts, + segmentRefs, + }); + + nextSegment?.current?.focus(); + } else if (!isValidValueForSegment(segment, segmentValue) && isDirty) { + handleValidation?.(value); + } + } + onSegmentChange?.(e); + }; + return ( ( value={value} setValue={setValue} segmentRefs={segmentRefs} - onChange={onSegmentChange} + onChange={handleSegmentChange} /> ); diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx new file mode 100644 index 0000000000..b1983f48e6 --- /dev/null +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { SegmentRefs } from '../../../shared/hooks'; + +import { getRelativeSegment } from '.'; + +const renderTestComponent = () => { + const segmentRefs: SegmentRefs = { + day: React.createRef(), + month: React.createRef(), + year: React.createRef(), + }; + + const result = render( + <> + + + + , + ); + + return { + ...result, + segmentRefs, + }; +}; + +describe('packages/date-picker/utils/getRelativeSegment', () => { + const formatParts: Array = [ + { type: 'year', value: '2023' }, + { type: 'literal', value: '-' }, + { type: 'month', value: '10' }, + { type: 'literal', value: '-' }, + { type: 'day', value: '31' }, + ]; + + let segmentRefs: SegmentRefs; + beforeEach(() => { + segmentRefs = renderTestComponent().segmentRefs; + }); + + test('next from year => month', () => { + expect( + getRelativeSegment('next', { + segment: segmentRefs.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); + test('next from month => day', () => { + expect( + getRelativeSegment('next', { + segment: segmentRefs.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); + + test('prev from day => month', () => { + expect( + getRelativeSegment('prev', { + segment: segmentRefs.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); + + test('prev from month => year', () => { + expect( + getRelativeSegment('prev', { + segment: segmentRefs.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); + + test('first = year', () => { + expect( + getRelativeSegment('first', { + segment: segmentRefs.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); + + test('last = day', () => { + expect( + getRelativeSegment('first', { + segment: segmentRefs.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); +}); diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts index 65fefd0b68..0800258a81 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts @@ -29,25 +29,31 @@ export const getRelativeSegment = ( } // only the relevant segments, not separators - const formatSegments = formatParts.filter(part => part.type !== 'literal'); - const orderedSegmentRefs = formatSegments.map( - ({ type }) => segmentRefs[type as DateSegment], - ); + const formatSegments: Array = formatParts + .filter(part => part.type !== 'literal') + .map(part => part.type as DateSegment); - const currentSegmentIndex: number | undefined = orderedSegmentRefs.findIndex( - ref => ref.current === segment, + /** The index of the reference segment relative to formatParts */ + const currentSegmentIndex: number | undefined = formatSegments.findIndex( + segmentName => segmentRefs[segmentName] === segment, ); + const getRefAtIndex = (index: number) => { + const segmentName = formatSegments[index]; + return segmentRefs[segmentName]; + }; + switch (direction) { case 'first': { - const firstSegmentRef = orderedSegmentRefs[0]; + const firstSegmentRef = getRefAtIndex(0); return firstSegmentRef; } case 'last': { - const lastSegmentRef = last(orderedSegmentRefs); + const lastSegmentName = last(formatSegments); - if (lastSegmentRef) { + if (lastSegmentName) { + const lastSegmentRef = segmentRefs[lastSegmentName]; return lastSegmentRef; } @@ -55,13 +61,13 @@ export const getRelativeSegment = ( } case 'next': { - if (currentSegmentIndex) { + if (!isUndefined(currentSegmentIndex)) { const nextSegmentIndex = Math.min( currentSegmentIndex + 1, - orderedSegmentRefs.length - 1, + formatSegments.length - 1, ); - const nextSegmentRef = orderedSegmentRefs[nextSegmentIndex]; + const nextSegmentRef = getRefAtIndex(nextSegmentIndex); return nextSegmentRef; } @@ -69,10 +75,10 @@ export const getRelativeSegment = ( } case 'prev': { - if (currentSegmentIndex) { + if (!isUndefined(currentSegmentIndex)) { const prevSegmentIndex = Math.max(currentSegmentIndex - 1, 0); - const prevSegmentRef = orderedSegmentRefs[prevSegmentIndex]; + const prevSegmentRef = getRefAtIndex(prevSegmentIndex); return prevSegmentRef; } diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index 7b2505cddb..cbcabe09d0 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -66,6 +66,7 @@ export const DateInputSegment = React.forwardRef< onBlur={onBlur} disabled={disabled} data-testid="lg-date_picker_input-segment" + data-segment={segment} className={cx( baseStyles, fontSizeStyles[baseFontSize], diff --git a/packages/date-picker/src/shared/types.ts b/packages/date-picker/src/shared/types.ts index ec8bde23e0..7949caf16b 100644 --- a/packages/date-picker/src/shared/types.ts +++ b/packages/date-picker/src/shared/types.ts @@ -36,7 +36,12 @@ export interface BaseDatePickerProps extends DarkModeProps { * * @default 'iso8601' */ - dateFormat?: 'en-US' | 'en-UK' | 'iso8601' | string; + dateFormat?: + | 'en-US' + | 'en-UK' + | 'iso8601' + | `${string}-${string}` + | Intl.Locale; /** * A valid IANA timezone string, or UTC offset. diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index 10a1b881fc..a60707eb9a 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -19,6 +19,7 @@ export { getWeeksArray } from './getWeeksArray'; export { isCurrentUTCDay } from './isCurrentUTCDay'; export { isDefined } from './isDefined'; export { isElementInputSegment } from './isElementInputSegment'; +export { isExplicitSegmentValue } from './isExplicitSegmentValue'; export { isOnOrAfter } from './isOnOrAfter'; export { isOnOrBefore } from './isOnOrBefore'; export { isSameTZDay } from './isSameTZDay'; @@ -28,7 +29,8 @@ export { isSameUTCRange } from './isSameUTCRange'; export { isTodayTZ } from './isTodayTZ'; export { isValidDate } from './isValidDate'; export { isValidLocale } from './isValidLocale'; -export { isValidSegment } from './isValidSegment'; +export { isValidSegmentName, isValidSegmentValue } from './isValidSegment'; +export { isValidValueForSegment } from './isValidValueForSegment'; export { isNotZeroLike, isZeroLike } from './isZeroLike'; export { maxDate } from './maxDate'; export { minDate } from './minDate'; diff --git a/packages/date-picker/src/shared/utils/isExplicitSegmentValue/index.ts b/packages/date-picker/src/shared/utils/isExplicitSegmentValue/index.ts new file mode 100644 index 0000000000..166f9f02a9 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isExplicitSegmentValue/index.ts @@ -0,0 +1,27 @@ +import { charsPerSegment } from '../../constants'; +import { DateSegment, DateSegmentValue } from '../../hooks'; +import { isValidValueForSegment } from '../isValidValueForSegment'; + +/** + * Returns whether the provided value is an explicit, unique value for a given segment. + * Contrast this with an ambiguous segment value: + * Explicit: Day = 5, 02 + * Ambiguous: Day = 2 (could be 20-29) + */ +export const isExplicitSegmentValue = ( + segment: DateSegment, + value: DateSegmentValue, +): boolean => { + if (!isValidValueForSegment(segment, value)) return false; + + switch (segment) { + case DateSegment.Day: + return value.length === charsPerSegment.day || Number(value) >= 4; + + case DateSegment.Month: + return value.length === charsPerSegment.month || Number(value) >= 2; + + case DateSegment.Year: + return value.length === charsPerSegment.year; + } +}; diff --git a/packages/date-picker/src/shared/utils/isExplicitSegmentValue/isExplicitSegmentValue.spec.ts b/packages/date-picker/src/shared/utils/isExplicitSegmentValue/isExplicitSegmentValue.spec.ts new file mode 100644 index 0000000000..48422da9c0 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isExplicitSegmentValue/isExplicitSegmentValue.spec.ts @@ -0,0 +1,26 @@ +import { isExplicitSegmentValue } from '.'; + +describe('packages/date-picker/utils/isExplicitSegmentValue', () => { + test('day', () => { + expect(isExplicitSegmentValue('day', '1')).toBe(false); + expect(isExplicitSegmentValue('day', '01')).toBe(true); + expect(isExplicitSegmentValue('day', '4')).toBe(true); + expect(isExplicitSegmentValue('day', '10')).toBe(true); + expect(isExplicitSegmentValue('day', '22')).toBe(true); + expect(isExplicitSegmentValue('day', '31')).toBe(true); + }); + + test('month', () => { + expect(isExplicitSegmentValue('month', '1')).toBe(false); + expect(isExplicitSegmentValue('month', '01')).toBe(true); + expect(isExplicitSegmentValue('month', '2')).toBe(true); + expect(isExplicitSegmentValue('month', '12')).toBe(true); + }); + + test('year', () => { + expect(isExplicitSegmentValue('year', '1')).toBe(false); + expect(isExplicitSegmentValue('year', '1970')).toBe(true); + expect(isExplicitSegmentValue('year', '2000')).toBe(true); + expect(isExplicitSegmentValue('year', '0001')).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/isValidSegment/index.ts b/packages/date-picker/src/shared/utils/isValidSegment/index.ts index 4892c42716..8799712099 100644 --- a/packages/date-picker/src/shared/utils/isValidSegment/index.ts +++ b/packages/date-picker/src/shared/utils/isValidSegment/index.ts @@ -5,7 +5,7 @@ import { DateSegment, DateSegmentValue } from '../../hooks'; /** * Returns whether a given value is a valid segment value */ -export const isValidSegment = ( +export const isValidSegmentValue = ( segment?: DateSegmentValue, ): segment is DateSegmentValue => !isUndefined(segment) && !isNaN(Number(segment)) && Number(segment) > 0; diff --git a/packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts b/packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts index 4dad662d5c..0993fec4be 100644 --- a/packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts +++ b/packages/date-picker/src/shared/utils/isValidSegment/isValidSegment.spec.ts @@ -1,38 +1,38 @@ -import { isValidSegment, isValidSegmentName } from '.'; +import { isValidSegmentName, isValidSegmentValue } from '.'; describe('packages/date-picker/utils/isValidSegment', () => { describe('isValidSegment', () => { test('undefined returns false', () => { - expect(isValidSegment()).toBeFalsy(); + expect(isValidSegmentValue()).toBeFalsy(); }); test('a string returns false', () => { - // @ts-expect-error - expect(isValidSegment('')).toBeFalsy(); + expect(isValidSegmentValue('')).toBeFalsy(); }); test('NaN returns false', () => { - expect(isValidSegment(NaN)).toBeFalsy(); + /// @ts-expect-error + expect(isValidSegmentValue(NaN)).toBeFalsy(); }); test('0 returns false', () => { - expect(isValidSegment(0)).toBeFalsy(); + expect(isValidSegmentValue('0')).toBeFalsy(); }); test('negative returns false', () => { - expect(isValidSegment(-1)).toBeFalsy(); + expect(isValidSegmentValue('-1')).toBeFalsy(); }); test('1970 returns true', () => { - expect(isValidSegment(1970)).toBeTruthy(); + expect(isValidSegmentValue('1970')).toBeTruthy(); }); test('1 returns true', () => { - expect(isValidSegment(1)).toBeTruthy(); + expect(isValidSegmentValue('1')).toBeTruthy(); }); test('2038 returns true', () => { - expect(isValidSegment(2038)).toBeTruthy(); + expect(isValidSegmentValue('2038')).toBeTruthy(); }); }); diff --git a/packages/date-picker/src/shared/utils/isValidValueForSegment/index.ts b/packages/date-picker/src/shared/utils/isValidValueForSegment/index.ts new file mode 100644 index 0000000000..6ba061f49a --- /dev/null +++ b/packages/date-picker/src/shared/utils/isValidValueForSegment/index.ts @@ -0,0 +1,15 @@ +import { inRange } from 'lodash'; + +import { defaultMax, defaultMin } from '../../constants'; +import { DateSegment, DateSegmentValue } from '../../hooks'; +import { isValidSegmentName, isValidSegmentValue } from '../isValidSegment'; + +/** Returns whether a value is valid for a given segment type */ +export const isValidValueForSegment = ( + segment: DateSegment, + value: DateSegmentValue, +): boolean => { + if (!(isValidSegmentValue(value) && isValidSegmentName(segment))) + return false; + return inRange(Number(value), defaultMin[segment], defaultMax[segment] + 1); +}; diff --git a/packages/date-picker/src/shared/utils/isValidValueForSegment/isValidValueForSegment.spec.ts b/packages/date-picker/src/shared/utils/isValidValueForSegment/isValidValueForSegment.spec.ts new file mode 100644 index 0000000000..2c0c5ff810 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isValidValueForSegment/isValidValueForSegment.spec.ts @@ -0,0 +1,31 @@ +import { isValidValueForSegment } from '.'; + +describe('packages/date-picker/utils/isValidSegmentValue', () => { + test('day', () => { + expect(isValidValueForSegment('day', '1')).toBe(true); + expect(isValidValueForSegment('day', '15')).toBe(true); + expect(isValidValueForSegment('day', '31')).toBe(true); + + expect(isValidValueForSegment('day', '0')).toBe(false); + expect(isValidValueForSegment('day', '32')).toBe(false); + }); + + test('month', () => { + expect(isValidValueForSegment('month', '1')).toBe(true); + expect(isValidValueForSegment('month', '9')).toBe(true); + expect(isValidValueForSegment('month', '12')).toBe(true); + + expect(isValidValueForSegment('month', '0')).toBe(false); + expect(isValidValueForSegment('month', '28')).toBe(false); + }); + + test('year', () => { + expect(isValidValueForSegment('year', '1970')).toBe(true); + expect(isValidValueForSegment('year', '2000')).toBe(true); + expect(isValidValueForSegment('year', '2038')).toBe(true); + + expect(isValidValueForSegment('year', '200')).toBe(false); + expect(isValidValueForSegment('year', '1945')).toBe(false); + expect(isValidValueForSegment('year', '2048')).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts index d0b18a86de..9f7cb12728 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts @@ -1,11 +1,20 @@ import { DateSegmentsState } from '../../hooks'; -import { isValidSegment } from '../isValidSegment'; +import { isValidSegmentName, isValidSegmentValue } from '../isValidSegment'; +import { isValidValueForSegment } from '../isValidValueForSegment'; /** Constructs a date object in UTC from day, month, year segments */ export const newDateFromSegments = ( segments: DateSegmentsState, ): Date | undefined => { - if (segments && Object.values(segments).every(isValidSegment)) { + if ( + segments && + Object.entries(segments).every( + ([key, value]) => + isValidSegmentName(key) && + isValidSegmentValue(value) && + isValidValueForSegment(key, value), + ) + ) { const { day, month, year } = segments; return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day))); } diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts index c3f5c475f7..e58c0882c6 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts @@ -3,9 +3,9 @@ import { newDateFromSegments } from '.'; describe('packages/date=picker/utils/newDateFromSegments', () => { test('returns the date in UTC', () => { const newDate = newDateFromSegments({ - day: 1, - month: 1, - year: 2023, + day: '1', + month: '1', + year: '2023', }); expect(newDate?.toISOString()).toEqual('2023-01-01T00:00:00.000Z'); }); @@ -13,8 +13,8 @@ describe('packages/date=picker/utils/newDateFromSegments', () => { test('returns undefined if any segment is undefined', () => { const newDate = newDateFromSegments({ day: undefined, - month: 1, - year: 2023, + month: '1', + year: '2023', }); expect(newDate).toBeUndefined(); diff --git a/yarn.lock b/yarn.lock index c0eba1ac83..1507f85828 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,27 +2274,6 @@ lodash "^4.17.21" prop-types "^15.7.2" -"@leafygreen-ui/lib@^12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@leafygreen-ui/lib/-/lib-12.0.0.tgz#1ab22c541435e6c0060e21d7097dd65892da24a1" - integrity sha512-nhaxi4oBesnizxO0YK7XwcmiLL9U5QuN7lkZdWGDdmoJgNNL+aRju4W5vmZc7vcazSHfr3gAL+NFAGaAuopyRA== - dependencies: - "@storybook/csf" "^0.1.0" - lodash "^4.17.21" - prop-types "^15.7.2" - -"@leafygreen-ui/typography@^17.0.0": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@leafygreen-ui/typography/-/typography-17.0.2.tgz#39b984d6725dbe38e191ac3cf383e77fd7cd2501" - integrity sha512-wx+kk5VNMOCTenrIG2AcgAKHt9TiLhSTirZARv0J6l4VOgnO+Mkbh3sqd4mk0EOBCAFmsBR3WqwQh00JXU8Htw== - dependencies: - "@leafygreen-ui/emotion" "^4.0.7" - "@leafygreen-ui/icon" "^11.22.2" - "@leafygreen-ui/lib" "^12.0.0" - "@leafygreen-ui/palette" "^4.0.7" - "@leafygreen-ui/polymorphic" "^1.3.6" - "@leafygreen-ui/tokens" "^2.1.4" - "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" From 0c323bdf4fcbed0ffada28d546d506f6193bc1ad Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 31 Oct 2023 16:03:04 -0400 Subject: [PATCH 262/351] Update getRelativeSegment.spec.tsx --- .../utils/getRelativeSegment/getRelativeSegment.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx index b1983f48e6..eb1292d296 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx @@ -91,7 +91,7 @@ describe('packages/date-picker/utils/getRelativeSegment', () => { test('last = day', () => { expect( - getRelativeSegment('first', { + getRelativeSegment('last', { segment: segmentRefs.year, formatParts, segmentRefs, From 9d47c70828c15e135e3c38be18dffc3bd933ae5c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 1 Nov 2023 16:26:31 -0400 Subject: [PATCH 263/351] LG-3699, LG-3723: DP keyboard interactions [WIP] (#2052) * WIP * comments * update comments * WIP test * bump lib * WIP test * passing test * comments * remove param * clean up --- .changeset/late-ducks-jump.md | 6 + .../src/DatePicker/DatePicker.spec.tsx | 44 +++++ .../DatePickerMenu/DatePickerMenu.tsx | 11 +- .../DatePickerMenuHeader/index.tsx | 184 ++++++++++-------- .../Calendar/CalendarCell/CalendarCell.tsx | 6 +- .../components/MenuWrapper/MenuWrapper.tsx | 8 +- .../src/PopoverContext.spec.tsx | 6 +- packages/popover/src/Popover.tsx | 2 +- 8 files changed, 178 insertions(+), 89 deletions(-) create mode 100644 .changeset/late-ducks-jump.md diff --git a/.changeset/late-ducks-jump.md b/.changeset/late-ducks-jump.md new file mode 100644 index 0000000000..14841b4b10 --- /dev/null +++ b/.changeset/late-ducks-jump.md @@ -0,0 +1,6 @@ +--- +'@leafygreen-ui/popover': patch +--- + + +Uses `onEnter` in favor of `onEntering` callback in `` since `OnEntering` does not fire inside tests \ No newline at end of file diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 800f43fe4a..e558c6b7b3 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -631,6 +631,28 @@ describe('packages/date-picker', () => { await waitForElementToBeRemoved(menuContainerEl); expect(menuContainerEl).not.toBeInTheDocument(); }); + + describe('chevron', () => { + test('if left chevron is focused, does not close the menu', async () => { + const { openMenu, getMenuElements } = renderDatePicker(); + const { leftChevron } = openMenu(); + tabNTimes(5); + expect(leftChevron).toHaveFocus(); + userEvent.keyboard('{enter}'); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + }); + + test('if right chevron is focused, does not close the menu', async () => { + const { openMenu, getMenuElements } = renderDatePicker(); + const { rightChevron } = openMenu(); + tabNTimes(8); + expect(rightChevron).toHaveFocus(); + userEvent.keyboard('{enter}'); + const { menuContainerEl } = getMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + }); + }); }); describe('Space key', () => { @@ -707,6 +729,28 @@ describe('packages/date-picker', () => { userEvent.keyboard('{escape}'); expect(handleValidation).toHaveBeenCalledWith(undefined); }); + + test('does not close the main menu if a select menu is open and focus is in the select menu', async () => { + const { openMenu, queryAllByRole, findAllByRole } = + renderDatePicker(); + const { monthSelect, menuContainerEl } = openMenu(); + + monthSelect?.focus(); + expect(monthSelect).toHaveFocus(); + userEvent.keyboard('{enter}'); + userEvent.keyboard('{arrowdown}'); + const options = await findAllByRole('option'); + const firstOption = options[0]; + expect(firstOption).toHaveFocus(); + const listBoxes = queryAllByRole('listbox'); + expect(listBoxes).toHaveLength(2); + const selectMenu = listBoxes[1]; + userEvent.keyboard('{escape}'); + await waitFor(() => { + expect(menuContainerEl).toBeInTheDocument(); + expect(selectMenu).not.toBeInTheDocument(); + }); + }); }); /** diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 329d5a13aa..aaeb6e8def 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -9,9 +9,12 @@ import React, { } from 'react'; import { addDays, subDays } from 'date-fns'; -import { useDynamicRefs, usePrevious } from '@leafygreen-ui/hooks'; +import { + useDynamicRefs, + useForwardedRef, + usePrevious, +} from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; -import { useForwardedRef } from '@leafygreen-ui/select/src/utils'; import { spacing } from '@leafygreen-ui/tokens'; import { @@ -160,7 +163,7 @@ export const DatePickerMenu = forwardRef( } }; - /** Called on any keydown within the menu element */ + /** Called on any keydown within the CalendarGrid element */ const handleCalendarKeyDown: KeyboardEventHandler = e => { const { key } = e; const highlightStart = highlight || value || today; @@ -253,6 +256,8 @@ export const DatePickerMenu = forwardRef( ref={headerRef} month={month} setMonth={updateMonth} + handleValidation={handleValidation} + value={value} /> diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx index 80b4b4a4e3..af8027f301 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx @@ -1,24 +1,31 @@ -import React, { forwardRef } from 'react'; +import React, { + forwardRef, + KeyboardEventHandler, + MouseEventHandler, +} from 'react'; import { isBefore } from 'date-fns'; import range from 'lodash/range'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; +import { usePopoverContext } from '@leafygreen-ui/leafygreen-provider'; +import { keyMap } from '@leafygreen-ui/lib'; import { Option, Select } from '@leafygreen-ui/select'; import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; import { Months, selectElementProps } from '../../../shared/constants'; import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../shared/utils'; +import { DatePickerProps } from '../../DatePicker.types'; import { menuHeaderSelectContainerStyles, menuHeaderStyles, selectInputWidthStyles, } from '../DatePickerMenu.styles'; -interface DatePickerMenuHeaderProps { +type DatePickerMenuHeaderProps = { month: Date; setMonth: (newMonth: Date) => void; -} +} & Pick; /** * A helper component for DatePickerMenu. @@ -28,86 +35,109 @@ interface DatePickerMenuHeaderProps { export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps ->(({ month, setMonth }: DatePickerMenuHeaderProps, fwdRef) => { - const { min, max, isInRange } = useDatePickerContext(); +>( + ( + { month, setMonth, value, handleValidation }: DatePickerMenuHeaderProps, + fwdRef, + ) => { + const { min, max, isInRange, setOpen } = useDatePickerContext(); + const { isPopoverOpen } = usePopoverContext(); - const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); + const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); - const updateMonth = (newMonth: Date) => { - // TODO: may need to update this function to check if the months are in range - // (could cause errors when the min date is near the end of the month) - if (isInRange(newMonth)) { - setMonth(newMonth); - } else if (isBefore(newMonth, min)) { - // if the selected month is not in range, - // set the month to the first or last possible month - setMonth(min); - } else { - setMonth(max); - } - }; + const updateMonth = (newMonth: Date) => { + // TODO: may need to update this function to check if the months are in range + // (could cause errors when the min date is near the end of the month) + if (isInRange(newMonth)) { + setMonth(newMonth); + } else if (isBefore(newMonth, min)) { + // if the selected month is not in range, + // set the month to the first or last possible month + setMonth(min); + } else { + setMonth(max); + } + }; - const prevMonth = month.getUTCMonth() - 1; - const nextMonth = month.getUTCMonth() + 1; + /** + * Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked + */ + const handleChevronClick = + (dir: 'left' | 'right'): MouseEventHandler => + e => { + e.stopPropagation(); + e.preventDefault(); + const increment = dir === 'left' ? -1 : 1; + const newMonthIndex = month.getUTCMonth() + increment; + const newMonth = setUTCMonth(month, newMonthIndex); + updateMonth(newMonth); + }; - return ( -
- { - const newMonth = setUTCMonth(month, prevMonth); - updateMonth(newMonth); - }} - > - - -
- - { + const newMonth = setUTCMonth(month, Number(m)); + updateMonth(newMonth); + }} + className={selectInputWidthStyles} + > + {Months.map((m, i) => ( + + ))} + + +
+ - {yearOptions.map(y => ( - - ))} - + +
- { - const newMonth = setUTCMonth(month, nextMonth); - updateMonth(newMonth); - }} - > - - - - ); -}); + ); + }, +); DatePickerMenuHeader.displayName = 'DatePickerMenuHeader'; diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx index b387822500..94db3d5111 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.tsx @@ -65,8 +65,8 @@ export const CalendarCell = React.forwardRef< } }; - // td does not trigger `onClick` on enter/space so we have to listen on key up - const handleKeyUp: KeyboardEventHandler = e => { + // td does not trigger `onClick` on enter/space so we have to listen on key down + const handleKeyDown: KeyboardEventHandler = e => { if (!isDisabled && (e.key === keyMap.Enter || e.key === keyMap.Space)) { (onClick as KeyboardEventHandler)?.(e); // TODO: add focus back to input @@ -104,7 +104,7 @@ export const CalendarCell = React.forwardRef< className, )} onClick={handleClick} - onKeyUp={handleKeyUp} + onKeyDown={handleKeyDown} onFocus={handleFocus} {...rest} > diff --git a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx index 43e239c733..cb78742cc5 100644 --- a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx +++ b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx @@ -1,7 +1,10 @@ import React, { forwardRef } from 'react'; import { cx } from '@leafygreen-ui/emotion'; -import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; +import { + PopoverProvider, + useDarkMode, +} from '@leafygreen-ui/leafygreen-provider'; import { HTMLElementProps } from '@leafygreen-ui/lib'; import Popover, { PopoverProps } from '@leafygreen-ui/popover'; @@ -26,7 +29,8 @@ export const MenuWrapper = forwardRef< className={cx(menuStyles[theme], className)} {...props} > - {children} + {/* Adding the provider inside of to keep track of only the select menus inside this popover */} + {children} ); }, diff --git a/packages/leafygreen-provider/src/PopoverContext.spec.tsx b/packages/leafygreen-provider/src/PopoverContext.spec.tsx index 756e77a449..b591acfade 100644 --- a/packages/leafygreen-provider/src/PopoverContext.spec.tsx +++ b/packages/leafygreen-provider/src/PopoverContext.spec.tsx @@ -53,7 +53,7 @@ describe('packages/leafygreen-provider/PopoverContext', () => { }); }); -function renderUsePopoverContext() { +function renderTestComponent() { const utils = render(); const testChild = utils.getByTestId(childTestID); return { ...utils, testChild }; @@ -61,12 +61,12 @@ function renderUsePopoverContext() { describe('usePopoverContext', () => { test('when child is not a descendent of PopoverProvider, isPopoverOpen is false', () => { - const { testChild } = renderUsePopoverContext(); + const { testChild } = renderTestComponent(); expect(testChild.textContent).toBe('false'); }); test('when child is not a descendent of PopoverProvider, isPopoverOpen is false when setIsPopoverOpen sets isPopoverOpen to true', () => { - const { testChild, getByTestId } = renderUsePopoverContext(); + const { testChild, getByTestId } = renderTestComponent(); // The button's click handler fires setIsPopoverOpen(true) fireEvent.click(getByTestId(buttonTestId)); diff --git a/packages/popover/src/Popover.tsx b/packages/popover/src/Popover.tsx index 1fa7c6d132..e8df1e2e1d 100644 --- a/packages/popover/src/Popover.tsx +++ b/packages/popover/src/Popover.tsx @@ -279,7 +279,7 @@ const Popover = forwardRef( mountOnEnter unmountOnExit appear - onEntered={() => setIsPopoverOpen(true)} + onEnter={() => setIsPopoverOpen(true)} onExit={() => setIsPopoverOpen(false)} > {state => ( From d9a44c98534540efc0b8ed24d45fa33357a2ac1c Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 1 Nov 2023 16:57:40 -0400 Subject: [PATCH 264/351] Update types.ts --- packages/date-picker/src/shared/types.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/date-picker/src/shared/types.ts b/packages/date-picker/src/shared/types.ts index 7949caf16b..c569cbf4b8 100644 --- a/packages/date-picker/src/shared/types.ts +++ b/packages/date-picker/src/shared/types.ts @@ -36,12 +36,7 @@ export interface BaseDatePickerProps extends DarkModeProps { * * @default 'iso8601' */ - dateFormat?: - | 'en-US' - | 'en-UK' - | 'iso8601' - | `${string}-${string}` - | Intl.Locale; + dateFormat?: 'en-US' | 'en-UK' | 'iso8601' | `${string}-${string}`; /** * A valid IANA timezone string, or UTC offset. From 6c88db4ef91c71a4beecfc098dc988d997a6584e Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:14:20 -0500 Subject: [PATCH 265/351] DatePicker: Highlight, tab & focus (#2063) * updates tab order tests * test default focus on menu open * SingleDateContext * converts tests & stories to use context * add refs to context * focus highlighted cell on cal button click * updates highlight cell focus logic * fixes failing tests * update context props api * fix tests & stories - provider api * Adds new workflow tests * set focus on transition end * Popover lifecycle callbacks [LG-3734] (#2067) * LG-3699, LG-3723: DP keyboard interactions [WIP] (#2052) * WIP * comments * update comments * WIP test * bump lib * WIP test * passing test * comments * remove param * clean up * creates utilities * Adds popover Transition lifecycle hooks * Create quiet-eggs-carry.md * mv Popover content * mv PopoverContext content * set isPopoverOpen in onEntered & onExited CBs * Update PopoverContext.spec.tsx * Update Popover.spec.tsx * Update Popover.spec.tsx * use transitionDuration in Popover timeout * extends div props in Popover * user renderHook from RTL/react-hooks * adds type specs * use PopoverComponentProps * Update package.json * Update Popover.spec.tsx * Update Popover.tsx --------- Co-authored-by: Shaneeza * updates tab order tests * test default focus on menu open * SingleDateContext * converts tests & stories to use context * add refs to context * focus highlighted cell on cal button click * updates highlight cell focus logic * fixes failing tests * update context props api * fix tests & stories - provider api * Adds new workflow tests * set focus on transition end * fix async tests. Move esc key handling. * use fireEvent.transitionEnd in tests. * setMonth on value change * update to use async findMenuElements * rename to queryCellByDate * adds waitForMenuToOpen * adds `onExited` handler fired when menu closed * adds test cases to calendar cell * close menu on click/enter press of current value * describe.each * add side effects fn * use single open/close/toggle setters * update getInitialHighlight logic * fix ts --------- Co-authored-by: Shaneeza --- .../src/DatePicker/DatePicker.spec.tsx | 748 ++++++++++-------- .../src/DatePicker/DatePicker.stories.tsx | 6 +- .../src/DatePicker/DatePicker.testutils.tsx | 210 +++-- .../date-picker/src/DatePicker/DatePicker.tsx | 13 +- .../DatePickerComponent.tsx | 126 ++- .../DatePickerComponent.types.ts | 9 +- .../DatePickerInput/DatePickerInput.spec.tsx | 23 +- .../DatePickerInput.stories.tsx | 31 +- .../DatePickerInput/DatePickerInput.tsx | 48 +- .../DatePickerInput/DatePickerInput.types.ts | 7 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 186 +++-- .../DatePickerMenu/DatePickerMenu.stories.tsx | 45 +- .../DatePickerMenu/DatePickerMenu.tsx | 120 +-- .../DatePickerMenu/DatePickerMenu.types.ts | 10 +- .../DatePickerMenuHeader/index.tsx | 187 +++-- .../SingleDateContext/SingleDateContext.tsx | 180 +++++ .../SingleDateContext.types.ts | 90 +++ .../src/DatePicker/SingleDateContext/index.ts | 5 + .../useDatePickerComponentRefs.ts | 25 + .../getInitialHighlight.spec.ts | 18 + .../utils/getInitialHighlight/index.ts | 10 + .../getRelativeSegment.spec.tsx | 203 +++-- .../utils/getRelativeSegment/index.ts | 11 +- .../CalendarCell/CalendarCell.spec.tsx | 165 ++-- .../CalendarCell/CalendarCell.styles.ts | 6 +- .../CalendarGrid/CalendarGrid.stories.tsx | 14 +- .../DateFormField/DateFormField.stories.tsx | 4 +- .../DateInput/DateFormField/DateFormField.tsx | 12 +- .../DateFormField/DateFormField.types.ts | 3 + .../DateInputBox/DateInputBox.spec.tsx | 4 +- .../DateInputBox/DateInputBox.stories.tsx | 6 +- .../DatePickerContext/DatePickerContext.tsx | 8 +- .../DatePickerContext.types.ts | 6 +- .../components/MenuWrapper/MenuWrapper.tsx | 12 +- .../shared/components/MenuWrapper/index.ts | 1 + .../src/shared/components/index.ts | 2 +- packages/date-picker/src/shared/types.ts | 2 +- .../utils/getISODate/getISODate.spec.ts | 14 + .../src/shared/utils/getISODate/index.ts | 14 + .../date-picker/src/shared/utils/index.ts | 1 + 40 files changed, 1656 insertions(+), 929 deletions(-) create mode 100644 packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx create mode 100644 packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts create mode 100644 packages/date-picker/src/DatePicker/SingleDateContext/index.ts create mode 100644 packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts create mode 100644 packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts create mode 100644 packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts create mode 100644 packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts create mode 100644 packages/date-picker/src/shared/utils/getISODate/index.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index e558c6b7b3..3a8a4836cd 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1,12 +1,14 @@ -/* eslint-disable jest/no-disabled-tests */ import React from 'react'; import { + fireEvent, + // prettyDOM, render, waitFor, waitForElementToBeRemoved, + within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { range } from 'lodash'; +import { addDays, subDays } from 'date-fns'; import { Month } from '../shared/constants'; import { newUTC } from '../shared/utils'; @@ -15,7 +17,11 @@ import { tabNTimes, } from '../shared/utils/testutils'; -import { renderDatePicker } from './DatePicker.testutils'; +import { + expectedTabStopLabels, + findTabStopElementMap, + renderDatePicker, +} from './DatePicker.testutils'; import { DatePicker } from '.'; const testToday = newUTC(2023, Month.December, 26); @@ -29,133 +35,135 @@ describe('packages/date-picker', () => { describe('Rendering', () => { /// Note: Many rendering tests should be handled by Chromatic - test('renders label', () => { - const { getByText } = render(); - const label = getByText('Label'); - expect(label).toBeInTheDocument(); - }); + describe('Input', () => { + test('renders label', () => { + const { getByText } = render(); + const label = getByText('Label'); + expect(label).toBeInTheDocument(); + }); - test('renders description', () => { - const { getByText } = render( - , - ); - const description = getByText('Description'); - expect(description).toBeInTheDocument(); - }); + test('renders description', () => { + const { getByText } = render( + , + ); + const description = getByText('Description'); + expect(description).toBeInTheDocument(); + }); - test('spreads rest to formField', () => { - const { getByTestId } = render( - , - ); - const formField = getByTestId('lg-date-picker'); - expect(formField).toBeInTheDocument(); - }); + test('spreads rest to formField', () => { + const { getByTestId } = render( + , + ); + const formField = getByTestId('lg-date-picker'); + expect(formField).toBeInTheDocument(); + }); - test('formField contains label & input elements', () => { - const { getByTestId, getByRole } = render( - , - ); - const formField = getByTestId('lg-date-picker'); - const inputContainer = getByRole('combobox'); - expect(formField.querySelector('label')).toBeInTheDocument(); - expect(formField.querySelector('label')).toHaveTextContent('Label'); - expect(inputContainer).toBeInTheDocument(); - }); + test('formField contains label & input elements', () => { + const { getByTestId, getByRole } = render( + , + ); + const formField = getByTestId('lg-date-picker'); + const inputContainer = getByRole('combobox'); + expect(formField.querySelector('label')).toBeInTheDocument(); + expect(formField.querySelector('label')).toHaveTextContent('Label'); + expect(inputContainer).toBeInTheDocument(); + }); - test('renders 3 inputs', () => { - const { dayInput, monthInput, yearInput } = renderDatePicker(); - expect(dayInput).toBeInTheDocument(); - expect(monthInput).toBeInTheDocument(); - expect(yearInput).toBeInTheDocument(); - }); + test('renders 3 inputs', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker(); + expect(dayInput).toBeInTheDocument(); + expect(monthInput).toBeInTheDocument(); + expect(yearInput).toBeInTheDocument(); + }); - test('renders `value` prop', () => { - const { dayInput, monthInput, yearInput } = renderDatePicker({ - value: new Date(Date.now()), + test('renders `value` prop', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + value: new Date(Date.now()), + }); + expect(dayInput.value).toEqual('26'); + expect(monthInput.value).toEqual('12'); + expect(yearInput.value).toEqual('2023'); }); - expect(dayInput.value).toEqual('26'); - expect(monthInput.value).toEqual('12'); - expect(yearInput.value).toEqual('2023'); - }); - test('renders `initialValue` prop', () => { - const { dayInput, monthInput, yearInput } = renderDatePicker({ - initialValue: new Date(Date.now()), + test('renders `initialValue` prop', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + initialValue: new Date(Date.now()), + }); + expect(dayInput.value).toEqual('26'); + expect(monthInput.value).toEqual('12'); + expect(yearInput.value).toEqual('2023'); }); - expect(dayInput.value).toEqual('26'); - expect(monthInput.value).toEqual('12'); - expect(yearInput.value).toEqual('2023'); }); describe('Menu', () => { - test('menu is initially closed', () => { - const { getMenuElements } = renderDatePicker(); - const { menuContainerEl } = getMenuElements(); + test('menu is initially closed', async () => { + const { findMenuElements } = renderDatePicker(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).not.toBeInTheDocument(); }); test('menu is initially open when rendered with `initialOpen`', async () => { - const { getMenuElements } = renderDatePicker({ initialOpen: true }); - const { menuContainerEl } = getMenuElements(); + const { findMenuElements } = renderDatePicker({ initialOpen: true }); + const { menuContainerEl } = await findMenuElements(); await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); }); - test('if no value is set, menu opens to current month', () => { + test('if no value is set, menu opens to current month', async () => { const { openMenu } = renderDatePicker(); - const { calendarGrid, monthSelect, yearSelect } = openMenu(); + const { calendarGrid, monthSelect, yearSelect } = await openMenu(); expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); expect(monthSelect).toHaveValue(Month.December.toString()); expect(yearSelect).toHaveValue('2023'); }); - test('if a value is set, menu opens to the month of that value', () => { + test('if a value is set, menu opens to the month of that value', async () => { const { openMenu } = renderDatePicker({ value: new Date(Date.UTC(2023, Month.March, 10)), }); - const { calendarGrid, monthSelect, yearSelect } = openMenu(); + const { calendarGrid, monthSelect, yearSelect } = await openMenu(); expect(calendarGrid).toHaveAttribute('aria-label', 'March 2023'); expect(monthSelect).toHaveValue(Month.March.toString()); expect(yearSelect).toHaveValue('2023'); }); - test('renders the appropriate number of cells', () => { + test('renders the appropriate number of cells', async () => { const { openMenu } = renderDatePicker({ value: new Date(Date.UTC(2024, Month.February, 14)), }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); expect(calendarCells).toHaveLength(29); }); describe('Chevrons', () => { - test('Left is disabled if prev. month is entirely out of range', () => { + test('Left is disabled if prev. month is entirely out of range', async () => { const { openMenu } = renderDatePicker({ min: new Date(Date.UTC(2023, Month.December, 1)), }); - const { leftChevron } = openMenu(); + const { leftChevron } = await openMenu(); expect(leftChevron).toHaveAttribute('aria-disabled', 'true'); }); - test('Right is disabled if next month is entirely out of range', () => { + test('Right is disabled if next month is entirely out of range', async () => { const { openMenu } = renderDatePicker({ max: new Date(Date.UTC(2023, Month.December, 31)), }); - const { rightChevron } = openMenu(); + const { rightChevron } = await openMenu(); expect(rightChevron).toHaveAttribute('aria-disabled', 'true'); }); - test('Left is not disabled if only part of prev. month is in range', () => { + test('Left is not disabled if only part of prev. month is in range', async () => { const { openMenu } = renderDatePicker({ min: new Date(Date.UTC(2023, Month.November, 29)), }); - const { leftChevron } = openMenu(); + const { leftChevron } = await openMenu(); expect(leftChevron).toHaveAttribute('aria-disabled', 'false'); }); - test('Right is not disabled if only part of next month is in of range', () => { + test('Right is not disabled if only part of next month is in of range', async () => { const { openMenu } = renderDatePicker({ max: new Date(Date.UTC(2024, Month.January, 2)), }); - const { rightChevron } = openMenu(); + const { rightChevron } = await openMenu(); expect(rightChevron).toHaveAttribute('aria-disabled', 'false'); }); }); @@ -165,10 +173,10 @@ describe('packages/date-picker', () => { describe('Interaction', () => { describe('Mouse interaction', () => { describe('Clicking the input', () => { - test('opens the menu', () => { - const { inputContainer, getMenuElements } = renderDatePicker(); + test('opens the menu', async () => { + const { inputContainer, findMenuElements } = renderDatePicker(); userEvent.click(inputContainer); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); }); @@ -202,124 +210,152 @@ describe('packages/date-picker', () => { }); describe('Clicking the Calendar button', () => { - test('toggles the menu open and close', async () => { - const { calendarButton, getMenuElements } = renderDatePicker(); + test('toggles the menu open and closed', async () => { + const { calendarButton, findMenuElements } = renderDatePicker(); userEvent.click(calendarButton); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); userEvent.click(calendarButton); await waitFor(() => expect(menuContainerEl).not.toBeInTheDocument()); }); test('closes the menu when "initialOpen: true"', async () => { - const { calendarButton, getMenuElements } = renderDatePicker({ + const { calendarButton, findMenuElements } = renderDatePicker({ initialOpen: true, }); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); userEvent.click(calendarButton); await waitFor(() => expect(menuContainerEl).not.toBeInTheDocument()); }); + + test('focuses on the `today` cell by default', async () => { + const { calendarButton, findMenuElements, findByRole } = + renderDatePicker(); + userEvent.click(calendarButton); + const menuContainerEl = await findByRole('listbox'); + const { todayCell } = await findMenuElements(); + // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM + fireEvent.transitionEnd(menuContainerEl!); + expect(todayCell).toHaveFocus(); + }); + + test('focuses on the selected cell', async () => { + const value = newUTC(1994, Month.September, 10); + const { calendarButton, findMenuElements, findByRole } = + renderDatePicker({ + value: value, + }); + userEvent.click(calendarButton); + const menuContainerEl = await findByRole('listbox'); + const { queryCellByDate } = await findMenuElements(); + const valueCell = queryCellByDate(value); + // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM + fireEvent.transitionEnd(menuContainerEl!); + expect(valueCell).toHaveFocus(); + }); }); describe('Clicking a Calendar cell', () => { - test('fires a change handler', () => { + test('fires a change handler', async () => { const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ onDateChange, }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); expect(onDateChange).toHaveBeenCalled(); }); - test('does nothing if the cell is out-of-range', () => { + test('does nothing if the cell is out-of-range', async () => { const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ onDateChange, value: new Date(Date.UTC(2023, Month.September, 15)), min: new Date(Date.UTC(2023, Month.September, 10)), }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell, {}, { skipPointerEventsCheck: true }); + userEvent.click(firstCell!, {}, { skipPointerEventsCheck: true }); expect(firstCell).toHaveAttribute('aria-disabled', 'true'); expect(onDateChange).not.toHaveBeenCalled(); }); - test('fires a validation handler', () => { + test('fires a validation handler', async () => { const handleValidation = jest.fn(); const { openMenu } = renderDatePicker({ handleValidation, }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); expect(handleValidation).toHaveBeenCalled(); }); }); describe('Clicking a Chevron', () => { describe('Left', () => { - test('does not close the menu', () => { + test('does not close the menu', async () => { const { openMenu } = renderDatePicker(); - const { leftChevron, menuContainerEl } = openMenu(); + const { leftChevron, menuContainerEl } = await openMenu(); userEvent.click(leftChevron!); expect(menuContainerEl).toBeInTheDocument(); }); - test('updates the displayed month to the previous', () => { + test('updates the displayed month to the previous', async () => { const { openMenu } = renderDatePicker({ value: newUTC(2023, Month.December, 25), }); const { leftChevron, monthSelect, yearSelect, calendarGrid } = - openMenu(); + await openMenu(); userEvent.click(leftChevron!); expect(calendarGrid).toHaveAttribute('aria-label', 'November 2023'); expect(monthSelect).toHaveValue(Month.November.toString()); expect(yearSelect).toHaveValue('2023'); }); - test('updates the displayed month to the previous, and updates year', () => { + test('updates the displayed month to the previous, and updates year', async () => { const { openMenu } = renderDatePicker({ value: newUTC(2023, Month.January, 5), }); const { leftChevron, monthSelect, yearSelect, calendarGrid } = - openMenu(); + await openMenu(); userEvent.click(leftChevron!); expect(calendarGrid).toHaveAttribute('aria-label', 'December 2022'); expect(monthSelect).toHaveValue(Month.December.toString()); expect(yearSelect).toHaveValue('2022'); }); + + test.todo('does not move focus to the calendar cell'); }); describe('Right', () => { - test('does not close the menu', () => { + test('does not close the menu', async () => { const { openMenu } = renderDatePicker(); - const { rightChevron, menuContainerEl } = openMenu(); + const { rightChevron, menuContainerEl } = await openMenu(); userEvent.click(rightChevron!); expect(menuContainerEl).toBeInTheDocument(); }); - test('updates the displayed month to the next', () => { + test('updates the displayed month to the next', async () => { const { openMenu } = renderDatePicker({ value: newUTC(2023, Month.January, 5), }); const { rightChevron, monthSelect, yearSelect, calendarGrid } = - openMenu(); + await openMenu(); userEvent.click(rightChevron!); expect(calendarGrid).toHaveAttribute('aria-label', 'February 2023'); expect(monthSelect).toHaveValue(Month.February.toString()); expect(yearSelect).toHaveValue('2023'); }); - test('updates the displayed month to the next and updates year', () => { + test('updates the displayed month to the next and updates year', async () => { const { openMenu } = renderDatePicker({ value: newUTC(2023, Month.December, 26), }); const { rightChevron, monthSelect, yearSelect, calendarGrid } = - openMenu(); + await openMenu(); userEvent.click(rightChevron!); expect(calendarGrid).toHaveAttribute('aria-label', 'January 2024'); expect(monthSelect).toHaveValue(Month.January.toString()); @@ -329,9 +365,9 @@ describe('packages/date-picker', () => { }); describe('Month select menu', () => { - test('menu opens over the calendar menu', () => { + test('menu opens over the calendar menu', async () => { const { openMenu, queryAllByRole } = renderDatePicker(); - const { monthSelect, menuContainerEl } = openMenu(); + const { monthSelect, menuContainerEl } = await openMenu(); userEvent.click(monthSelect!); expect(menuContainerEl).toBeInTheDocument(); const listBoxes = queryAllByRole('listbox'); @@ -340,7 +376,7 @@ describe('packages/date-picker', () => { test('selecting the month updates the calendar', async () => { const { openMenu, findAllByRole } = renderDatePicker(); - const { monthSelect, calendarGrid } = openMenu(); + const { monthSelect, calendarGrid } = await openMenu(); userEvent.click(monthSelect!); const options = await findAllByRole('option'); const Jan = options[0]; @@ -350,11 +386,11 @@ describe('packages/date-picker', () => { test('making a selection with enter does not close the datePicker menu', async () => { const { openMenu, findAllByRole } = renderDatePicker(); - const { monthSelect, menuContainerEl } = openMenu(); + const { monthSelect, menuContainerEl } = await openMenu(); userEvent.click(monthSelect!); await findAllByRole('option'); userEvent.keyboard('{arrowdown}'); - userEvent.keyboard('{enter}'); + userEvent.keyboard('[Enter]'); await waitFor(() => { expect(menuContainerEl).toBeInTheDocument(); }); @@ -362,9 +398,9 @@ describe('packages/date-picker', () => { }); describe('Year select menu', () => { - test('menu opens over the calendar menu', () => { + test('menu opens over the calendar menu', async () => { const { openMenu, queryAllByRole } = renderDatePicker(); - const { yearSelect, menuContainerEl } = openMenu(); + const { yearSelect, menuContainerEl } = await openMenu(); userEvent.click(yearSelect!); expect(menuContainerEl).toBeInTheDocument(); const listBoxes = queryAllByRole('listbox'); @@ -375,7 +411,7 @@ describe('packages/date-picker', () => { const { openMenu, findAllByRole } = renderDatePicker({ value: new Date(Date.UTC(2023, Month.December, 26)), }); - const { yearSelect, calendarGrid } = openMenu(); + const { yearSelect, calendarGrid } = await openMenu(); userEvent.click(yearSelect!); const options = await findAllByRole('option'); const _1970 = options[0]; @@ -386,11 +422,11 @@ describe('packages/date-picker', () => { test('making a selection with enter does not close the datePicker menu', async () => { const { openMenu, findAllByRole } = renderDatePicker(); - const { yearSelect, menuContainerEl } = openMenu(); + const { yearSelect, menuContainerEl } = await openMenu(); userEvent.click(yearSelect!); await findAllByRole('option'); userEvent.keyboard('{arrowdown}'); - userEvent.keyboard('{enter}'); + userEvent.keyboard('[Enter]'); await waitFor(() => { expect(menuContainerEl).toBeInTheDocument(); }); @@ -400,18 +436,25 @@ describe('packages/date-picker', () => { describe('Clicking backdrop', () => { test('closes the menu', async () => { const { openMenu, container } = renderDatePicker(); - const { menuContainerEl } = openMenu(); + const { menuContainerEl } = await openMenu(); userEvent.click(container.parentElement!); await waitForElementToBeRemoved(menuContainerEl); }); - test('does not fire a change handler', () => { + test('does not fire a change handler', async () => { const onDateChange = jest.fn(); const { openMenu, container } = renderDatePicker({ onDateChange }); - openMenu(); + await openMenu(); userEvent.click(container.parentElement!); expect(onDateChange).not.toHaveBeenCalled(); }); + + test('returns focus to the calendar button', async () => { + const { openMenu, container, calendarButton } = renderDatePicker(); + await openMenu(); + userEvent.click(container.parentElement!); + await waitFor(() => expect(calendarButton).toHaveFocus()); + }); }); }); @@ -421,48 +464,44 @@ describe('packages/date-picker', () => { describe('updates the highlighted cell', () => { test('to the end of the month if we went backwards', async () => { const { openMenu, findAllByRole } = renderDatePicker({ - value: new Date(Date.UTC(2023, Month.July, 5)), + value: newUTC(2023, Month.July, 5), }); - const { monthSelect, calendarGrid } = openMenu(); + const { monthSelect, queryCellByDate } = await openMenu(); userEvent.click(monthSelect!); const options = await findAllByRole('option'); const Jan = options[0]; userEvent.click(Jan); - const jan31Cell = calendarGrid?.querySelector( - '[data-iso="2023-01-31T00:00:00.000Z"]', - ); - expect(jan31Cell).toHaveFocus(); + const jan31Cell = queryCellByDate(newUTC(2023, Month.January, 31)); + await waitFor(() => expect(jan31Cell).toHaveFocus()); }); test('to the beginning of the month if we went forwards', async () => { const { openMenu, findAllByRole } = renderDatePicker({ - value: new Date(Date.UTC(2023, Month.July, 5)), + value: newUTC(2023, Month.July, 5), }); - const { monthSelect, calendarGrid } = openMenu(); + const { monthSelect, queryCellByDate } = await openMenu(); userEvent.click(monthSelect!); const options = await findAllByRole('option'); const Dec = options[11]; userEvent.click(Dec); - const dec1Cell = calendarGrid?.querySelector( - '[data-iso="2023-12-01T00:00:00.000Z"]', - ); - expect(dec1Cell).toHaveFocus(); + const dec1Cell = queryCellByDate(newUTC(2023, Month.December, 1)); + await waitFor(() => expect(dec1Cell).toHaveFocus()); }); }); }); describe('Keyboard navigation', () => { describe('Tab', () => { - test('menu does not open on keyboard focus', () => { - const { getMenuElements } = renderDatePicker(); + test('menu does not open on keyboard focus', async () => { + const { findMenuElements } = renderDatePicker(); userEvent.tab(); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).not.toBeInTheDocument(); }); - test('menu does not open on subsequent keyboard focus', () => { - const { getMenuElements } = renderDatePicker(); + test('menu does not open on subsequent keyboard focus', async () => { + const { findMenuElements } = renderDatePicker(); tabNTimes(3); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).not.toBeInTheDocument(); }); @@ -473,7 +512,7 @@ describe('packages/date-picker', () => { expect(handleValidation).toHaveBeenCalled(); }); - test('does not call validation handler when changing segment focus', () => { + test('todayCelldoes not call validation handler when changing segment focus', () => { const handleValidation = jest.fn(); renderDatePicker({ handleValidation }); tabNTimes(2); @@ -481,263 +520,190 @@ describe('packages/date-picker', () => { }); describe('Tab order', () => { - describe.each(range(0, 4))('when menu is closed', n => { - test(`Tab ${n} times`, () => { - const { - yearInput, - monthInput, - dayInput, - calendarButton, - inputContainer, - } = renderDatePicker(); - tabNTimes(n); - - switch (n) { - case 0: - expect( - inputContainer.contains(document.activeElement), - ).toBeFalsy(); - break; - case 1: - expect(yearInput).toHaveFocus(); - break; - case 2: - expect(monthInput).toHaveFocus(); - break; - case 3: - expect(dayInput).toHaveFocus(); - break; - case 4: - expect(calendarButton).toHaveFocus(); - break; - case 5: + describe('when menu is closed', () => { + const tabStops = expectedTabStopLabels['closed']; + + test('Tab order proceeds as expected', async () => { + const renderResult = renderDatePicker(); + + for (const label of tabStops) { + const elementMap = await findTabStopElementMap(renderResult); + const element = elementMap[label]; + + if (element !== null) { + expect(element).toHaveFocus(); + } else { expect( - inputContainer.contains(document.activeElement), + renderResult.inputContainer.contains( + document.activeElement, + ), ).toBeFalsy(); - break; + } + + userEvent.tab(); } }); }); - describe.each(range(0, 9))('when the menu is open', n => { - test(`Tab ${n} times`, () => { - const { - yearInput, - monthInput, - dayInput, - calendarButton, - openMenu, - } = renderDatePicker(); - - const { - leftChevron, - monthSelect, - yearSelect, - rightChevron, - todayCell, - } = openMenu(); - - tabNTimes(n); - - switch (n) { - case 0: - expect(yearInput).toHaveFocus(); - break; - case 1: - expect(monthInput).toHaveFocus(); - break; - case 2: - expect(dayInput).toHaveFocus(); - break; - case 3: - expect(calendarButton).toHaveFocus(); - break; - case 4: - expect(todayCell).toHaveFocus(); - break; - case 5: - expect(leftChevron).toHaveFocus(); - break; - case 6: - expect(monthSelect).toHaveFocus(); - break; - case 7: - expect(yearSelect).toHaveFocus(); - break; - case 8: - expect(rightChevron).toHaveFocus(); - break; - case 9: - // Focus is trapped within the menu - expect(todayCell).toHaveFocus(); - break; + describe('when menu is open', () => { + const tabStops = expectedTabStopLabels['open']; + + test(`Tab order proceeds as expected`, async () => { + const renderResult = renderDatePicker({ + initialOpen: true, + }); + + for (const label of tabStops) { + const elementMap = await findTabStopElementMap(renderResult); + const element = elementMap[label]; + + if (element !== null) { + await waitFor(() => expect(element).toHaveFocus()); + } else { + expect( + renderResult.inputContainer.contains( + document.activeElement, + ), + ).toBeFalsy(); + } + + userEvent.tab(); } }); }); }); }); - describe('Enter key', () => { - test('if menu is closed, does not open the menu', () => { - const { getMenuElements } = renderDatePicker(); - userEvent.tab(); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).not.toBeInTheDocument(); - }); - - test('calls validation handler', () => { - const handleValidation = jest.fn(); - renderDatePicker({ handleValidation }); - userEvent.tab(); - userEvent.keyboard('{enter}'); - expect(handleValidation).toHaveBeenCalledWith(undefined); - }); - - test('opens menu if calendar button is focused', () => { - const { getMenuElements } = renderDatePicker(); + describe.each(['Enter', 'Space'])('%p key', key => { + test('opens menu if calendar button is focused', async () => { + const { findMenuElements } = renderDatePicker(); tabNTimes(4); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); + userEvent.keyboard(`[${key}]`); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); }); - test('if month/year select is focused, opens the select menu', async () => { + test('if month select is focused, opens the select menu', async () => { const { openMenu, findAllByRole } = renderDatePicker(); - const { monthSelect } = openMenu(); - tabNTimes(6); - userEvent.keyboard('{enter}'); + const { monthSelect } = await openMenu(); + tabNTimes(2); expect(monthSelect).toHaveFocus(); + userEvent.keyboard(`[${key}]`); const options = await findAllByRole('option'); expect(options.length).toBeGreaterThan(0); }); - test('if a cell is focused, fires a change handler', () => { + test('if a cell is focused, fires a change handler', async () => { const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ onDateChange }); - const { todayCell } = openMenu(); - tabNTimes(4); + const { todayCell } = await openMenu(); expect(todayCell).toHaveFocus(); - userEvent.type(todayCell!, '{enter}'); + userEvent.keyboard(`[${key}]`); expect(onDateChange).toHaveBeenCalled(); }); test('if a cell is focused, closes the menu', async () => { const { openMenu } = renderDatePicker(); - const { todayCell, menuContainerEl } = openMenu(); - tabNTimes(4); - userEvent.keyboard('{enter}'); - userEvent.type(todayCell!, '{enter}'); + const { todayCell, menuContainerEl } = await openMenu(); + expect(todayCell).toHaveFocus(); + userEvent.keyboard(`[${key}]`); await waitForElementToBeRemoved(menuContainerEl); expect(menuContainerEl).not.toBeInTheDocument(); }); + test('if a cell is focused on current value, closes the menu, but does not fire a change handler', async () => { + const onDateChange = jest.fn(); + const value = newUTC(2023, Month.September, 10); + const { openMenu } = renderDatePicker({ value, onDateChange }); + const { menuContainerEl, queryCellByDate } = await openMenu(); + expect(queryCellByDate(value)).toHaveFocus(); + userEvent.keyboard(`[${key}]`); + await waitForElementToBeRemoved(menuContainerEl); + expect(menuContainerEl).not.toBeInTheDocument(); + expect(onDateChange).not.toHaveBeenCalled(); + }); + describe('chevron', () => { test('if left chevron is focused, does not close the menu', async () => { - const { openMenu, getMenuElements } = renderDatePicker(); - const { leftChevron } = openMenu(); - tabNTimes(5); + const { openMenu, findMenuElements } = renderDatePicker(); + const { leftChevron } = await openMenu(); + tabNTimes(1); expect(leftChevron).toHaveFocus(); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); + userEvent.keyboard(`[${key}]`); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); }); test('if right chevron is focused, does not close the menu', async () => { - const { openMenu, getMenuElements } = renderDatePicker(); - const { rightChevron } = openMenu(); - tabNTimes(8); + const { openMenu, findMenuElements } = renderDatePicker(); + const { rightChevron } = await openMenu(); + tabNTimes(4); expect(rightChevron).toHaveFocus(); - userEvent.keyboard('{enter}'); - const { menuContainerEl } = getMenuElements(); + userEvent.keyboard(`[${key}]`); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); }); }); }); - describe('Space key', () => { - test('opens menu if calendar button is focused', () => { - const { getMenuElements } = renderDatePicker(); - tabNTimes(3); - userEvent.keyboard('{space}'); - const { menuContainerEl } = getMenuElements(); - expect(menuContainerEl).toBeInTheDocument(); - }); - - test('if month/year select is focused, opens the select menu', async () => { - const { openMenu, findAllByRole } = renderDatePicker(); - const { monthSelect } = openMenu(); - tabNTimes(6); - userEvent.keyboard('{space}'); - expect(monthSelect).toHaveFocus(); - const options = await findAllByRole('option'); - expect(options.length).toBeGreaterThan(0); - }); - - test('if a cell is focused, fires a change handler', () => { - const onChange = jest.fn(); - const { openMenu } = renderDatePicker({ onChange }); - const { todayCell } = openMenu(); - tabNTimes(4); - expect(todayCell).toHaveFocus(); - userEvent.type(todayCell!, '{space}'); - expect(onChange).toHaveBeenCalled(); + describe('Enter key only', () => { + test('does not open the menu if input is focused', async () => { + const { findMenuElements } = renderDatePicker(); + userEvent.tab(); + userEvent.keyboard(`[Enter]`); + const { menuContainerEl } = await findMenuElements(); + expect(menuContainerEl).not.toBeInTheDocument(); }); - test('if a cell is focused, closes the menu', async () => { - const { openMenu } = renderDatePicker(); - const { todayCell, menuContainerEl } = openMenu(); - tabNTimes(4); - userEvent.keyboard('{space}'); - userEvent.type(todayCell!, '{space}'); - await waitForElementToBeRemoved(menuContainerEl); - expect(menuContainerEl).not.toBeInTheDocument(); + test('calls validation handler', () => { + const handleValidation = jest.fn(); + renderDatePicker({ handleValidation }); + userEvent.tab(); + userEvent.keyboard(`[Enter]`); + expect(handleValidation).toHaveBeenCalledWith(undefined); }); }); describe('Escape key', () => { test('closes the menu', async () => { const { openMenu } = renderDatePicker(); - const { menuContainerEl } = openMenu(); + const { menuContainerEl } = await openMenu(); userEvent.keyboard('{escape}'); await waitForElementToBeRemoved(menuContainerEl); expect(menuContainerEl).not.toBeInTheDocument(); }); - test('does not fire a change handler', () => { + test('does not fire a change handler', async () => { const onDateChange = jest.fn(); const { openMenu } = renderDatePicker({ onDateChange }); - openMenu(); + await openMenu(); userEvent.keyboard('{escape}'); expect(onDateChange).not.toHaveBeenCalled(); }); - test('focus remains in the input element', () => { - const onDateChange = jest.fn(); - const { openMenu, inputContainer } = renderDatePicker({ - onDateChange, - }); - openMenu(); + test('returns focus to the calendar button', async () => { + const { openMenu, calendarButton } = renderDatePicker(); + await openMenu(); userEvent.keyboard('{escape}'); - expect(inputContainer.contains(document.activeElement)).toBeTruthy(); + await waitFor(() => expect(calendarButton).toHaveFocus()); }); - test('fires a validation handler', () => { + test('fires a validation handler', async () => { const handleValidation = jest.fn(); const { openMenu } = renderDatePicker({ handleValidation }); - openMenu(); + await openMenu(); userEvent.keyboard('{escape}'); expect(handleValidation).toHaveBeenCalledWith(undefined); }); - test('does not close the main menu if a select menu is open and focus is in the select menu', async () => { + test('does not close the main menu if a select menu is open', async () => { const { openMenu, queryAllByRole, findAllByRole } = renderDatePicker(); - const { monthSelect, menuContainerEl } = openMenu(); + const { monthSelect, menuContainerEl } = await openMenu(); monthSelect?.focus(); expect(monthSelect).toHaveFocus(); - userEvent.keyboard('{enter}'); + userEvent.keyboard('[Enter]'); userEvent.keyboard('{arrowdown}'); const options = await findAllByRole('option'); const firstOption = options[0]; @@ -758,17 +724,63 @@ describe('packages/date-picker', () => { * Since arrow key behavior changes based on whether the input or menu is focused, * many of these tests exist in the "DatePickerInput" and "DatePickerMenu" components */ - test.todo('Basic arrow key tests'); + describe('Arrow key', () => { + describe('Input', () => { + test.todo('moves focus to segments'); + }); + describe('Menu', () => { + test('left arrow moves focus to the previous day', async () => { + const { calendarButton, findMenuElements } = renderDatePicker(); + userEvent.click(calendarButton); + const { todayCell, menuContainerEl, queryCellByDate } = + await findMenuElements(); + // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM + fireEvent.transitionEnd(menuContainerEl!); + expect(todayCell).toHaveFocus(); + + userEvent.keyboard('{arrowleft}'); + const prevDayCell = queryCellByDate(subDays(testToday, 1)); + await waitFor(() => expect(prevDayCell).toHaveFocus()); + }); + + test('down arrow moves focus to next week', async () => { + const { calendarButton, findMenuElements } = renderDatePicker(); + userEvent.click(calendarButton); + const { todayCell, menuContainerEl, queryCellByDate } = + await findMenuElements(); + // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM + fireEvent.transitionEnd(menuContainerEl!); + expect(todayCell).toHaveFocus(); + + userEvent.keyboard('{arrowdown}'); + const nextWeekCell = queryCellByDate(addDays(testToday, 7)); + await waitFor(() => expect(nextWeekCell).toHaveFocus()); + }); + + test('down arrow can change month', async () => { + const { calendarButton, findByRole } = renderDatePicker(); + userEvent.click(calendarButton); + const menuContainerEl = await findByRole('listbox'); + const calendarGrid = within(menuContainerEl).getByRole('grid'); + fireEvent.transitionEnd(menuContainerEl!); + + expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); + + userEvent.keyboard('{arrowdown}'); + expect(calendarGrid).toHaveAttribute('aria-label', 'January 2024'); + }); + }); + }); }); describe('Typing', () => { describe('Typing into the input', () => { - test('opens the menu', () => { - const { yearInput, getMenuElements } = renderDatePicker(); + test('opens the menu', async () => { + const { yearInput, findMenuElements } = renderDatePicker(); userEvent.tab(); expect(yearInput).toHaveFocus(); userEvent.keyboard('2'); - const { menuContainerEl } = getMenuElements(); + const { menuContainerEl } = await findMenuElements(); expect(menuContainerEl).toBeInTheDocument(); }); @@ -933,6 +945,102 @@ describe('packages/date-picker', () => { }); }); }); + + // TODO: Move these suites to Cypress (or other e2e/integration platform) + describe('User flows', () => { + test('month is updated when value changes', async () => { + const value = newUTC(2023, Month.September, 10); + const { calendarButton, findMenuElements, rerenderDatePicker } = + renderDatePicker(); + rerenderDatePicker({ value }); + userEvent.click(calendarButton); + const { calendarGrid } = await findMenuElements(); + await waitFor(() => + expect(calendarGrid).toHaveAttribute('aria-label', 'September 2023'), + ); + }); + + describe('when closing and re-opening the menu', () => { + test('month is reset to today by default', async () => { + const { openMenu } = renderDatePicker(); + const { calendarGrid, menuContainerEl } = await openMenu(); + + expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); + + userEvent.keyboard('{arrowdown}'); + expect(calendarGrid).toHaveAttribute('aria-label', 'January 2024'); + + userEvent.keyboard('{escape}'); + await waitForElementToBeRemoved(menuContainerEl); + + await openMenu(); + expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); + }); + + test('month is reset to value', async () => { + const value = newUTC(2023, Month.September, 10); + + const { openMenu } = renderDatePicker({ + value, + }); + const { calendarGrid, menuContainerEl } = await openMenu(); + + expect(calendarGrid).toHaveAttribute('aria-label', 'September 2023'); + + userEvent.keyboard('{arrowup}{arrowup}'); + expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); + + userEvent.keyboard('{escape}'); + await waitForElementToBeRemoved(menuContainerEl); + + await openMenu(); + expect(calendarGrid).toHaveAttribute('aria-label', 'September 2023'); + }); + + test('highlight returns to today by default', async () => { + const { openMenu } = renderDatePicker(); + const { todayCell, menuContainerEl, queryCellByDate } = + await openMenu(); + expect(todayCell).toHaveFocus(); + + userEvent.keyboard('{arrowdown}'); + const jan2 = addDays(testToday, 7); + const jan2Cell = queryCellByDate(jan2); + await waitFor(() => expect(jan2Cell).toHaveFocus()); + + userEvent.keyboard('{escape}'); + await waitForElementToBeRemoved(menuContainerEl); + + const { todayCell: newTodayCell } = await openMenu(); + expect(newTodayCell).toHaveFocus(); + }); + + test('highlight returns to value', async () => { + const value = newUTC(2023, Month.September, 10); + const { openMenu, findMenuElements } = renderDatePicker({ + value, + }); + let queryCellByDate = (await openMenu()).queryCellByDate; + const { menuContainerEl } = await findMenuElements(); + let valueCell = queryCellByDate(value); + expect(valueCell).not.toBeNull(); + await waitFor(() => expect(valueCell).toHaveFocus()); + + userEvent.keyboard('{arrowup}{arrowup}'); + const aug27 = subDays(value, 14); + const aug27Cell = queryCellByDate(aug27); + await waitFor(() => expect(aug27Cell).toHaveFocus()); + + userEvent.keyboard('{escape}'); + await waitForElementToBeRemoved(menuContainerEl); + + queryCellByDate = (await openMenu()).queryCellByDate; + valueCell = queryCellByDate(value); + expect(valueCell).not.toBeNull(); + await waitFor(() => expect(valueCell).toHaveFocus()); + }); + }); + }); }); describe('Controlled vs Uncontrolled', () => { @@ -942,9 +1050,9 @@ describe('packages/date-picker', () => { value: new Date(), onDateChange, }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); await waitFor(() => expect(onDateChange).toHaveBeenCalled()); }); @@ -954,9 +1062,9 @@ describe('packages/date-picker', () => { value: new Date(), onDateChange, }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); await waitFor(() => { expect(dayInput.value).toEqual('26'); expect(monthInput.value).toEqual('12'); @@ -969,9 +1077,9 @@ describe('packages/date-picker', () => { const { openMenu } = renderDatePicker({ onDateChange, }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); await waitFor(() => expect(onDateChange).toHaveBeenCalled()); }); @@ -981,9 +1089,9 @@ describe('packages/date-picker', () => { onDateChange, initialValue: new Date(), }); - const { calendarCells } = openMenu(); + const { calendarCells } = await openMenu(); const firstCell = calendarCells?.[0]; - userEvent.click(firstCell); + userEvent.click(firstCell!); await waitFor(() => { expect(dayInput.value).toEqual('01'); expect(monthInput.value).toEqual('12'); diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx index e9135e81c5..d98d53b72a 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx @@ -18,11 +18,7 @@ import { DatePicker } from './DatePicker'; const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - + diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 9924edda02..4bb1ea4689 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -1,14 +1,24 @@ import React from 'react'; import { - getByRole as globalGetByRole, + fireEvent, + // prettyDOM, + queryByRole, render, RenderResult, + waitFor, + within, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { getISODate } from '../shared/utils/getISODate'; + import { DatePickerProps } from './DatePicker.types'; import { DatePicker } from '.'; +const withinElement = (element: HTMLElement | null) => { + return element ? within(element) : null; +}; + interface RenderDatePickerResult extends RenderResult { formField: HTMLElement; inputContainer: HTMLElement; @@ -16,8 +26,27 @@ interface RenderDatePickerResult extends RenderResult { monthInput: HTMLInputElement; yearInput: HTMLInputElement; calendarButton: HTMLButtonElement; - getMenuElements: () => RenderMenuResult; - openMenu: () => RenderMenuResult; + + /** + * Asynchronously query for menu elements + */ + findMenuElements: () => Promise; + + /** + * Wait for the menu element to finish opening. + * When this method resolves, the appropriate calendar cell will be focused + */ + waitForMenuToOpen: () => Promise; + + /** + * Opens the menu by clicking the calendar button. + */ + openMenu: () => Promise; + + /** + * Rerender the Date Picker with new props + */ + rerenderDatePicker: (newProps: Partial) => void; } interface RenderMenuResult { @@ -27,8 +56,10 @@ interface RenderMenuResult { monthSelect: HTMLButtonElement | null; yearSelect: HTMLButtonElement | null; calendarGrid: HTMLTableElement | null; - calendarCells: Array; + calendarCells: Array; todayCell: HTMLTableCellElement | null; + /** Query for a cell with a given date value */ + queryCellByDate: (date: Date) => HTMLTableCellElement | null; } /** @@ -42,70 +73,141 @@ export const renderDatePicker = ( , ); - const formField = result.getByTestId('lg-date-picker'); - const inputContainer = result.getByRole('combobox'); - const dayInput = result.getByLabelText('day') as HTMLInputElement; - const monthInput = result.getByLabelText('month') as HTMLInputElement; - const yearInput = result.getByLabelText('year') as HTMLInputElement; - const calendarButton = globalGetByRole( - inputContainer, - 'button', - ) as HTMLButtonElement; + /** Rerender the Date Picker with new props */ + const rerenderDatePicker = (newProps: Partial) => { + result.rerender( + , + ); + }; + + const inputElements = { + formField: result.getByTestId('lg-date-picker'), + inputContainer: result.getByRole('combobox'), + dayInput: result.getByLabelText('day') as HTMLInputElement, + monthInput: result.getByLabelText('month') as HTMLInputElement, + yearInput: result.getByLabelText('year') as HTMLInputElement, + calendarButton: within(result.getByRole('combobox')).getByRole( + 'button', + ) as HTMLButtonElement, + }; /** - * Returns relevant menu elements. - * Call this after the menu has been opened + * Asynchronously query for menu elements. */ - function getMenuElements(): RenderMenuResult { - const menuContainerEl = result.queryByRole('listbox'); - const calendarGrid = result.queryByRole('grid') as HTMLTableElement; - const calendarCells = result.queryAllByRole( - 'gridcell', - ) as Array; - - // label text is tested in DatePickerMenu.spec - const leftChevron = result.queryByLabelText( - 'Previous month', - ) as HTMLButtonElement; - const rightChevron = result.queryByLabelText( - 'Next month', - ) as HTMLButtonElement; - const monthSelect = result.queryByLabelText( - 'Select month', - ) as HTMLButtonElement; - const yearSelect = result.queryByLabelText( - 'Select year', - ) as HTMLButtonElement; - const todayCell = calendarGrid?.querySelector( - '[aria-current="true"]', - ) as HTMLTableCellElement; + async function findMenuElements(): Promise { + const menuContainerEl = await waitFor(() => + queryByRole(document.body, 'listbox'), + ); + + const calendarGrid = withinElement(menuContainerEl)?.queryByRole('grid'); + const calendarCells = + withinElement(menuContainerEl)?.getAllByRole('gridcell'); + const leftChevron = + withinElement(menuContainerEl)?.queryByLabelText('Previous month'); + const rightChevron = + withinElement(menuContainerEl)?.queryByLabelText('Next month'); + const monthSelect = + withinElement(menuContainerEl)?.queryByLabelText('Select month'); + const yearSelect = + withinElement(menuContainerEl)?.queryByLabelText('Select year'); + + const queryCellByDate = (date: Date): HTMLTableCellElement | null => { + const cell = calendarGrid?.querySelector( + `[data-iso="${getISODate(date)}"]`, + ); + + return cell as HTMLTableCellElement | null; + }; + + const todayCell = queryCellByDate(new Date(Date.now())); return { menuContainerEl, - calendarGrid, - calendarCells, + calendarGrid: calendarGrid as HTMLTableElement | null, + calendarCells: calendarCells as Array, + leftChevron: leftChevron as HTMLButtonElement | null, + rightChevron: rightChevron as HTMLButtonElement | null, + monthSelect: monthSelect as HTMLButtonElement | null, + yearSelect: yearSelect as HTMLButtonElement | null, todayCell, - leftChevron, - rightChevron, - monthSelect, - yearSelect, + queryCellByDate, }; } - function openMenu(): RenderMenuResult { - userEvent.click(inputContainer); - return getMenuElements(); + async function waitForMenuToOpen(): Promise { + const menuElements = await findMenuElements(); + + fireEvent.transitionEnd(menuElements.menuContainerEl!); + + return menuElements; + } + + async function openMenu(): Promise { + userEvent.click(inputElements.calendarButton); + return await waitForMenuToOpen(); } return { ...result, - formField, - inputContainer, - dayInput, - monthInput, - yearInput, - calendarButton, - getMenuElements, + ...inputElements, + findMenuElements, + waitForMenuToOpen, openMenu, + rerenderDatePicker, + }; +}; + +/** Labels used for Tab stop testing */ +export const expectedTabStopLabels = { + closed: [ + 'none', + 'input > year', + 'input > month', + 'input > day', + 'input > open menu button', + 'none', + ], + open: [ + 'none', + 'input > year', + 'input > month', + 'input > day', + 'input > open menu button', + 'menu > today cell', + 'menu > left chevron', + 'menu > month select', + 'menu > year select', + 'menu > right chevron', + 'menu > today cell', + ], +}; + +type TabStopLabel = + (typeof expectedTabStopLabels)[keyof typeof expectedTabStopLabels][number]; + +export const findTabStopElementMap = async ( + renderResult: RenderDatePickerResult, +): Promise> => { + const { yearInput, monthInput, dayInput, calendarButton, findMenuElements } = + renderResult; + const { todayCell, monthSelect, yearSelect, leftChevron, rightChevron } = + await findMenuElements(); + + return { + none: null, + 'input > year': yearInput, + 'input > month': monthInput, + 'input > day': dayInput, + 'input > open menu button': calendarButton, + 'menu > today cell': todayCell, + 'menu > left chevron': leftChevron, + 'menu > month select': monthSelect, + 'menu > year select': yearSelect, + 'menu > right chevron': rightChevron, }; }; diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index a4eea83177..ede91dd991 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -9,6 +9,7 @@ import { pickAndOmit } from '../shared/utils'; import { DatePickerProps } from './DatePicker.types'; import { DatePickerComponent } from './DatePickerComponent'; +import { SingleDateProvider } from './SingleDateContext'; /** * LeafyGreen Date Picker component @@ -19,6 +20,7 @@ export const DatePicker = forwardRef( value: valueProp, initialValue: initialProp, onDateChange: onChangeProp, + handleValidation, ...props }: DatePickerProps, fwdRef, @@ -32,13 +34,14 @@ export const DatePicker = forwardRef( ); return ( - - + + handleValidation={handleValidation} + > + + ); }, diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index 145205687c..133c48df68 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -1,83 +1,81 @@ -import React, { forwardRef, useRef } from 'react'; +import React, { + forwardRef, + KeyboardEventHandler, + TransitionEventHandler, + useRef, +} from 'react'; +import { ExitHandler } from 'react-transition-group/Transition'; import { useBackdropClick, useForwardedRef } from '@leafygreen-ui/hooks'; +import { keyMap } from '@leafygreen-ui/lib'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; -import { isSameUTCDay } from '../../shared/utils'; import { DatePickerInput } from '../DatePickerInput'; -import { DatePickerMenu, DatePickerMenuProps } from '../DatePickerMenu'; +import { DatePickerMenu } from '../DatePickerMenu'; +import { useSingleDateContext } from '../SingleDateContext'; import { DatePickerComponentProps } from './DatePickerComponent.types'; export const DatePickerComponent = forwardRef< HTMLDivElement, DatePickerComponentProps ->( - ( - { value, setValue, handleValidation, ...rest }: DatePickerComponentProps, - fwdRef, - ) => { - const { isOpen, setOpen, isDirty, setIsDirty, menuId } = - useDatePickerContext(); - const closeMenu = () => setOpen(false); +>(({ ...rest }: DatePickerComponentProps, fwdRef) => { + const { isOpen, menuId } = useDatePickerContext(); + const { value, closeMenu, handleValidation, getHighlightedCell } = + useSingleDateContext(); - const formFieldRef = useForwardedRef(fwdRef, null); - const menuRef = useRef(null); + const formFieldRef = useForwardedRef(fwdRef, null); + const menuRef = useRef(null); - /** setValue with possible side effects */ - const updateValue = (newVal: Date | null) => { - setValue(newVal); - }; + useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen); - useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen); + /** Fired when the CSS transition to open the menu is fired */ + const handleMenuTransitionEntered: TransitionEventHandler = () => { + if (isOpen) { + // When the menu opens, set focus to the `highlight` cell + const highlightedCell = getHighlightedCell(); + highlightedCell?.focus(); + } + }; - /** Called when the input's Date value has changed */ - const handleInputValueChange = (inputVal?: Date | null) => { - if (!isSameUTCDay(inputVal, value)) { - // When the value changes via the input element, - // we only trigger validation if the component is dirty - if (isDirty) { - handleValidation?.(inputVal); - } - updateValue(inputVal || null); - } - }; + const handleMenuTransitionExited: ExitHandler = () => { + if (!isOpen) { + closeMenu(); + } + }; - /** Called when any calendar cell is clicked */ - const handleCalendarCellClick: DatePickerMenuProps['onCellClick'] = ( - cellValue: Date, - ) => { - if (!isSameUTCDay(cellValue, value)) { - // when the value is changed via cell, - // we trigger validation every time - handleValidation?.(cellValue); - setIsDirty(true); - // finally we update the component value - updateValue(cellValue); - // and close the menu - setOpen(false); - } - }; + /** Handle key down events that should be fired regardless of target */ + const handleKeyDown: KeyboardEventHandler = e => { + const { key } = e; - return ( - <> - - - - ); - }, -); + switch (key) { + case keyMap.Escape: + closeMenu(); + handleValidation?.(value); + break; + + case keyMap.Enter: + handleValidation?.(value); + break; + + default: + break; + } + }; + + return ( + <> + + + + ); +}); DatePickerComponent.displayName = 'DatePickerContents'; diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts index 4f8b039c96..7f32b24b18 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.types.ts @@ -1,6 +1,4 @@ import { ContextPropKeys } from '../../shared/components/DatePickerContext'; -import { useControlledValue } from '../../shared/hooks'; -import { DateType } from '../../shared/types'; import { DatePickerProps } from '../DatePicker.types'; /** @@ -9,6 +7,7 @@ import { DatePickerProps } from '../DatePicker.types'; * Replaces `onDateChange` with a `setValue` setter function */ export interface DatePickerComponentProps - extends Omit { - setValue: ReturnType>['setValue']; -} + extends Omit< + DatePickerProps, + ContextPropKeys | 'value' | 'handleValidation' | 'onDateChange' + > {} diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index ae3e48149a..e2d2416242 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -8,16 +8,27 @@ import { defaultDatePickerContext, } from '../../shared/components/DatePickerContext'; import { Month } from '../../shared/constants'; +import { + SingleDateProvider, + SingleDateProviderProps, +} from '../SingleDateContext'; import { DatePickerInput, DatePickerInputProps } from '.'; const renderDatePickerInput = ( - props?: Omit, - context?: DatePickerProviderProps, + props?: Omit | null, + singleDateContext?: Partial, + context?: Partial, ) => { const result = render( - - {}} /> + + {}} + {...singleDateContext} + > + + , ); @@ -53,7 +64,7 @@ describe('packages/date-picker/date-picker-input', () => { }); test('moves the cursor when the segment has a value', () => { - const { monthInput } = renderDatePickerInput({ + const { monthInput } = renderDatePickerInput(null, { value: new Date(), }); userEvent.type(monthInput, '{arrowleft}'); @@ -73,7 +84,7 @@ describe('packages/date-picker/date-picker-input', () => { }); test('moves the cursor when the segment has a value', () => { - const { monthInput } = renderDatePickerInput({ + const { monthInput } = renderDatePickerInput(null, { value: new Date(), }); userEvent.type(monthInput, '{arrowright}'); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index ea65d36224..44d63ec4bf 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -12,22 +12,29 @@ import { DatePickerContextProps, DatePickerProvider, } from '../../shared/components/DatePickerContext'; +import { DatePickerProps } from '../DatePicker.types'; +import { + SingleDateContextProps, + SingleDateProvider, + SingleDateProviderProps, +} from '../SingleDateContext'; import { DatePickerInput } from './DatePickerInput'; const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - - + + {}}> + + ); -const meta: StoryMetaType = { +const meta: StoryMetaType< + typeof DatePickerInput, + SingleDateContextProps & DatePickerContextProps +> = { title: 'Components/DatePicker/DatePicker/DatePickerInput', component: DatePickerInput, decorators: [ProviderWrapper], @@ -58,7 +65,9 @@ const meta: StoryMetaType = { export default meta; -export const Basic: StoryFn = props => { +export const Basic: StoryFn< + DatePickerProps & SingleDateProviderProps +> = props => { const [date, setDate] = useState(null); useEffect(() => { @@ -74,9 +83,9 @@ export const Basic: StoryFn = props => { }; return ( - <> - - + + + ); }; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 1ff9e60d91..a34cfb729c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -10,14 +10,15 @@ import { keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; -import { useSegmentRefs } from '../../shared/hooks'; import { isElementInputSegment, isExplicitSegmentValue, + isSameUTCDay, isValidSegmentName, isValidValueForSegment, isZeroLike, } from '../../shared/utils'; +import { useSingleDateContext } from '../SingleDateContext'; import { getRelativeSegment } from '../utils/getRelativeSegment'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; @@ -26,19 +27,31 @@ import { DatePickerInputProps } from './DatePickerInput.types'; export const DatePickerInput = forwardRef( ( { - value, - setValue, onClick, onKeyDown, onChange: onSegmentChange, - handleValidation, ...rest }: DatePickerInputProps, fwdRef, ) => { - const { formatParts, disabled, setOpen, isDirty, setIsDirty } = + const { formatParts, disabled, isDirty, setIsDirty } = useDatePickerContext(); - const segmentRefs = useSegmentRefs(); + const { + refs: { segmentRefs, calendarButtonRef }, + value, + setValue, + openMenu, + toggleMenu, + handleValidation, + } = useSingleDateContext(); + + /** Called when the input's Date value has changed */ + const handleInputValueChange = (inputVal?: Date | null) => { + if (!isSameUTCDay(inputVal, value)) { + handleValidation?.(inputVal); + setValue(inputVal || null); + } + }; /** * Called when the input, or any of its children, is clicked. @@ -46,7 +59,7 @@ export const DatePickerInput = forwardRef( */ const handleInputClick: MouseEventHandler = ({ target }) => { if (!disabled) { - setOpen(true); + openMenu(); const segmentToFocus = getSegmentToFocus({ target, @@ -60,12 +73,12 @@ export const DatePickerInput = forwardRef( /** * Called when the calendar button is clicked. - * Opens the menu + * Opens the menu & focuses the appropriate cell */ const handleIconButtonClick: MouseEventHandler = e => { // Prevent the parent click handler from being called since clicks on the parent always opens the dropdown e.stopPropagation(); - setOpen(o => !o); + toggleMenu(); }; /** Called on any keydown within the input element */ @@ -121,7 +134,7 @@ export const DatePickerInput = forwardRef( case keyMap.ArrowDown: { // if decrementing the segment's value is in range // decrement that segment value - // This is the default `input type=number` behavior + // This is the default `input type=number` & `role="spinbutton"` behavior break; } @@ -138,22 +151,14 @@ export const DatePickerInput = forwardRef( } case keyMap.Enter: - handleValidation?.(value); - break; - case keyMap.Escape: - setOpen(false); - handleValidation?.(value); - break; - case keyMap.Tab: - // default behavior - // focus trap handled by parent + // Behavior handled by parent or menu break; default: // any other keydown should open the menu - setOpen(true); + openMenu(); } // call any handler that was passed in @@ -207,6 +212,7 @@ export const DatePickerInput = forwardRef( return ( ( > diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts index 01bdc732e2..68a73c4f62 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.types.ts @@ -5,11 +5,8 @@ import { HTMLElementProps } from '@leafygreen-ui/lib'; import { DatePickerComponentProps } from '../DatePickerComponent'; export interface DatePickerInputProps - extends Pick< - DatePickerComponentProps, - 'setValue' | 'value' | 'handleValidation' | 'onChange' - >, - Omit, 'onChange'> { + extends Omit, 'onChange'>, + Pick { /** * Click handler */ diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index fddbf6a358..4257aa3590 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { addDays, subDays } from 'date-fns'; +import { addDays } from 'date-fns'; import { DatePickerProvider, @@ -9,7 +9,11 @@ import { defaultDatePickerContext, } from '../../shared/components/DatePickerContext'; import { Month } from '../../shared/constants'; -import { newUTC, setUTCDate } from '../../shared/utils'; +import { getISODate, newUTC, setUTCDate } from '../../shared/utils'; +import { + SingleDateProvider, + SingleDateProviderProps, +} from '../SingleDateContext'; import { DatePickerMenu, DatePickerMenuProps } from '.'; @@ -17,28 +21,48 @@ const testToday = new Date(Date.UTC(2023, Month.September, 10)); const testValue = new Date(Date.UTC(2023, Month.September, 14)); const renderDatePickerMenu = ( - props?: Partial, - context?: Partial, + props?: Partial | null, + singleContext?: Partial | null, + context?: Partial | null, ) => { const result = render( - {}} {...props} />, + {}} + handleValidation={undefined} + {...singleContext} + > + , + , ); - const rerenderDatePickerMenu = (newProps?: Partial) => + const rerenderDatePickerMenu = ( + newProps?: Partial | null, + newSingleContext?: Partial | null, + ) => result.rerender( - {}} - {...({ ...props, ...newProps } as Partial)} - /> - , + setValue={() => {}} + handleValidation={undefined} + {...singleContext} + {...newSingleContext} + > + )} + /> + , ); @@ -49,11 +73,11 @@ const renderDatePickerMenu = ( ) as Array; const todayCell = calendarGrid.querySelector( - `[data-iso="${testToday.toISOString()}"]`, + `[data-iso="${getISODate(testToday)}"]`, ); const getCellWithValue = (date: Date) => - calendarGrid.querySelector(`[data-iso="${date.toISOString()}"]`); + calendarGrid.querySelector(`[data-iso="${getISODate(date)}"]`); return { ...result, @@ -113,14 +137,18 @@ describe('packages/date-picker/date-picker-menu', () => { describe('when value is updated', () => { test('grid is labelled as the current month', () => { const { getByRole, rerenderDatePickerMenu } = renderDatePickerMenu(); - rerenderDatePickerMenu({ value: newUTC(2024, Month.March, 10) }); + rerenderDatePickerMenu(null, { + value: newUTC(2024, Month.March, 10), + }); const grid = getByRole('grid'); expect(grid).toHaveAttribute('aria-label', 'March 2024'); }); test('select menus have correct values', () => { const { getByLabelText, rerenderDatePickerMenu } = renderDatePickerMenu(); - rerenderDatePickerMenu({ value: newUTC(2024, Month.March, 10) }); + rerenderDatePickerMenu(null, { + value: newUTC(2024, Month.March, 10), + }); const monthSelect = getByLabelText('Select month'); const yearSelect = getByLabelText('Select year'); @@ -136,7 +164,7 @@ describe('packages/date-picker/date-picker-menu', () => { }); test('highlight starts on current value when provided', () => { - const { getCellWithValue } = renderDatePickerMenu({ + const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, }); userEvent.tab(); @@ -148,92 +176,93 @@ describe('packages/date-picker/date-picker-menu', () => { describe('Keyboard navigation', () => { describe('Arrow Keys', () => { test('left arrow moves focus to the previous day', async () => { - const { getCellWithValue } = renderDatePickerMenu({ + const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, }); userEvent.tab(); userEvent.keyboard('{arrowleft}'); - const prevDay = getCellWithValue(setUTCDate(testValue, 13)); - expect(prevDay).toHaveFocus(); + + await waitFor(() => expect(prevDay).toHaveFocus()); }); - test('right arrow moves focus to the next day', () => { - const { getCellWithValue } = renderDatePickerMenu({ + test('right arrow moves focus to the next day', async () => { + const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, }); userEvent.tab(); userEvent.keyboard('{arrowright}'); + const nextDay = getCellWithValue(setUTCDate(testValue, 15)); - expect(nextDay).toHaveFocus(); + await waitFor(() => expect(nextDay).toHaveFocus()); }); - test('up arrow moves focus to the previous week', () => { - const { getCellWithValue } = renderDatePickerMenu({ + test('up arrow moves focus to the previous week', async () => { + const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, }); userEvent.tab(); userEvent.keyboard('{arrowup}'); const prevWeek = getCellWithValue(setUTCDate(testValue, 7)); - expect(prevWeek).toHaveFocus(); + await waitFor(() => expect(prevWeek).toHaveFocus()); }); - test('down arrow moves focus to the next week', () => { - const { getCellWithValue } = renderDatePickerMenu({ + test('down arrow moves focus to the next week', async () => { + const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, }); userEvent.tab(); userEvent.keyboard('{arrowdown}'); const nextWeek = getCellWithValue(setUTCDate(testValue, 21)); - expect(nextWeek).toHaveFocus(); + await waitFor(() => expect(nextWeek).toHaveFocus()); }); describe('when next day would be out of range', () => { - const props = { + const singleCtx = { value: testToday, }; - test('left arrow does nothing', () => { - const { todayCell } = renderDatePickerMenu(props, { + test('left arrow does nothing', async () => { + const { todayCell } = renderDatePickerMenu(null, singleCtx, { min: testToday, }); userEvent.tab(); userEvent.keyboard('{arrowleft}'); - expect(todayCell).toHaveFocus(); + await waitFor(() => expect(todayCell).toHaveFocus()); }); - test('right arrow does nothing', () => { - const { todayCell } = renderDatePickerMenu(props, { + test('right arrow does nothing', async () => { + const { todayCell } = renderDatePickerMenu(null, singleCtx, { max: testToday, }); userEvent.tab(); userEvent.keyboard('{arrowright}'); - expect(todayCell).toHaveFocus(); + await waitFor(() => expect(todayCell).toHaveFocus()); }); - test('up arrow does nothing', () => { - const { todayCell } = renderDatePickerMenu(props, { + test('up arrow does nothing', async () => { + const { todayCell } = renderDatePickerMenu(null, singleCtx, { min: addDays(testToday, -6), }); userEvent.tab(); userEvent.keyboard('{arrowup}'); - expect(todayCell).toHaveFocus(); + await waitFor(() => expect(todayCell).toHaveFocus()); }); - test('down arrow does nothing', () => { - const { todayCell } = renderDatePickerMenu(props, { + test('down arrow does nothing', async () => { + const { todayCell } = renderDatePickerMenu(null, singleCtx, { max: addDays(testToday, 6), }); userEvent.tab(); userEvent.keyboard('{arrowdown}'); - expect(todayCell).toHaveFocus(); + await waitFor(() => expect(todayCell).toHaveFocus()); }); }); describe('update the displayed month', () => { test('left arrow updates displayed month to previous', () => { const value = new Date(Date.UTC(2023, Month.September, 1)); - const { calendarGrid } = renderDatePickerMenu({ value }); + const { calendarGrid } = renderDatePickerMenu(null, { value }); userEvent.tab(); userEvent.keyboard('{arrowleft}'); expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); @@ -241,7 +270,7 @@ describe('packages/date-picker/date-picker-menu', () => { test('right arrow updates displayed month to next', () => { const value = new Date(Date.UTC(2023, Month.September, 30)); - const { calendarGrid } = renderDatePickerMenu({ value }); + const { calendarGrid } = renderDatePickerMenu(null, { value }); userEvent.tab(); userEvent.keyboard('{arrowright}'); expect(calendarGrid).toHaveAttribute('aria-label', 'October 2023'); @@ -249,7 +278,7 @@ describe('packages/date-picker/date-picker-menu', () => { test('up arrow updates displayed month to previous', () => { const value = new Date(Date.UTC(2023, Month.September, 6)); - const { calendarGrid } = renderDatePickerMenu({ value }); + const { calendarGrid } = renderDatePickerMenu(null, { value }); userEvent.tab(); userEvent.keyboard('{arrowup}'); expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); @@ -257,14 +286,16 @@ describe('packages/date-picker/date-picker-menu', () => { test('down arrow updates displayed month to next', () => { const value = new Date(Date.UTC(2023, Month.September, 25)); - const { calendarGrid } = renderDatePickerMenu({ value }); + const { calendarGrid } = renderDatePickerMenu(null, { value }); userEvent.tab(); userEvent.keyboard('{arrowdown}'); expect(calendarGrid).toHaveAttribute('aria-label', 'October 2023'); }); test('does not update month when month does not need to change', () => { - const { calendarGrid } = renderDatePickerMenu({ value: testValue }); + const { calendarGrid } = renderDatePickerMenu(null, { + value: testValue, + }); userEvent.tab(); userEvent.keyboard('{arrowleft}{arrowright}{arrowup}{arrowdown}'); expect(calendarGrid).toHaveAttribute('aria-label', 'September 2023'); @@ -272,37 +303,54 @@ describe('packages/date-picker/date-picker-menu', () => { }); describe('when month should be updated', () => { - test('left arrow focuses the previous day', () => { - const value = new Date(Date.UTC(2023, Month.September, 1)); - const { getCellWithValue } = renderDatePickerMenu({ value }); + test('left arrow focuses the previous day', async () => { + const value = newUTC(2023, Month.September, 1); + const { getCellWithValue } = renderDatePickerMenu(null, { + value, + }); userEvent.tab(); userEvent.keyboard('{arrowleft}'); - const highlightedCell = getCellWithValue(subDays(value, 1)); - expect(highlightedCell).toHaveFocus(); + const highlightedCell = getCellWithValue( + newUTC(2023, Month.August, 31), + ); + + await waitFor(() => expect(highlightedCell).toHaveFocus()); }); - test('right arrow focuses the next day', () => { - const value = new Date(Date.UTC(2023, Month.September, 30)); - const { getCellWithValue } = renderDatePickerMenu({ value }); + test('right arrow focuses the next day', async () => { + const value = newUTC(2023, Month.September, 30); + const { getCellWithValue } = renderDatePickerMenu(null, { + value, + }); userEvent.tab(); userEvent.keyboard('{arrowright}'); - const highlightedCell = getCellWithValue(addDays(value, 1)); - expect(highlightedCell).toHaveFocus(); + const highlightedCell = getCellWithValue( + newUTC(2023, Month.October, 1), + ); + await waitFor(() => expect(highlightedCell).toHaveFocus()); }); - test('up arrow focuses the previous week', () => { - const value = new Date(Date.UTC(2023, Month.September, 6)); - const { getCellWithValue } = renderDatePickerMenu({ value }); + test('up arrow focuses the previous week', async () => { + const value = newUTC(2023, Month.September, 7); + const { getCellWithValue } = renderDatePickerMenu(null, { + value, + }); userEvent.tab(); userEvent.keyboard('{arrowup}'); - const highlightedCell = getCellWithValue(subDays(value, 7)); - expect(highlightedCell).toHaveFocus(); + const highlightedCell = getCellWithValue( + newUTC(2023, Month.August, 31), + ); + await waitFor(() => expect(highlightedCell).toHaveFocus()); }); - test('down arrow focuses the next week', () => { - const value = new Date(Date.UTC(2023, Month.September, 25)); - const { getCellWithValue } = renderDatePickerMenu({ value }); + test('down arrow focuses the next week', async () => { + const value = newUTC(2023, Month.September, 24); + const { getCellWithValue } = renderDatePickerMenu(null, { + value, + }); userEvent.tab(); userEvent.keyboard('{arrowdown}'); - const highlightedCell = getCellWithValue(addDays(value, 7)); - expect(highlightedCell).toHaveFocus(); + const highlightedCell = getCellWithValue( + newUTC(2023, Month.October, 1), + ); + await waitFor(() => expect(highlightedCell).toHaveFocus()); }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 88ebc4b5a3..833c4a3417 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -18,12 +18,18 @@ import { } from '../../shared/components/DatePickerContext'; import { Month } from '../../shared/constants'; import { newUTC, pickAndOmit } from '../../shared/utils'; +import { + SingleDateContextProps, + SingleDateProvider, +} from '../SingleDateContext'; import { DatePickerMenu } from './DatePickerMenu'; import { DatePickerMenuProps } from './DatePickerMenu.types'; const mockToday = newUTC(2023, Month.September, 14); -type DecoratorArgs = DatePickerMenuProps & DatePickerContextProps; +type DecoratorArgs = DatePickerMenuProps & + SingleDateContextProps & + DatePickerContextProps; const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { const [{ darkMode, ...contextProps }, { ...props }] = pickAndOmit( @@ -37,11 +43,9 @@ const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { return ( @@ -82,15 +86,10 @@ export const Basic: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( - <> + refEl - - + + ); }, }; @@ -100,15 +99,15 @@ export const WithValue: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( -
- refEl - {}} - /> -
+ {}} + > +
+ refEl + +
+
); }, }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index aaeb6e8def..b7cdd22804 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -1,22 +1,18 @@ import React, { forwardRef, KeyboardEventHandler, + useCallback, useEffect, - useLayoutEffect, useMemo, useRef, - useState, } from 'react'; import { addDays, subDays } from 'date-fns'; -import { - useDynamicRefs, - useForwardedRef, - usePrevious, -} from '@leafygreen-ui/hooks'; +import { useForwardedRef, usePrevious } from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; import { spacing } from '@leafygreen-ui/tokens'; +import { DateType } from '../../shared'; import { CalendarCell, CalendarCellState, @@ -27,11 +23,13 @@ import { MenuWrapper } from '../../shared/components/MenuWrapper'; import { getFirstOfMonth, getFullMonthLabel, + getISODate, getUTCDateString, isSameUTCDay, isSameUTCMonth, setToUTCMidnight, } from '../../shared/utils'; +import { useSingleDateContext } from '../SingleDateContext'; import { getNewHighlight } from './utils/getNewHighlight'; import { @@ -43,45 +41,58 @@ import { DatePickerMenuProps } from './DatePickerMenu.types'; import { DatePickerMenuHeader } from './DatePickerMenuHeader'; export const DatePickerMenu = forwardRef( - ( - { value, onCellClick, handleValidation, ...rest }: DatePickerMenuProps, - fwdRef, - ) => { + ({ onKeyDown, ...rest }: DatePickerMenuProps, fwdRef) => { const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); - const { isInRange, isOpen, setOpen } = useDatePickerContext(); + const { isInRange, isOpen, setIsDirty } = useDatePickerContext(); + const { + refs, + value, + setValue, + handleValidation, + month, + setMonth: setDisplayMonth, + highlight, + closeMenu, + setHighlight, + getCellWithValue, + getHighlightedCell, + } = useSingleDateContext(); - // TODO: https://jira.mongodb.org/browse/LG-3666 - // useDynamicRefs may overflow if a user navigates to too many months. - // consider purging the refs map within the hook - const cellRefs = useDynamicRefs(); const ref = useForwardedRef(fwdRef, null); + const cellRefs = refs.calendarCellRefs; const headerRef = useRef(null); const calendarRef = useRef(null); - const [month, setDisplayMonth] = useState( - value ?? getFirstOfMonth(today), - ); - const [highlight, setHighlight] = useState(value || today); - const prevValue = usePrevious(value); - const prevHighlight = usePrevious(highlight); + // const prevOpen = usePrevious(isOpen); + // const prevHighlight = usePrevious(highlight); const monthLabel = getFullMonthLabel(month); + // focuses the DOM element for the appropriate cell + const focusCellWithDate = useCallback( + (date: DateType) => { + requestAnimationFrame(() => { + const highlightedCell = getCellWithValue(date); + highlightedCell?.focus(); + }); + }, + [getCellWithValue], + ); + /** setDisplayMonth with side effects */ const updateMonth = (newMonth: Date) => { if (isSameUTCMonth(newMonth, month)) { return; } - + setDisplayMonth(newMonth); const newHighlight = getNewHighlight(highlight, month, newMonth); const shouldUpdateHighlight = !isSameUTCDay(highlight, newHighlight); if (newHighlight && shouldUpdateHighlight) { setHighlight(newHighlight); + focusCellWithDate(newHighlight); } - - setDisplayMonth(newMonth); }; /** setHighlight with side effects */ @@ -90,21 +101,10 @@ export const DatePickerMenu = forwardRef( if (!isSameUTCMonth(month, newHighlight)) { setDisplayMonth(newHighlight); } - - // keep track of the highlighted cell setHighlight(newHighlight); + focusCellWithDate(newHighlight); }; - /** - * When highlight changes, after the DOM changes, focus the relevant cell - */ - useLayoutEffect(() => { - if (highlight && !isSameUTCDay(highlight, prevHighlight)) { - const highlightCellRef = cellRefs(highlight.toISOString()); - highlightCellRef.current?.focus(); - } - }, [cellRefs, highlight, prevHighlight]); - /** * If the new value is not the current month, update the month */ @@ -116,7 +116,7 @@ export const DatePickerMenu = forwardRef( ) { setDisplayMonth(getFirstOfMonth(value)); } - }, [month, prevValue, value]); + }, [month, prevValue, setDisplayMonth, value]); /** Returns the current state of the cell */ const getCellState = (cellDay: Date | null): CalendarCellState => { @@ -131,10 +131,24 @@ export const DatePickerMenu = forwardRef( return CalendarCellState.Disabled; }; + /** Called when any calendar cell is clicked */ + const handleCalendarCellClick = (cellValue: Date) => { + if (!isSameUTCDay(cellValue, value)) { + // when the value is changed via cell, + // we trigger validation every time + handleValidation?.(cellValue); + setIsDirty(true); + // finally we update the component value + setValue(cellValue); + } + // and close the menu + closeMenu(); + }; + /** Creates a click handler for a specific cell date */ const cellClickHandlerForDay = (day: Date) => () => { if (isInRange(day)) { - onCellClick(day); + handleCalendarCellClick(day); } }; @@ -147,16 +161,16 @@ export const DatePickerMenu = forwardRef( if (e.key === keyMap.Tab) { const currentFocus = document.activeElement; - const highlightKey = highlight?.toISOString(); - const highlightedCellElement = highlightKey - ? cellRefs(highlightKey)?.current - : undefined; + const highlightedCellElement = getHighlightedCell(); const rightChevronElement = headerRef.current?.lastElementChild; - if (!e.shiftKey && currentFocus === rightChevronElement) { + const isFocusOnRightChevron = currentFocus === rightChevronElement; + const isFocusOnCell = currentFocus === highlightedCellElement; + + if (!e.shiftKey && isFocusOnRightChevron) { (highlightedCellElement as HTMLElement)?.focus(); e.preventDefault(); - } else if (e.shiftKey && currentFocus === highlightedCellElement) { + } else if (e.shiftKey && isFocusOnCell) { (rightChevronElement as HTMLElement)?.focus(); e.preventDefault(); } @@ -190,11 +204,6 @@ export const DatePickerMenu = forwardRef( break; } - case keyMap.Escape: - setOpen(false); - handleValidation?.(value); - break; - // The isInRange check below prevents tab presses from propagating up so we add a switch case for tab presses where we can then call handleWrapperTabKeyPress which will handle trapping focus case keyMap.Tab: handleWrapperTabKeyPress(e); @@ -205,11 +214,14 @@ export const DatePickerMenu = forwardRef( } // if nextHighlight is in range - if (isInRange(nextHighlight)) { + if (isInRange(nextHighlight) && !isSameUTCDay(nextHighlight, highlight)) { updateHighlight(nextHighlight); // Prevent the parent keydown handler from being called e.stopPropagation(); } + + // call any handler that was passed in + onKeyDown?.(e); }; return ( @@ -219,8 +231,8 @@ export const DatePickerMenu = forwardRef( active={isOpen} spacing={spacing[1]} className={menuWrapperStyles} - onKeyDown={handleWrapperTabKeyPress} usePortal + onKeyDown={handleWrapperTabKeyPress} {...rest} >
@@ -240,13 +252,13 @@ export const DatePickerMenu = forwardRef( {(day, i) => ( {day.getUTCDate()} diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts index 768d00d35d..1a21370038 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.types.ts @@ -2,12 +2,6 @@ import { HTMLElementProps } from '@leafygreen-ui/lib'; import { PopoverProps } from '@leafygreen-ui/popover'; import { PortalControlProps } from '@leafygreen-ui/popover'; -import { DatePickerProps } from '..'; - export type DatePickerMenuProps = PortalControlProps & - Pick & - Pick & - HTMLElementProps<'div'> & { - /** Callback fired when a cell is clicked */ - onCellClick: (cellDate: Date) => void; - }; + Omit & + HTMLElementProps<'div'>; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx index af8027f301..220088f64c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx @@ -9,7 +9,6 @@ import range from 'lodash/range'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import { usePopoverContext } from '@leafygreen-ui/leafygreen-provider'; -import { keyMap } from '@leafygreen-ui/lib'; import { Option, Select } from '@leafygreen-ui/select'; import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; @@ -35,109 +34,103 @@ type DatePickerMenuHeaderProps = { export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps ->( - ( - { month, setMonth, value, handleValidation }: DatePickerMenuHeaderProps, - fwdRef, - ) => { - const { min, max, isInRange, setOpen } = useDatePickerContext(); - const { isPopoverOpen } = usePopoverContext(); +>(({ month, setMonth }: DatePickerMenuHeaderProps, fwdRef) => { + const { min, max, isInRange } = useDatePickerContext(); + const { isPopoverOpen: isSelectMenuOpen } = usePopoverContext(); - const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); + const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); - const updateMonth = (newMonth: Date) => { - // TODO: may need to update this function to check if the months are in range - // (could cause errors when the min date is near the end of the month) - if (isInRange(newMonth)) { - setMonth(newMonth); - } else if (isBefore(newMonth, min)) { - // if the selected month is not in range, - // set the month to the first or last possible month - setMonth(min); - } else { - setMonth(max); - } - }; - - /** - * Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked - */ - const handleChevronClick = - (dir: 'left' | 'right'): MouseEventHandler => - e => { - e.stopPropagation(); - e.preventDefault(); - const increment = dir === 'left' ? -1 : 1; - const newMonthIndex = month.getUTCMonth() + increment; - const newMonth = setUTCMonth(month, newMonthIndex); - updateMonth(newMonth); - }; + const updateMonth = (newMonth: Date) => { + // TODO: may need to update this function to check if the months are in range + // (could cause errors when the min date is near the end of the month) + if (isInRange(newMonth)) { + setMonth(newMonth); + } else if (isBefore(newMonth, min)) { + // if the selected month is not in range, + // set the month to the first or last possible month + setMonth(min); + } else { + setMonth(max); + } + }; - /** - * Ensure that the date picker menu will not close when a select menu is open, focus is inside the select menu, and the ESC key is pressed. - */ - const handleEcsPress: KeyboardEventHandler = e => { - // `isPopoverOpen` updated value is not accessible in `` since the `` is inside `` - if (!isPopoverOpen && e.key === keyMap.Escape) { - setOpen(false); - handleValidation?.(value); - } + /** + * Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked + */ + const handleChevronClick = + (dir: 'left' | 'right'): MouseEventHandler => + e => { + e.stopPropagation(); + e.preventDefault(); + const increment = dir === 'left' ? -1 : 1; + const newMonthIndex = month.getUTCMonth() + increment; + const newMonth = setUTCMonth(month, newMonthIndex); + updateMonth(newMonth); }; - return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
- = e => { + // `isSelectMenuOpen` provided by `PopoverProvider` is `true` if any popover _within_ the menu is open + if (isSelectMenuOpen) { + e.stopPropagation(); + } + }; + + return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
+ + + +
+ { - const newMonth = setUTCMonth(month, Number(m)); - updateMonth(newMonth); - }} - className={selectInputWidthStyles} - > - {Months.map((m, i) => ( - - ))} - - -
- ( + + ))} + +
- ); - }, -); + + + +
+ ); +}); DatePickerMenuHeader.displayName = 'DatePickerMenuHeader'; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx new file mode 100644 index 0000000000..c6242c0fbd --- /dev/null +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx @@ -0,0 +1,180 @@ +import React, { + createContext, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; + +import { usePrevious } from '@leafygreen-ui/hooks'; + +import { + DateType, + getFirstOfMonth, + setToUTCMidnight, + useDatePickerContext, +} from '../../shared'; +import { getISODate, isSameUTCDay } from '../../shared/utils'; +import { getInitialHighlight } from '../utils/getInitialHighlight'; + +import { + SingleDateContextProps, + SingleDateProviderProps, +} from './SingleDateContext.types'; +import { useDateRangeComponentRefs } from './useDatePickerComponentRefs'; + +const SingleDateContext = createContext( + {} as SingleDateContextProps, +); + +/** + * A provider for context values in a single DatePicker + */ +export const SingleDateProvider = ({ + children, + value, + setValue: _setValue, + handleValidation, +}: PropsWithChildren) => { + const refs = useDateRangeComponentRefs(); + const { isOpen, setOpen } = useDatePickerContext(); + const prevValue = usePrevious(value); + + const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); + + /** + * Keep track of the displayed month + */ + const [month, _setMonth] = useState(getFirstOfMonth(value ?? today)); + + /** + * Keep track of the element the user is highlighting with the keyboard + */ + const [highlight, _setHighlight] = useState( + getInitialHighlight(value, today), + ); + + /*********** + * SETTERS * + ***********/ + + /** + * Set the value and run side effects here + */ + const setValue = (newVal?: DateType) => { + _setValue(newVal ?? null); + setMonth(getFirstOfMonth(newVal ?? today)); + }; + + /** + * Set the displayed month and handle side effects + */ + const setMonth = useCallback((newMonth: Date) => { + _setMonth(newMonth); + }, []); + + /** + * Set the `highlight` value & handle side effects + */ + const setHighlight = useCallback((newHighlight: DateType) => { + _setHighlight(newHighlight); + }, []); + + /** + * Opens the menu and handles side effects + */ + const openMenu = () => { + setOpen(true); + }; + + /** Closes the menu and handles side effects */ + const closeMenu = () => { + setOpen(false); + + // Perform side effects once the state has settle + requestAnimationFrame(() => { + // Return focus to the calendar button + refs.calendarButtonRef.current?.focus(); + // update month to something valid + setMonth(getFirstOfMonth(value ?? today)); + // update highlight to something valid + setHighlight(getInitialHighlight(value, today)); + }); + }; + + /** Toggles the menu and handles appropriate side effects */ + const toggleMenu = () => { + if (isOpen) { + closeMenu(); + } else { + openMenu(); + } + }; + + /*********** + * GETTERS * + ***********/ + + /** + * Returns the cell element with the provided value + */ + const getCellWithValue = (date: DateType): HTMLTableCellElement | null => { + const highlightKey = getISODate(date); + const cell = highlightKey + ? refs.calendarCellRefs(highlightKey)?.current + : null; + return cell; + }; + + /** + * Returns the cell element with the current highlight value + */ + const getHighlightedCell = () => { + return getCellWithValue(highlight); + }; + + /**************** + * SIDE EFFECTS * + ****************/ + + /** + * If `value` prop changes, update the month + */ + useEffect(() => { + if (!isSameUTCDay(value, prevValue)) { + setMonth(getFirstOfMonth(value ?? today)); + } + }, [prevValue, setMonth, today, value]); + + return ( + + {children} + + ); +}; + +/** + * Access single date picker context values + */ +export const useSingleDateContext = () => { + return useContext(SingleDateContext); +}; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts new file mode 100644 index 0000000000..b4925b6a72 --- /dev/null +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts @@ -0,0 +1,90 @@ +import { DynamicRefGetter } from '@leafygreen-ui/hooks'; + +import { DateType, SegmentRefs } from '../../shared'; +import { DatePickerProps } from '../DatePicker.types'; + +export interface DatePickerComponentRefs { + segmentRefs: SegmentRefs; + calendarCellRefs: DynamicRefGetter; + calendarButtonRef: React.RefObject; +} + +export interface SingleDateContextProps { + /** + * Ref objects for important date picker elements + */ + refs: DatePickerComponentRefs; + + /** The current value of the date picker */ + value: DateType | undefined; + + /** + * Dispatches a setter for the date picker value. + * Performs common side-effects + */ + setValue: (newVal: DateType | undefined) => void; + + /** + * Calls the `handleValidation` function provided by the consumer + */ + handleValidation: DatePickerProps['handleValidation']; + + /** + * The current date, at UTC midnight + */ + today: Date; + + /** + * The currently displayed month in the menu + */ + month: Date; + + /** + * Sets the current month in the menu, + * and performs any side-effects + */ + setMonth: (newMonth: Date) => void; + + /** + * The Date value for the calendar cell in the menu that has, or should have focus + */ + highlight: DateType; + + /** + * Sets the value of the calendar cell that should have focus, + * and performs any side-effects + */ + setHighlight: (newHighlight: DateType) => void; + + /** + * Opens the menu and handles side effects + */ + openMenu: () => void; + + /** + * Closes the menu and handles side effects + */ + closeMenu: () => void; + + /** + * Toggles the menu and handles appropriate side effects + */ + toggleMenu: () => void; + + /** + * Returns the calendar cell element that has, or should have focus + */ + getHighlightedCell: () => HTMLTableCellElement | null | undefined; + + /** + * Returns the calendar cell with the provided value + */ + getCellWithValue: (date: DateType) => HTMLTableCellElement | null | undefined; +} + +/** Props passed into the provider component */ +export interface SingleDateProviderProps { + value: DateType | undefined; + setValue: (newVal: DateType) => void; + handleValidation?: DatePickerProps['handleValidation']; +} diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/index.ts b/packages/date-picker/src/DatePicker/SingleDateContext/index.ts new file mode 100644 index 0000000000..ca13524a7e --- /dev/null +++ b/packages/date-picker/src/DatePicker/SingleDateContext/index.ts @@ -0,0 +1,5 @@ +export { SingleDateProvider, useSingleDateContext } from './SingleDateContext'; +export { + type SingleDateContextProps, + type SingleDateProviderProps, +} from './SingleDateContext.types'; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts b/packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts new file mode 100644 index 0000000000..0e61b23268 --- /dev/null +++ b/packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts @@ -0,0 +1,25 @@ +import { useRef } from 'react'; + +import { useDynamicRefs } from '@leafygreen-ui/hooks'; + +import { useSegmentRefs } from '../../shared/hooks'; + +import { DatePickerComponentRefs } from './SingleDateContext.types'; + +/** Creates `ref` objects for any & all relevant component elements */ +export const useDateRangeComponentRefs = (): DatePickerComponentRefs => { + const segmentRefs = useSegmentRefs(); + + // TODO: https://jira.mongodb.org/browse/LG-3666 + // useDynamicRefs may overflow if a user navigates to too many months. + // consider purging the refs map within the hook + const calendarCellRefs = useDynamicRefs(); + + const calendarButtonRef = useRef(null); + + return { + segmentRefs, + calendarCellRefs, + calendarButtonRef, + }; +}; diff --git a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts new file mode 100644 index 0000000000..09e9c278b3 --- /dev/null +++ b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts @@ -0,0 +1,18 @@ +import { Month, newUTC } from '../../../shared'; + +import { getInitialHighlight } from '.'; + +describe('packages/date-picker/utils/getInitialHighlight', () => { + const value = newUTC(2023, Month.September, 10); + const today = newUTC(2023, Month.December, 26); + + test('returns `value` when provided', () => { + const highlight = getInitialHighlight(value, today); + expect(highlight).toBe(value); + }); + + test('returns `today` if no value is provided', () => { + const highlight = getInitialHighlight(null, today); + expect(highlight).toBe(today); + }); +}); diff --git a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts new file mode 100644 index 0000000000..56f453f6d0 --- /dev/null +++ b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts @@ -0,0 +1,10 @@ +import { DateType, isValidDate } from '../../../shared'; + +/** Returns the initial highlight value when the date picker is opened */ +export const getInitialHighlight = ( + value: DateType | undefined, + today: Date, +) => { + if (isValidDate(value)) return value; + return today; +}; diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx index eb1292d296..1d6ad1cb90 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/getRelativeSegment.spec.tsx @@ -14,15 +14,26 @@ const renderTestComponent = () => { const result = render( <> - - - + + + , ); + const elements = { + day: result.getByTestId('day'), + month: result.getByTestId('month'), + year: result.getByTestId('year'), + } as { + day: HTMLInputElement; + month: HTMLInputElement; + year: HTMLInputElement; + }; + return { ...result, segmentRefs, + elements, }; }; @@ -35,67 +46,141 @@ describe('packages/date-picker/utils/getRelativeSegment', () => { { type: 'day', value: '31' }, ]; - let segmentRefs: SegmentRefs; - beforeEach(() => { - segmentRefs = renderTestComponent().segmentRefs; - }); + describe('from ref', () => { + let segmentRefs: SegmentRefs; + beforeEach(() => { + segmentRefs = renderTestComponent().segmentRefs; + }); + test('next from year => month', () => { + expect( + getRelativeSegment('next', { + segment: segmentRefs.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); + test('next from month => day', () => { + expect( + getRelativeSegment('next', { + segment: segmentRefs.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); - test('next from year => month', () => { - expect( - getRelativeSegment('next', { - segment: segmentRefs.year, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.month); - }); - test('next from month => day', () => { - expect( - getRelativeSegment('next', { - segment: segmentRefs.month, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.day); - }); + test('prev from day => month', () => { + expect( + getRelativeSegment('prev', { + segment: segmentRefs.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); - test('prev from day => month', () => { - expect( - getRelativeSegment('prev', { - segment: segmentRefs.day, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.month); - }); + test('prev from month => year', () => { + expect( + getRelativeSegment('prev', { + segment: segmentRefs.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); - test('prev from month => year', () => { - expect( - getRelativeSegment('prev', { - segment: segmentRefs.month, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.year); - }); + test('first = year', () => { + expect( + getRelativeSegment('first', { + segment: segmentRefs.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); - test('first = year', () => { - expect( - getRelativeSegment('first', { - segment: segmentRefs.day, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.year); + test('last = day', () => { + expect( + getRelativeSegment('last', { + segment: segmentRefs.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); }); - test('last = day', () => { - expect( - getRelativeSegment('last', { - segment: segmentRefs.year, - formatParts, - segmentRefs, - }), - ).toBe(segmentRefs.day); + describe('from element', () => { + let segmentRefs: SegmentRefs; + + let elements: { + day: HTMLInputElement; + month: HTMLInputElement; + year: HTMLInputElement; + }; + beforeEach(() => { + const result = renderTestComponent(); + segmentRefs = result.segmentRefs; + elements = result.elements; + }); + test('next from year => month', () => { + expect( + getRelativeSegment('next', { + segment: elements.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); + test('next from month => day', () => { + expect( + getRelativeSegment('next', { + segment: elements.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); + + test('prev from day => month', () => { + expect( + getRelativeSegment('prev', { + segment: elements.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.month); + }); + + test('prev from month => year', () => { + expect( + getRelativeSegment('prev', { + segment: elements.month, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); + + test('first = year', () => { + expect( + getRelativeSegment('first', { + segment: elements.day, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.year); + }); + + test('last = day', () => { + expect( + getRelativeSegment('last', { + segment: elements.year, + formatParts, + segmentRefs, + }), + ).toBe(segmentRefs.day); + }); }); }); diff --git a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts index 0800258a81..44948a6063 100644 --- a/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getRelativeSegment/index.ts @@ -35,7 +35,12 @@ export const getRelativeSegment = ( /** The index of the reference segment relative to formatParts */ const currentSegmentIndex: number | undefined = formatSegments.findIndex( - segmentName => segmentRefs[segmentName] === segment, + segmentName => { + return ( + segmentRefs[segmentName] === segment || + segmentRefs[segmentName].current === segment + ); + }, ); const getRefAtIndex = (index: number) => { @@ -61,7 +66,7 @@ export const getRelativeSegment = ( } case 'next': { - if (!isUndefined(currentSegmentIndex)) { + if (!isUndefined(currentSegmentIndex) && currentSegmentIndex >= 0) { const nextSegmentIndex = Math.min( currentSegmentIndex + 1, formatSegments.length - 1, @@ -75,7 +80,7 @@ export const getRelativeSegment = ( } case 'prev': { - if (!isUndefined(currentSegmentIndex)) { + if (!isUndefined(currentSegmentIndex) && currentSegmentIndex >= 0) { const prevSegmentIndex = Math.max(currentSegmentIndex - 1, 0); const prevSegmentRef = getRefAtIndex(prevSegmentIndex); diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx index 123b27bfb5..c5bb886c14 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.spec.tsx @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event'; import { CalendarCell, CalendarCellState } from '.'; +/** Ensures valid DOM nesting when testing CalendarCells */ const TestCellWrapper = ({ children }: PropsWithChildren) => ( @@ -13,30 +14,32 @@ const TestCellWrapper = ({ children }: PropsWithChildren) => ( ); describe('packages/date-picker/shared/calendar-cell', () => { - test('has `gridcell` role', () => { - const { queryByRole, getByTestId } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - expect(gridcell).toBeInTheDocument(); - expect(getByTestId('tr').firstChild).toEqual(gridcell); - }); + describe('Rendering', () => { + test('has `gridcell` role', () => { + const { queryByRole, getByTestId } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + expect(gridcell).toBeInTheDocument(); + expect(getByTestId('tr').firstChild).toEqual(gridcell); + }); - test('renders as aria-disabled', () => { - const clickHandler = jest.fn(); - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - expect(gridcell).toHaveAttribute('aria-disabled', 'true'); + test('renders as aria-disabled', () => { + const clickHandler = jest.fn(); + const { queryByRole } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + expect(gridcell).toHaveAttribute('aria-disabled', 'true'); + }); }); test('triggers click handler on click', () => { @@ -51,67 +54,65 @@ describe('packages/date-picker/shared/calendar-cell', () => { expect(clickHandler).toHaveBeenCalled(); }); - test('triggers click handler on enter', () => { - const clickHandler = jest.fn(); - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - gridcell!.focus(); - userEvent.keyboard('[Enter]'); - expect(clickHandler).toHaveBeenCalled(); - }); + describe('Interaction', () => { + const stateCases = [CalendarCellState.Default, CalendarCellState.Active]; + describe.each(stateCases)('when cell is in %p state', state => { + const keypressCases = ['Enter', 'Space']; + test.each(keypressCases)('%p key triggers click handler', key => { + const clickHandler = jest.fn(); + const { queryByRole } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + gridcell!.focus(); + userEvent.keyboard(`[${key}]`); + expect(clickHandler).toHaveBeenCalled(); + }); + }); - test('triggers click handler on space', () => { - const clickHandler = jest.fn(); - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - gridcell!.focus(); - userEvent.keyboard('{space}'); - expect(clickHandler).toHaveBeenCalled(); - }); + test('Does not fire click handler when disabled', () => { + const clickHandler = jest.fn(); + const { queryByRole } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); + expect(clickHandler).not.toHaveBeenCalled(); + }); - test('Does not fire click handler when disabled', () => { - const clickHandler = jest.fn(); - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); - expect(clickHandler).not.toHaveBeenCalled(); - }); + test('is focusable when highlighted', () => { + const { queryByRole } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); + expect(gridcell).toHaveFocus(); + }); - test('is focusable when highlighted', () => { - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); - expect(gridcell).toHaveFocus(); - }); - - test('is not focusable when disabled', () => { - const { queryByRole } = render( - - - , - ); - const gridcell = queryByRole('gridcell'); - userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); - expect(gridcell).not.toHaveFocus(); + test('is not focusable when disabled', () => { + const { queryByRole } = render( + + + , + ); + const gridcell = queryByRole('gridcell'); + userEvent.click(gridcell!, {}, { skipPointerEventsCheck: true }); + expect(gridcell).not.toHaveFocus(); + }); }); }); diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts index 421b497292..40e0717b6f 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts +++ b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.styles.ts @@ -5,7 +5,6 @@ import { fontFamilies, fontWeights, spacing, - transitionDuration, typeScales, } from '@leafygreen-ui/tokens'; @@ -201,7 +200,7 @@ export const calendarCellRangeStyles: Record< /** * Highlighted / Focus styles */ -const highlightSelector = '&:focus-visible, &[data-highlight="true"]'; // using a data selector lets us easily test these states +const highlightSelector = '&:focus, &[data-highlight="true"]'; // using a data selector lets us easily test these states export const calendarCellHighlightStyles: Record = { [Theme.Light]: css` @@ -210,7 +209,6 @@ export const calendarCellHighlightStyles: Record = { & > .${indicatorClassName} { box-shadow: ${calendarCellFocusRing.light}; - transition: ease-in-out ${transitionDuration.default}ms box-shadow; } } `, @@ -219,8 +217,6 @@ export const calendarCellHighlightStyles: Record = { outline: none; & > .${indicatorClassName} { - transition: ease-in-out ${transitionDuration.default}ms box-shadow; - box-shadow: ${calendarCellFocusRing.dark}; } } diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index e00c7f61bc..195cec540d 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -6,7 +6,7 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { Month } from '../../../constants'; -import { isTodayTZ, newUTC } from '../../../utils'; +import { getISODate, isTodayTZ, newUTC } from '../../../utils'; import { Locales, TimeZones } from '../../../utils/testutils'; import { DatePickerContextProps, @@ -19,11 +19,7 @@ import { CalendarGrid } from './CalendarGrid'; const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - + @@ -83,9 +79,9 @@ export const Demo: StoryFn = ({ ...props }) => { aria-label="test" key={i} isCurrent={isTodayTZ(day, timeZone)} - isHighlighted={hovered ? hovered === day?.toISOString() : false} - onMouseEnter={handleHover(day?.toISOString())} - data-iso={day?.toISOString()} + isHighlighted={hovered ? hovered === getISODate(day) : false} + onMouseEnter={handleHover(getISODate(day))} + data-iso={getISODate(day)} > {day?.getUTCDate()} diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx index d7540c2d24..06a2194fa0 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx @@ -85,14 +85,14 @@ export default meta; const Template: StoryFn = () => { return ( - + ( ( - { children, onInputClick, onIconButtonClick, ...rest }: DateFormFieldProps, + { + children, + onInputClick, + onIconButtonClick, + buttonRef, + ...rest + }: DateFormFieldProps, fwdRef, ) => { const { @@ -47,7 +53,9 @@ export const DateFormField = React.forwardRef< aria-expanded={isOpen} aria-controls={menuId} onClick={onInputClick} - contentEnd={} + contentEnd={ + + } > {children} diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts index e1c4414330..97090026eb 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.types.ts @@ -9,4 +9,7 @@ export type DateFormFieldProps = HTMLElementProps<'div'> & { onInputClick?: MouseEventHandler; /** Fired then the calendar icon button is clicked */ onIconButtonClick?: MouseEventHandler; + + /** A ref for the content end button */ + buttonRef: React.RefObject; }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index 90e5ac653d..09a8dbb025 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -25,7 +25,7 @@ const renderDateInputBox = ( }; const result = render( - + { - const testContext = { + const testContext: Partial = { dateFormat: 'iso8601', timeZone: 'UTC', }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 6349f2350c..1a43f18582 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -19,11 +19,7 @@ const testDate = newUTC(1993, Month.December, 26); const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - + diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx index c42b47eaea..6059dbaa38 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx @@ -17,11 +17,15 @@ export const DatePickerContext = createContext( defaultDatePickerContext, ); +// TODO: Consider renaming this to `SharedDatePickerContext`, +// and use `DatePickerContext` for what's currently `SingleDateContext` + /** The Provider component for DatePickerContext */ export const DatePickerProvider = ({ children, - value: { initialOpen, ...rest }, -}: PropsWithChildren<{ value: DatePickerProviderProps }>) => { + initialOpen, + ...rest +}: PropsWithChildren) => { const [isOpen, setOpen] = useState(initialOpen ?? false); const [isDirty, setIsDirty] = useState(false); const menuId = useIdAllocator({ prefix: 'lg-date-picker-menu' }); diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts index 2e56146a7b..db6b48a70c 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts @@ -31,7 +31,11 @@ export interface DatePickerContextProps /** Whether the menu is open */ isOpen: boolean; - /** Setter to open or close the menu */ + /** + * Setter to open or close the menu + * @internal - Prefer using `open/close/toggleMenu` + * from single/range component context + */ setOpen: React.Dispatch>; /** Identifies whether the component has been interacted with */ diff --git a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx index cb78742cc5..35a32737a6 100644 --- a/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx +++ b/packages/date-picker/src/shared/components/MenuWrapper/MenuWrapper.tsx @@ -10,17 +10,13 @@ import Popover, { PopoverProps } from '@leafygreen-ui/popover'; import { menuStyles } from './MenuWrapper.styles'; +export type MenuWrapperProps = PopoverProps & HTMLElementProps<'div'>; + /** * A simple styled popover component */ -export const MenuWrapper = forwardRef< - HTMLDivElement, - PopoverProps & HTMLElementProps<'div'> ->( - ( - { className, children, ...props }: PopoverProps & HTMLElementProps<'div'>, - fwdRef, - ) => { +export const MenuWrapper = forwardRef( + ({ className, children, ...props }: MenuWrapperProps, fwdRef) => { const { theme } = useDarkMode(); return ( diff --git a/packages/date-picker/src/shared/components/MenuWrapper/index.ts b/packages/date-picker/src/shared/components/MenuWrapper/index.ts index 018de6fce3..792d4a3521 100644 --- a/packages/date-picker/src/shared/components/MenuWrapper/index.ts +++ b/packages/date-picker/src/shared/components/MenuWrapper/index.ts @@ -1 +1,2 @@ export { MenuWrapper } from './MenuWrapper'; +export { type MenuWrapperProps } from './MenuWrapper'; diff --git a/packages/date-picker/src/shared/components/index.ts b/packages/date-picker/src/shared/components/index.ts index 009742c5d1..9adeddaeb3 100644 --- a/packages/date-picker/src/shared/components/index.ts +++ b/packages/date-picker/src/shared/components/index.ts @@ -15,4 +15,4 @@ export { defaultDatePickerContext, useDatePickerContext, } from './DatePickerContext'; -export { MenuWrapper } from './MenuWrapper'; +export { MenuWrapper, type MenuWrapperProps } from './MenuWrapper'; diff --git a/packages/date-picker/src/shared/types.ts b/packages/date-picker/src/shared/types.ts index c569cbf4b8..aa6e9ba254 100644 --- a/packages/date-picker/src/shared/types.ts +++ b/packages/date-picker/src/shared/types.ts @@ -36,7 +36,7 @@ export interface BaseDatePickerProps extends DarkModeProps { * * @default 'iso8601' */ - dateFormat?: 'en-US' | 'en-UK' | 'iso8601' | `${string}-${string}`; + dateFormat?: 'iso8601' | `${string}-${string}`; /** * A valid IANA timezone string, or UTC offset. diff --git a/packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts b/packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts new file mode 100644 index 0000000000..deb7189971 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts @@ -0,0 +1,14 @@ +import { Month } from '../../constants'; +import { newUTC } from '../newUTC'; + +import { getISODate } from '.'; + +describe('packages/date-picker/utils/getISODate', () => { + test('returns an ISO Date string', () => { + const d1 = newUTC(2023, Month.July, 10); + expect(getISODate(d1)).toEqual('2023-07-10'); + + const d2 = new Date('2023-11-15T02:00:00.000Z'); + expect(getISODate(d2)).toEqual('2023-11-15'); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getISODate/index.ts b/packages/date-picker/src/shared/utils/getISODate/index.ts new file mode 100644 index 0000000000..6872036238 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getISODate/index.ts @@ -0,0 +1,14 @@ +import { DateType } from '../../types'; +import { isValidDate } from '../isValidDate'; + +/** + * Returns only the Date portion of the ISOString for a given date + * i.e. 2023-11-01T00:00:00.000Z => 2023-11-01 + */ +export const getISODate = (date?: DateType): string => { + if (!isValidDate(date)) return ''; + + const isoString = date.toISOString(); + const isoDate = isoString.split('T')[0]; + return isoDate; +}; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index a60707eb9a..202e6bd1f4 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -6,6 +6,7 @@ export { getFirstEmptySegment } from './getFirstEmptySegment'; export { getFirstOfMonth } from './getFirstOfMonth'; export { getFormatParts } from './getFormatParts'; export { getFullMonthLabel } from './getFullMonthLabel'; +export { getISODate } from './getISODate'; export { getLastOfMonth } from './getLastOfMonth'; export { getMonthName } from './getMonthName'; export { getRemainingParts } from './getRemainingParts'; From 4c810e2ee963ef5591adc58deb6987183e372436 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 6 Nov 2023 14:56:12 -0500 Subject: [PATCH 266/351] Fire transitionEnd in Tab tests --- .../src/DatePicker/DatePicker.spec.tsx | 21 +++++++------------ .../DatePickerComponent.tsx | 4 ++-- .../DatePickerMenu/DatePickerMenu.tsx | 5 ----- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 3a8a4836cd..8992b07336 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -558,7 +558,7 @@ describe('packages/date-picker', () => { const element = elementMap[label]; if (element !== null) { - await waitFor(() => expect(element).toHaveFocus()); + expect(element).toHaveFocus(); } else { expect( renderResult.inputContainer.contains( @@ -568,6 +568,9 @@ describe('packages/date-picker', () => { } userEvent.tab(); + // There are side-effects triggered on CSS transition-end events. + // Fire this event here to ensure these side-effects don't impact Tab order + if (element) fireEvent.transitionEnd(element); } }); }); @@ -730,12 +733,8 @@ describe('packages/date-picker', () => { }); describe('Menu', () => { test('left arrow moves focus to the previous day', async () => { - const { calendarButton, findMenuElements } = renderDatePicker(); - userEvent.click(calendarButton); - const { todayCell, menuContainerEl, queryCellByDate } = - await findMenuElements(); - // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM - fireEvent.transitionEnd(menuContainerEl!); + const { openMenu } = renderDatePicker(); + const { todayCell, queryCellByDate } = await openMenu(); expect(todayCell).toHaveFocus(); userEvent.keyboard('{arrowleft}'); @@ -744,12 +743,8 @@ describe('packages/date-picker', () => { }); test('down arrow moves focus to next week', async () => { - const { calendarButton, findMenuElements } = renderDatePicker(); - userEvent.click(calendarButton); - const { todayCell, menuContainerEl, queryCellByDate } = - await findMenuElements(); - // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM - fireEvent.transitionEnd(menuContainerEl!); + const { openMenu } = renderDatePicker(); + const { todayCell, queryCellByDate } = await openMenu(); expect(todayCell).toHaveFocus(); userEvent.keyboard('{arrowdown}'); diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index 133c48df68..d5b69428cc 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -30,8 +30,8 @@ export const DatePickerComponent = forwardRef< useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen); /** Fired when the CSS transition to open the menu is fired */ - const handleMenuTransitionEntered: TransitionEventHandler = () => { - if (isOpen) { + const handleMenuTransitionEntered: TransitionEventHandler = e => { + if (isOpen && e.target === menuRef.current) { // When the menu opens, set focus to the `highlight` cell const highlightedCell = getHighlightedCell(); highlightedCell?.focus(); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index b7cdd22804..63a277c642 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -204,11 +204,6 @@ export const DatePickerMenu = forwardRef( break; } - // The isInRange check below prevents tab presses from propagating up so we add a switch case for tab presses where we can then call handleWrapperTabKeyPress which will handle trapping focus - case keyMap.Tab: - handleWrapperTabKeyPress(e); - break; - default: break; } From d371e7e9ad875625c92999a6ab9aa013a7894c70 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 6 Nov 2023 15:54:52 -0500 Subject: [PATCH 267/351] update how we test month change highlight --- .../src/DatePicker/DatePicker.spec.tsx | 71 ++++++++++--------- .../DatePickerMenu/DatePickerMenu.tsx | 17 ++--- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 8992b07336..780f3fca95 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -327,7 +327,12 @@ describe('packages/date-picker', () => { expect(yearSelect).toHaveValue('2022'); }); - test.todo('does not move focus to the calendar cell'); + test('keeps focus on chevron button', async () => { + const { openMenu } = renderDatePicker(); + const { leftChevron } = await openMenu(); + userEvent.click(leftChevron!); + expect(leftChevron).toHaveFocus(); + }); }); describe('Right', () => { @@ -458,37 +463,6 @@ describe('packages/date-picker', () => { }); }); - describe('Changing the month', () => { - test.todo('is announced in an aria-live region'); - - describe('updates the highlighted cell', () => { - test('to the end of the month if we went backwards', async () => { - const { openMenu, findAllByRole } = renderDatePicker({ - value: newUTC(2023, Month.July, 5), - }); - const { monthSelect, queryCellByDate } = await openMenu(); - userEvent.click(monthSelect!); - const options = await findAllByRole('option'); - const Jan = options[0]; - userEvent.click(Jan); - const jan31Cell = queryCellByDate(newUTC(2023, Month.January, 31)); - await waitFor(() => expect(jan31Cell).toHaveFocus()); - }); - test('to the beginning of the month if we went forwards', async () => { - const { openMenu, findAllByRole } = renderDatePicker({ - value: newUTC(2023, Month.July, 5), - }); - const { monthSelect, queryCellByDate } = await openMenu(); - userEvent.click(monthSelect!); - const options = await findAllByRole('option'); - const Dec = options[11]; - userEvent.click(Dec); - const dec1Cell = queryCellByDate(newUTC(2023, Month.December, 1)); - await waitFor(() => expect(dec1Cell).toHaveFocus()); - }); - }); - }); - describe('Keyboard navigation', () => { describe('Tab', () => { test('menu does not open on keyboard focus', async () => { @@ -1035,6 +1009,39 @@ describe('packages/date-picker', () => { await waitFor(() => expect(valueCell).toHaveFocus()); }); }); + + describe('Changing the month', () => { + test.todo('is announced in an aria-live region'); + + describe('updates the highlighted cell...', () => { + test('to the end of the month if we went backwards', async () => { + const { openMenu, findAllByRole } = renderDatePicker({ + value: newUTC(2023, Month.July, 5), + }); + const { monthSelect, queryCellByDate } = await openMenu(); + userEvent.click(monthSelect!); + const options = await findAllByRole('option'); + const Jan = options[0]; + userEvent.click(Jan); + tabNTimes(3); + const jan31Cell = queryCellByDate(newUTC(2023, Month.January, 31)); + await waitFor(() => expect(jan31Cell).toHaveFocus()); + }); + test('to the beginning of the month if we went forwards', async () => { + const { openMenu, findAllByRole } = renderDatePicker({ + value: newUTC(2023, Month.July, 5), + }); + const { monthSelect, queryCellByDate } = await openMenu(); + userEvent.click(monthSelect!); + const options = await findAllByRole('option'); + const Dec = options[11]; + userEvent.click(Dec); + tabNTimes(3); + const dec1Cell = queryCellByDate(newUTC(2023, Month.December, 1)); + await waitFor(() => expect(dec1Cell).toHaveFocus()); + }); + }); + }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 63a277c642..675317ebe1 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -69,17 +69,6 @@ export const DatePickerMenu = forwardRef( const monthLabel = getFullMonthLabel(month); - // focuses the DOM element for the appropriate cell - const focusCellWithDate = useCallback( - (date: DateType) => { - requestAnimationFrame(() => { - const highlightedCell = getCellWithValue(date); - highlightedCell?.focus(); - }); - }, - [getCellWithValue], - ); - /** setDisplayMonth with side effects */ const updateMonth = (newMonth: Date) => { if (isSameUTCMonth(newMonth, month)) { @@ -91,7 +80,6 @@ export const DatePickerMenu = forwardRef( if (newHighlight && shouldUpdateHighlight) { setHighlight(newHighlight); - focusCellWithDate(newHighlight); } }; @@ -102,7 +90,10 @@ export const DatePickerMenu = forwardRef( setDisplayMonth(newHighlight); } setHighlight(newHighlight); - focusCellWithDate(newHighlight); + requestAnimationFrame(() => { + const highlightedCell = getCellWithValue(newHighlight); + highlightedCell?.focus(); + }); }; /** From 871c25998ca126e2bd4f17e90fca466ee3dfc785 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 6 Nov 2023 16:04:08 -0500 Subject: [PATCH 268/351] ts fix --- .../src/DatePicker/DatePickerMenu/DatePickerMenu.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 675317ebe1..db7b46092f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -1,7 +1,6 @@ import React, { forwardRef, KeyboardEventHandler, - useCallback, useEffect, useMemo, useRef, @@ -12,7 +11,6 @@ import { useForwardedRef, usePrevious } from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; import { spacing } from '@leafygreen-ui/tokens'; -import { DateType } from '../../shared'; import { CalendarCell, CalendarCellState, From dafb47065eef18c35a05293314a1840135ddf72c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Tue, 7 Nov 2023 11:23:19 -0500 Subject: [PATCH 269/351] LG-3740, LG-3741: DP initialOpen + disabled (#2069) * updates tab order tests * test default focus on menu open * SingleDateContext * converts tests & stories to use context * add refs to context * focus highlighted cell on cal button click * updates highlight cell focus logic * fixes failing tests * update context props api * fix tests & stories - provider api * Adds new workflow tests * set focus on transition end * updates tab order tests * test default focus on menu open * SingleDateContext * converts tests & stories to use context * add refs to context * focus highlighted cell on cal button click * updates highlight cell focus logic * fixes failing tests * update context props api * fix tests & stories - provider api * Adds new workflow tests * set focus on transition end * fix async tests. Move esc key handling. * WIP * update formfield disabled icon color * update types * update types def * update types def * use fireEvent.transitionEnd in tests. * setMonth on value change * update to use async findMenuElements * rename to queryCellByDate * add test * add another test * adds waitForMenuToOpen * adds `onExited` handler fired when menu closed * fix conflicts * update tests * test name update * remove unused stuff * move disabled logic * changeset * update test * update const name * address comments * revert changes --------- Co-authored-by: Adam Thompson --- .changeset/rare-apples-deny.md | 5 +++ .../src/DatePicker/DatePicker.spec.tsx | 39 ++++++++++++++++++- .../DatePickerComponent.tsx | 11 +++++- .../DatePickerContext/DatePickerContext.tsx | 16 ++++++-- packages/date-picker/src/shared/types.ts | 4 +- .../FormFieldInputContainer.styles.ts | 10 +++++ 6 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 .changeset/rare-apples-deny.md diff --git a/.changeset/rare-apples-deny.md b/.changeset/rare-apples-deny.md new file mode 100644 index 0000000000..9690172af5 --- /dev/null +++ b/.changeset/rare-apples-deny.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/form-field': patch +--- + +Updates disabled icon colors diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 780f3fca95..310824f701 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -105,7 +105,16 @@ describe('packages/date-picker', () => { test('menu is initially open when rendered with `initialOpen`', async () => { const { findMenuElements } = renderDatePicker({ initialOpen: true }); const { menuContainerEl } = await findMenuElements(); - await waitFor(() => expect(menuContainerEl).toBeInTheDocument()); + expect(menuContainerEl).toBeInTheDocument(); + }); + + test('menu is initially closed when rendered with `initialOpen` and `disabled`', async () => { + const { findMenuElements } = renderDatePicker({ + initialOpen: true, + disabled: true, + }); + const { menuContainerEl } = await findMenuElements(); + expect(menuContainerEl).not.toBeInTheDocument(); }); test('if no value is set, menu opens to current month', async () => { @@ -134,6 +143,34 @@ describe('packages/date-picker', () => { expect(calendarCells).toHaveLength(29); }); + describe('when disabled is toggled to true', () => { + test('menu closes', async () => { + const { findMenuElements, rerenderDatePicker } = renderDatePicker({ + initialOpen: true, + }); + const { menuContainerEl } = await findMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + rerenderDatePicker({ disabled: true }); + await waitFor(() => { + expect(menuContainerEl).not.toBeInTheDocument(); + }); + }); + + test('validation handler fires', async () => { + const handleValidation = jest.fn(); + const { findMenuElements, rerenderDatePicker } = renderDatePicker({ + initialOpen: true, + handleValidation, + }); + const { menuContainerEl } = await findMenuElements(); + expect(menuContainerEl).toBeInTheDocument(); + rerenderDatePicker({ disabled: true }); + await waitFor(() => { + expect(handleValidation).toHaveBeenCalled(); + }); + }); + }); + describe('Chevrons', () => { test('Left is disabled if prev. month is entirely out of range', async () => { const { openMenu } = renderDatePicker({ diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index d5b69428cc..32c64a404f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, KeyboardEventHandler, TransitionEventHandler, + useEffect, useRef, } from 'react'; import { ExitHandler } from 'react-transition-group/Transition'; @@ -20,7 +21,7 @@ export const DatePickerComponent = forwardRef< HTMLDivElement, DatePickerComponentProps >(({ ...rest }: DatePickerComponentProps, fwdRef) => { - const { isOpen, menuId } = useDatePickerContext(); + const { isOpen, menuId, disabled } = useDatePickerContext(); const { value, closeMenu, handleValidation, getHighlightedCell } = useSingleDateContext(); @@ -29,6 +30,14 @@ export const DatePickerComponent = forwardRef< useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen); + /** This listens to when the disabled prop changes to true and closes the menu */ + useEffect(() => { + if (disabled) { + closeMenu(); + handleValidation?.(value); + } + }, [closeMenu, disabled, handleValidation, value]); + /** Fired when the CSS transition to open the menu is fired */ const handleMenuTransitionEntered: TransitionEventHandler = e => { if (isOpen && e.target === menuRef.current) { diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx index 6059dbaa38..bb90f06ed6 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx @@ -23,17 +23,27 @@ export const DatePickerContext = createContext( /** The Provider component for DatePickerContext */ export const DatePickerProvider = ({ children, - initialOpen, + initialOpen = false, + disabled = false, ...rest }: PropsWithChildren) => { - const [isOpen, setOpen] = useState(initialOpen ?? false); + const isInitiallyOpen = disabled ? false : initialOpen; + const [isOpen, setOpen] = useState(isInitiallyOpen); const [isDirty, setIsDirty] = useState(false); const menuId = useIdAllocator({ prefix: 'lg-date-picker-menu' }); const contextValue = getContextProps(rest); return ( {children} diff --git a/packages/date-picker/src/shared/types.ts b/packages/date-picker/src/shared/types.ts index aa6e9ba254..5037e3d4a0 100644 --- a/packages/date-picker/src/shared/types.ts +++ b/packages/date-picker/src/shared/types.ts @@ -59,7 +59,7 @@ export interface BaseDatePickerProps extends DarkModeProps { baseFontSize?: BaseFontSize; /** - * Whether the input is disabled. Note: will not set the `disabled` attribute on an input + * Whether the input is disabled. Note: will not set the `disabled` attribute on an input and the calendar menu will not open if disabled is set to true. */ disabled?: boolean; @@ -76,6 +76,6 @@ export interface BaseDatePickerProps extends DarkModeProps { */ errorMessage?: string; - /** Whether the calendar menu is initially open */ + /** Whether the calendar menu is initially open. Note: The calendar menu will not open if disabled is set to true. */ initialOpen?: boolean; } diff --git a/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts index b247dd0a77..af9aa52c8c 100644 --- a/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts +++ b/packages/form-field/src/FormFieldInputContainer/FormFieldInputContainer.styles.ts @@ -332,9 +332,19 @@ export const iconsWrapperStyles = (size: Size) => css` export const iconStyles: Record = { [Theme.Light]: css` color: ${palette.gray.base}; + + &[aria-disabled='true'], + &:disabled { + color: ${palette.gray.light1}; + } `, [Theme.Dark]: css` color: ${palette.gray.base}; + + &[aria-disabled='true'], + &:disabled { + color: ${palette.gray.dark2}; + } `, }; From fc5a4b2910751b146ab891a3106122b2ab62a71e Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:31:42 -0500 Subject: [PATCH 270/351] DatePicker [LG-3748]: Restricts month menu options (#2070) * Restricts month menu options * adds no disabled option test * fix deps --- .changeset/rude-tomatoes-hang.md | 5 + .../DatePickerMenu/DatePickerMenu.tsx | 8 +- .../DatePickerMenuHeader.spec.tsx | 130 ++++++++++++++++++ .../{index.tsx => DatePickerMenuHeader.tsx} | 29 +++- .../DatePickerMenuHeader/index.ts | 1 + .../getMonthOptions/getMonthOptions.spec.ts | 89 ++++++++++++ .../utils/getMonthOptions/index.ts | 39 ++++++ .../SingleDateContext/SingleDateContext.tsx | 2 +- .../src/DatePicker/SingleDateContext/index.ts | 6 +- packages/date-picker/src/shared/constants.ts | 43 ++++-- .../hooks/useSegmentRefs/segmentRefs.types.ts | 2 +- .../src/shared/utils/getMonthIndex/index.ts | 7 + .../src/shared/utils/getMonthName/index.ts | 7 +- .../date-picker/src/shared/utils/index.ts | 1 + .../src/InputOption/InputOption.tsx | 1 + 15 files changed, 340 insertions(+), 30 deletions(-) create mode 100644 .changeset/rude-tomatoes-hang.md create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx rename packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/{index.tsx => DatePickerMenuHeader.tsx} (85%) create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.ts create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/getMonthOptions.spec.ts create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/index.ts create mode 100644 packages/date-picker/src/shared/utils/getMonthIndex/index.ts diff --git a/.changeset/rude-tomatoes-hang.md b/.changeset/rude-tomatoes-hang.md new file mode 100644 index 0000000000..8a3da7a455 --- /dev/null +++ b/.changeset/rude-tomatoes-hang.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/input-option': minor +--- + +Renders `aria-disabled` attribute when `disabled` is provided diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index db7b46092f..6b2f16d296 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -248,13 +248,7 @@ export const DatePickerMenu = forwardRef( )} - + ); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx new file mode 100644 index 0000000000..9163889093 --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { Month, newUTC } from '../../../shared'; +import { + DatePickerContext, + defaultDatePickerContext, +} from '../../../shared/components'; +import { + SingleDateContext, + SingleDateContextProps, +} from '../../SingleDateContext'; + +import { DatePickerMenuHeader } from '.'; + +const MockDatePickerProvider = DatePickerContext.Provider; +const MockSingleDateProvider = SingleDateContext.Provider; + +describe('packages/date-picker/menu/header', () => { + describe('Rendering', () => { + describe('Some month options are disabled', () => { + test('When `month` and `min` are the same year, earlier month options are disabled', async () => { + const { getByLabelText, findAllByRole } = render( + + + {}} /> + + , + ); + + const monthSelect = getByLabelText('Select month'); + + userEvent.click(monthSelect); + + const options = await findAllByRole('option'); + + for (const element of options) { + expect(element).toBeInTheDocument(); + const monthIndex = Number(element.getAttribute('value')); + + if (monthIndex < Month.March) { + expect(element).toHaveAttribute('aria-disabled', 'true'); + } + } + }); + + test('When `month` and `max` are the same year, later month options are disabled', async () => { + const { getByLabelText, findAllByRole } = render( + + + {}} /> + + , + ); + + const monthSelect = getByLabelText('Select month'); + + userEvent.click(monthSelect); + + const options = await findAllByRole('option'); + + for (const element of options) { + expect(element).toBeInTheDocument(); + const monthIndex = Number(element.getAttribute('value')); + + if (monthIndex > Month.September) { + expect(element).toHaveAttribute('aria-disabled', 'true'); + } + } + }); + + test('When `month` and `max`/`min` are different years, no month options are disabled', async () => { + const { getByLabelText, findAllByRole } = render( + + + {}} /> + + , + ); + + const monthSelect = getByLabelText('Select month'); + + userEvent.click(monthSelect); + + const options = await findAllByRole('option'); + + for (const element of options) { + expect(element).toBeInTheDocument(); + + expect(element).not.toHaveAttribute('aria-disabled', 'true'); + } + }); + }); + }); +}); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx similarity index 85% rename from packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx rename to packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index 220088f64c..d7d95832b0 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -14,17 +14,18 @@ import { Option, Select } from '@leafygreen-ui/select'; import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; import { Months, selectElementProps } from '../../../shared/constants'; import { isSameUTCMonth, setUTCMonth, setUTCYear } from '../../../shared/utils'; -import { DatePickerProps } from '../../DatePicker.types'; +import { useSingleDateContext } from '../../SingleDateContext'; import { menuHeaderSelectContainerStyles, menuHeaderStyles, selectInputWidthStyles, } from '../DatePickerMenu.styles'; -type DatePickerMenuHeaderProps = { - month: Date; +import { shouldMonthBeEnabled } from './utils/getMonthOptions'; + +interface DatePickerMenuHeaderProps { setMonth: (newMonth: Date) => void; -} & Pick; +} /** * A helper component for DatePickerMenu. @@ -34,8 +35,9 @@ type DatePickerMenuHeaderProps = { export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps ->(({ month, setMonth }: DatePickerMenuHeaderProps, fwdRef) => { +>(({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { const { min, max, isInRange } = useDatePickerContext(); + const { month } = useSingleDateContext(); const { isPopoverOpen: isSelectMenuOpen } = usePopoverContext(); const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); @@ -78,9 +80,18 @@ export const DatePickerMenuHeader = forwardRef< } }; + /** Returns whether the provided month should be enabled */ + const isMonthEnabled = (monthName: string) => + shouldMonthBeEnabled(monthName, { month, min, max }); + return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
+
{Months.map((m, i) => ( - ))} diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.ts new file mode 100644 index 0000000000..c1b41ba02b --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/index.ts @@ -0,0 +1 @@ +export { DatePickerMenuHeader } from './DatePickerMenuHeader'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/getMonthOptions.spec.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/getMonthOptions.spec.ts new file mode 100644 index 0000000000..17effe21b8 --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/getMonthOptions.spec.ts @@ -0,0 +1,89 @@ +import { Month, newUTC } from '../../../../../shared'; + +import { shouldMonthBeEnabled } from '.'; + +describe('packages/date-picker/menu/utils/shouldMonthBeEnabled', () => { + test('returns true by default', () => { + expect(shouldMonthBeEnabled('July')).toBeTruthy(); + }); + + test('returns false if given an invalid month', () => { + expect(shouldMonthBeEnabled('Quintember')).toBeFalsy(); + }); + + test('returns all 12 months when only month is provided', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2023, Month.July, 15), + }), + ).toBeTruthy(); + }); + + test('returns true when month, min & max are different years', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2023, Month.July, 15), + min: newUTC(2000, Month.January, 15), + max: newUTC(2050, Month.December, 15), + }), + ).toBeTruthy(); + }); + + describe('when month & min are the same year', () => { + test('returns false when month is before min month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.October, 14), + min: newUTC(2024, Month.September, 10), + }), + ).toBeFalsy(); + }); + + test('returns true when month is same min month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.October, 14), + min: newUTC(2024, Month.July, 5), + }), + ).toBeTruthy(); + }); + + test('returns true when month is after min month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.October, 14), + min: newUTC(2024, Month.March, 10), + }), + ).toBeTruthy(); + }); + }); + + describe('when month & max are the same year', () => { + test('returns false when month is after max month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.February, 14), + max: newUTC(2024, Month.March, 10), + }), + ).toBeFalsy(); + }); + + test('returns true when month is same as max month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.March, 10), + max: newUTC(2024, Month.July, 5), + }), + ).toBeTruthy(); + }); + + test('returns true when month is before max month', () => { + expect( + shouldMonthBeEnabled('July', { + month: newUTC(2024, Month.March, 10), + max: newUTC(2024, Month.September, 10), + }), + ).toBeTruthy(); + }); + }); +}); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/index.ts new file mode 100644 index 0000000000..be14f3164b --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/getMonthOptions/index.ts @@ -0,0 +1,39 @@ +import { isNull } from 'lodash'; + +import { DateType } from '../../../../../shared/types'; +import { getMonthIndex } from '../../../../../shared/utils'; + +export const shouldMonthBeEnabled = ( + monthName: string, + context?: { + month?: DateType; + min?: DateType; + max?: DateType; + }, +): boolean => { + const monthIndex = getMonthIndex(monthName); + + if (isNull(monthIndex)) return false; + + if (!context) { + return true; + } + + const { month, min, max } = context; + + const year = context.month?.getUTCFullYear(); + const minYear = context.min?.getUTCFullYear(); + const maxYear = context.max?.getUTCFullYear(); + + if (month && min && year === minYear) { + if (monthIndex < min.getUTCMonth()) return false; + return true; + } + + if (month && max && year === maxYear) { + if (monthIndex > max.getUTCMonth()) return false; + return true; + } + + return true; +}; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx index c6242c0fbd..096f358fc7 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx @@ -25,7 +25,7 @@ import { } from './SingleDateContext.types'; import { useDateRangeComponentRefs } from './useDatePickerComponentRefs'; -const SingleDateContext = createContext( +export const SingleDateContext = createContext( {} as SingleDateContextProps, ); diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/index.ts b/packages/date-picker/src/DatePicker/SingleDateContext/index.ts index ca13524a7e..5428d9e13c 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/index.ts +++ b/packages/date-picker/src/DatePicker/SingleDateContext/index.ts @@ -1,4 +1,8 @@ -export { SingleDateProvider, useSingleDateContext } from './SingleDateContext'; +export { + SingleDateContext, + SingleDateProvider, + useSingleDateContext, +} from './SingleDateContext'; export { type SingleDateContextProps, type SingleDateProviderProps, diff --git a/packages/date-picker/src/shared/constants.ts b/packages/date-picker/src/shared/constants.ts index 8f991397e4..f8ea5a2ea1 100644 --- a/packages/date-picker/src/shared/constants.ts +++ b/packages/date-picker/src/shared/constants.ts @@ -22,26 +22,45 @@ export enum Month { December, } +/** An array of all English month names */ +export const MonthsArray = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; + /** The default earliest selectable date */ export const MIN_DATE = new Date(Date.UTC(1970, Month.January, 1)); /** The default latest selectable date */ export const MAX_DATE = new Date(Date.UTC(2038, Month.January, 19)); -/** - * Long & short form of each month index - */ -export const Months: Array<{ +export interface MonthObject { long: string; short: string; -}> = range(12).map((monthIndex: number) => { - const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; - const month = new Date(str); - return { - long: month.toLocaleString('default', { month: 'long' }), - short: month.toLocaleString('default', { month: 'short' }), - }; -}); +} +/** + * Long & short form of each month index. Updates based on locale + */ +export const Months: Array = range(12).map( + (monthIndex: number) => { + const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; + const month = new Date(str); + return { + long: month.toLocaleString('default', { month: 'long' }), + short: month.toLocaleString('default', { month: 'short' }), + }; + }, +); /** Long & short form for each Day of the week */ export const DaysOfWeek = [ diff --git a/packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts b/packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts index f189b58e7f..2fc74f1fa7 100644 --- a/packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts +++ b/packages/date-picker/src/shared/hooks/useSegmentRefs/segmentRefs.types.ts @@ -1,4 +1,4 @@ -import { DynamicRefGetter } from '@leafygreen-ui/hooks/src/useDynamicRefs'; +import { DynamicRefGetter } from '@leafygreen-ui/hooks'; import { DateSegment } from '../useDateSegments'; diff --git a/packages/date-picker/src/shared/utils/getMonthIndex/index.ts b/packages/date-picker/src/shared/utils/getMonthIndex/index.ts new file mode 100644 index 0000000000..fc5e94131b --- /dev/null +++ b/packages/date-picker/src/shared/utils/getMonthIndex/index.ts @@ -0,0 +1,7 @@ +import { MonthsArray } from '../../constants'; + +/** Returns the index of an English month name */ +export const getMonthIndex = (monthName: string): number | null => { + const index = MonthsArray.indexOf(monthName); + return index >= 0 ? index : null; +}; diff --git a/packages/date-picker/src/shared/utils/getMonthName/index.ts b/packages/date-picker/src/shared/utils/getMonthName/index.ts index e786b70212..4d50c0a282 100644 --- a/packages/date-picker/src/shared/utils/getMonthName/index.ts +++ b/packages/date-picker/src/shared/utils/getMonthName/index.ts @@ -1,9 +1,14 @@ import padStart from 'lodash/padStart'; +import { MonthObject } from '../../constants'; + /** * Returns the month name from a given index and optional locale */ -export const getMonthName = (monthIndex: number, locale = 'default') => { +export const getMonthName = ( + monthIndex: number, + locale = 'default', +): MonthObject => { const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; const month = new Date(str); return { diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index 202e6bd1f4..f093db1404 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -8,6 +8,7 @@ export { getFormatParts } from './getFormatParts'; export { getFullMonthLabel } from './getFullMonthLabel'; export { getISODate } from './getISODate'; export { getLastOfMonth } from './getLastOfMonth'; +export { getMonthIndex } from './getMonthIndex'; export { getMonthName } from './getMonthName'; export { getRemainingParts } from './getRemainingParts'; export { diff --git a/packages/input-option/src/InputOption/InputOption.tsx b/packages/input-option/src/InputOption/InputOption.tsx index 73b6d35727..100148811c 100644 --- a/packages/input-option/src/InputOption/InputOption.tsx +++ b/packages/input-option/src/InputOption/InputOption.tsx @@ -42,6 +42,7 @@ export const InputOption = Polymorphic( ref={ref} role="option" aria-selected={highlighted} + aria-disabled={disabled} tabIndex={-1} className={cx( inputOptionStyles, From 561df1a690d205e0e25378ef03371326a0bf5ce2 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:26:18 -0500 Subject: [PATCH 271/351] DatePicker [LG-3758, LG-3760, LG-3764, LG-3765] Menu year/month select (#2072) * adds new esc test to date picker * adds PopoverContext tests * adds provider test in MenuWrapper * Update Select.spec.tsx * debug * Update DatePickerMenuHeader.spec.tsx * does this work? * wip - adding isSelectOpen to context` * fix build * fix menu bug * use popover callbacks * remove comment from popover * remove comments * remove test from menuWrapper * add select tests * update cs * comment * remove popover provider test * update tests * test name * remove unused stuff --------- Co-authored-by: Shaneeza --- .changeset/famous-timers-eat.md | 8 + .../src/DatePicker/DatePicker.spec.tsx | 31 +++- .../DatePickerComponent.tsx | 20 ++- .../DatePickerMenu/DatePickerMenu.tsx | 18 +- .../DatePickerMenuHeader.spec.tsx | 65 ++++++- .../DatePickerMenuHeader.tsx | 32 +--- .../DatePickerContext.spec.tsx | 79 ++++++++- .../DatePickerContext/DatePickerContext.tsx | 3 + .../DatePickerContext.types.ts | 6 + .../DatePickerContext.utils.ts | 2 + .../MenuWrapper/MenuWrapper.spec.tsx | 2 +- .../components/MenuWrapper/MenuWrapper.tsx | 8 +- packages/select/src/ListMenu/ListMenu.tsx | 12 ++ packages/select/src/Select/Select.spec.tsx | 161 +++++++++++++++++- packages/select/src/Select/Select.tsx | 14 ++ 15 files changed, 402 insertions(+), 59 deletions(-) create mode 100644 .changeset/famous-timers-eat.md diff --git a/.changeset/famous-timers-eat.md b/.changeset/famous-timers-eat.md new file mode 100644 index 0000000000..a2ac969445 --- /dev/null +++ b/.changeset/famous-timers-eat.md @@ -0,0 +1,8 @@ +--- +'@leafygreen-ui/select': patch +--- + +- Passes `onEnter*` and `onExit*` props to internal `Popover` component +- Adds tests to test `onEnter*` and `onExit*` callbacks +- Adds tests to test `PopoverContext` + diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 310824f701..72ed28dbd7 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -10,6 +10,8 @@ import { import userEvent from '@testing-library/user-event'; import { addDays, subDays } from 'date-fns'; +import { transitionDuration } from '@leafygreen-ui/tokens'; + import { Month } from '../shared/constants'; import { newUTC } from '../shared/utils'; import { @@ -710,26 +712,43 @@ describe('packages/date-picker', () => { expect(handleValidation).toHaveBeenCalledWith(undefined); }); + test('closes the menu regardless of which element is focused', async () => { + const { openMenu } = renderDatePicker(); + const { menuContainerEl, leftChevron } = await openMenu(); + userEvent.tab(); + expect(leftChevron).toHaveFocus(); + + userEvent.keyboard('{escape}'); + await waitForElementToBeRemoved(menuContainerEl); + expect(menuContainerEl).not.toBeInTheDocument(); + }); + test('does not close the main menu if a select menu is open', async () => { const { openMenu, queryAllByRole, findAllByRole } = renderDatePicker(); const { monthSelect, menuContainerEl } = await openMenu(); - monthSelect?.focus(); + tabNTimes(2); expect(monthSelect).toHaveFocus(); + userEvent.keyboard('[Enter]'); - userEvent.keyboard('{arrowdown}'); + await waitFor(() => + jest.advanceTimersByTime(transitionDuration.default), + ); + const options = await findAllByRole('option'); const firstOption = options[0]; + userEvent.keyboard('{arrowdown}'); expect(firstOption).toHaveFocus(); + const listBoxes = queryAllByRole('listbox'); expect(listBoxes).toHaveLength(2); + const selectMenu = listBoxes[1]; userEvent.keyboard('{escape}'); - await waitFor(() => { - expect(menuContainerEl).toBeInTheDocument(); - expect(selectMenu).not.toBeInTheDocument(); - }); + await waitForElementToBeRemoved(selectMenu); + expect(menuContainerEl).toBeInTheDocument(); + expect(monthSelect).toHaveFocus(); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index 32c64a404f..af2b1742ce 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -21,7 +21,7 @@ export const DatePickerComponent = forwardRef< HTMLDivElement, DatePickerComponentProps >(({ ...rest }: DatePickerComponentProps, fwdRef) => { - const { isOpen, menuId, disabled } = useDatePickerContext(); + const { isOpen, menuId, disabled, isSelectOpen } = useDatePickerContext(); const { value, closeMenu, handleValidation, getHighlightedCell } = useSingleDateContext(); @@ -54,13 +54,17 @@ export const DatePickerComponent = forwardRef< }; /** Handle key down events that should be fired regardless of target */ - const handleKeyDown: KeyboardEventHandler = e => { + const handleDatePickerKeyDown: KeyboardEventHandler = e => { const { key } = e; switch (key) { case keyMap.Escape: - closeMenu(); - handleValidation?.(value); + // Ensure that the menu will not close when a select menu is open and the ESC key is pressed. + if (!isSelectOpen) { + closeMenu(); + handleValidation?.(value); + } + break; case keyMap.Enter: @@ -74,12 +78,16 @@ export const DatePickerComponent = forwardRef< return ( <> - + diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 6b2f16d296..17d01eee1f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -142,12 +142,12 @@ export const DatePickerMenu = forwardRef( }; // Focus trap - const handleWrapperTabKeyPress: KeyboardEventHandler< - HTMLDivElement - > = e => { + const handleMenuKeyPress: KeyboardEventHandler = e => { + const { key } = e; + // Implementing custom focus-trap logic, // since focus-trap-react focuses the first element immediately on mount - if (e.key === keyMap.Tab) { + if (key === keyMap.Tab) { const currentFocus = document.activeElement; const highlightedCellElement = getHighlightedCell(); @@ -164,11 +164,15 @@ export const DatePickerMenu = forwardRef( e.preventDefault(); } } + + // call any handler that was passed in + onKeyDown?.(e); }; /** Called on any keydown within the CalendarGrid element */ const handleCalendarKeyDown: KeyboardEventHandler = e => { const { key } = e; + const highlightStart = highlight || value || today; let nextHighlight = highlightStart; @@ -200,12 +204,10 @@ export const DatePickerMenu = forwardRef( // if nextHighlight is in range if (isInRange(nextHighlight) && !isSameUTCDay(nextHighlight, highlight)) { updateHighlight(nextHighlight); + // Prevent the parent keydown handler from being called e.stopPropagation(); } - - // call any handler that was passed in - onKeyDown?.(e); }; return ( @@ -216,7 +218,7 @@ export const DatePickerMenu = forwardRef( spacing={spacing[1]} className={menuWrapperStyles} usePortal - onKeyDown={handleWrapperTabKeyPress} + onKeyDown={handleMenuKeyPress} {...rest} >
diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx index 9163889093..228c41ba03 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx @@ -1,7 +1,9 @@ -import React from 'react'; -import { render } from '@testing-library/react'; +import React, { PropsWithChildren, useState } from 'react'; +import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { transitionDuration } from '@leafygreen-ui/tokens'; + import { Month, newUTC } from '../../../shared'; import { DatePickerContext, @@ -127,4 +129,63 @@ describe('packages/date-picker/menu/header', () => { }); }); }); + + describe('Interaction', () => { + const mockSetIsSelectOpen = jest.fn(); + + beforeEach(() => { + mockSetIsSelectOpen.mockClear(); + jest.useFakeTimers(); + }); + + const AllMockProviders = ({ children }: PropsWithChildren<{}>) => { + const [isSelectOpen, _setIsSelectOpen] = useState(false); + + const setIsSelectOpen = (action: React.SetStateAction) => { + mockSetIsSelectOpen(action); + _setIsSelectOpen(action); + }; + + return ( + + + {children} + + + ); + }; + + test('opening & closing a select menu calls `setIsSelectOpen` in DatePickerContext', async () => { + const { getByLabelText } = render( + + {}} /> + , + ); + + const monthSelect = getByLabelText('Select month'); + userEvent.click(monthSelect); + await waitFor(() => { + jest.advanceTimersByTime(transitionDuration.default); + expect(mockSetIsSelectOpen).toHaveBeenCalledWith(true); + }); + userEvent.click(monthSelect); + + await waitFor(() => { + jest.advanceTimersByTime(transitionDuration.default); + expect(mockSetIsSelectOpen).toHaveBeenCalledWith(false); + }); + }); + }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index d7d95832b0..f4dfd723de 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -1,14 +1,9 @@ -import React, { - forwardRef, - KeyboardEventHandler, - MouseEventHandler, -} from 'react'; +import React, { forwardRef, MouseEventHandler } from 'react'; import { isBefore } from 'date-fns'; import range from 'lodash/range'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; -import { usePopoverContext } from '@leafygreen-ui/leafygreen-provider'; import { Option, Select } from '@leafygreen-ui/select'; import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; @@ -36,9 +31,8 @@ export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps >(({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { - const { min, max, isInRange } = useDatePickerContext(); + const { min, max, isInRange, setIsSelectOpen } = useDatePickerContext(); const { month } = useSingleDateContext(); - const { isPopoverOpen: isSelectMenuOpen } = usePopoverContext(); const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); @@ -70,28 +64,12 @@ export const DatePickerMenuHeader = forwardRef< updateMonth(newMonth); }; - /** - * Ensure that the date picker menu will not close when a select menu is open, focus is inside the select menu, and the ESC key is pressed. - */ - const handleEcsPress: KeyboardEventHandler = e => { - // `isSelectMenuOpen` provided by `PopoverProvider` is `true` if any popover _within_ the menu is open - if (isSelectMenuOpen) { - e.stopPropagation(); - } - }; - /** Returns whether the provided month should be enabled */ const isMonthEnabled = (monthName: string) => shouldMonthBeEnabled(monthName, { month, min, max }); return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions -
+
setIsSelectOpen(true)} + onExited={() => setIsSelectOpen(false)} > {Months.map((m, i) => ( @@ -113,7 +114,7 @@ export const DatePickerMenuHeader = forwardRef< onExited={() => setIsSelectOpen(false)} > {yearOptions.map(y => ( - ))} From 057aa0e0cd1966e38abe1877bbc959781d99d14c Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Thu, 30 Nov 2023 13:02:54 -0500 Subject: [PATCH 293/351] fix error icon placement --- .../DateInput/DateFormField/DateFormField.styles.ts | 4 +++- .../components/DateInput/DateFormField/DateFormField.tsx | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts index 35d1d682a4..2b9fb94b0e 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.styles.ts @@ -1,5 +1,7 @@ import { css } from '@leafygreen-ui/emotion'; export const iconButtonStyles = css` - margin-inline-end: -8px; + svg + button { + margin-left: -8px; + } `; diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx index 661c4244b4..b268f8451e 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx @@ -5,6 +5,7 @@ import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; import { useDatePickerContext } from '../../DatePickerContext'; import { CalendarButton } from '../CalendarButton'; +import { iconButtonStyles } from './DateFormField.styles'; import { DateFormFieldProps } from './DateFormField.types'; /** @@ -53,6 +54,7 @@ export const DateFormField = React.forwardRef< aria-expanded={isOpen} aria-controls={menuId} onClick={onInputClick} + className={iconButtonStyles} contentEnd={ Date: Thu, 30 Nov 2023 13:09:30 -0500 Subject: [PATCH 294/351] increase select width --- .../src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts index 242ef409b5..cb4d158628 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.styles.ts @@ -41,5 +41,5 @@ export const menuCalendarGridStyles = css` // Hardcoding the width export const selectInputWidthStyles = css` - width: 68.5px; + width: 70px; `; From 0ef6cbd5b53af928bfd2afae99ea04b190269413 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Fri, 1 Dec 2023 11:55:00 -0500 Subject: [PATCH 295/351] Date picker [LG-3834] Internal error handling (#2105) * Adds internal error handling * Updates internal validation * updates validation handling tests * Restore State prop. Update error message logic * Create shaggy-falcons-invite.md * fix types --- .changeset/shaggy-falcons-invite.md | 5 + .../src/DatePicker/DatePicker.spec.tsx | 235 +++++++++++++----- .../src/DatePicker/DatePicker.stories.tsx | 13 +- .../src/DatePicker/DatePicker.testutils.tsx | 2 +- .../date-picker/src/DatePicker/DatePicker.tsx | 18 +- .../DatePickerComponent.tsx | 15 ++ .../DatePickerInput/DatePickerInput.tsx | 1 - .../DatePickerMenu/DatePickerMenu.spec.tsx | 2 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 20 +- .../SingleDateContext/SingleDateContext.tsx | 48 +++- .../SingleDateContext.types.ts | 5 +- .../DateFormField/DateFormField.stories.tsx | 3 +- .../DateInput/DateFormField/DateFormField.tsx | 5 +- .../DatePickerContext/DatePickerContext.tsx | 16 +- .../DatePickerContext.types.ts | 12 +- .../DatePickerContext.utils.ts | 12 +- .../useDatePickerErrorNotifications.ts | 85 +++++++ packages/date-picker/src/shared/types.ts | 2 +- .../getFormattedDateString.spec.ts | 20 ++ .../utils/getFormattedDateString/index.ts | 20 ++ .../date-picker/src/shared/utils/index.ts | 1 + .../newDateFromSegments.spec.ts | 5 +- .../form-field/src/FormField/FormField.tsx | 3 + 23 files changed, 445 insertions(+), 103 deletions(-) create mode 100644 .changeset/shaggy-falcons-invite.md create mode 100644 packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts create mode 100644 packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts create mode 100644 packages/date-picker/src/shared/utils/getFormattedDateString/index.ts diff --git a/.changeset/shaggy-falcons-invite.md b/.changeset/shaggy-falcons-invite.md new file mode 100644 index 0000000000..aae5a58f35 --- /dev/null +++ b/.changeset/shaggy-falcons-invite.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/form-field': minor +--- + +Adds `data-testid` to Label, Description & Error elements diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index daa3de4bdb..69748f0490 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -13,7 +13,12 @@ import { addDays, subDays } from 'date-fns'; import { transitionDuration } from '@leafygreen-ui/tokens'; import { defaultMax, defaultMin, Month } from '../shared/constants'; -import { getValueFormatter, newUTC } from '../shared/utils'; +import { + getFormattedDateString, + getValueFormatter, + newUTC, + setUTCYear, +} from '../shared/utils'; import { eventContainingTargetValue, tabNTimes, @@ -23,6 +28,7 @@ import { expectedTabStopLabels, findTabStopElementMap, renderDatePicker, + RenderDatePickerResult, RenderMenuResult, } from './DatePicker.testutils'; import { DatePicker } from '.'; @@ -97,6 +103,107 @@ describe('packages/date-picker', () => { expect(monthInput.value).toEqual('12'); expect(yearInput.value).toEqual('2023'); }); + + describe('Error states', () => { + test('renders error state when `state` is "error"', () => { + const { getByRole } = render( + , + ); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + }); + + test('renders with `errorMessage` when provided', () => { + const { queryByTestId } = render( + , + ); + const errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent('Custom error message'); + }); + + test('does not render `errorMessage` when state is not set', () => { + const { getByRole, queryByTestId } = render( + , + ); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-invalid', 'false'); + const errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).not.toBeInTheDocument(); + }); + + test('renders with internal error state when value is out of range', () => { + const { getByTestId, getByRole } = render( + , + ); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + + const errorElement = getByTestId('lg-form_field-error_message'); + expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + 'Date must be before 2038-01-19', + ); + }); + + test('external error message overrides internal error message', () => { + const { getByTestId, getByRole } = renderDatePicker({ + value: newUTC(2100, 1, 1), + state: 'error', + errorMessage: 'Custom error message', + }); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + + const errorElement = getByTestId('lg-form_field-error_message'); + expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent('Custom error message'); + }); + + test('renders internal message if external error message is not set', () => { + const { inputContainer, getByTestId } = renderDatePicker({ + value: newUTC(2100, 1, 1), + state: 'error', + errorMessage: undefined, + }); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( + 'Date must be before 2038-01-19', + ); + }); + + test('removing an external error displays an internal error when applicable', () => { + const { inputContainer, rerenderDatePicker, getByTestId } = + renderDatePicker({ + value: newUTC(2100, 1, 1), + }); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( + 'Date must be before 2038-01-19', + ); + + rerenderDatePicker({ errorMessage: 'Some error', state: 'error' }); + + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( + 'Some error', + ); + + rerenderDatePicker({ state: 'none' }); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( + 'Date must be before 2038-01-19', + ); + }); + }); }); describe('Menu', () => { @@ -894,81 +1001,81 @@ describe('packages/date-picker', () => { }); describe('if new value would be out of range', () => { - test('updates the input', () => { - const min = newUTC(1999, Month.July, 4); - const max = newUTC(2020, Month.July, 4); - const value = - key === 'arrowup' - ? newUTC(2019, Month.August, 1) - : newUTC(2000, Month.June, 30); - const { yearInput } = renderDatePicker({ + const onDateChange = jest.fn(); + const onSegmentChange = jest.fn(); + const handleValidation = jest.fn(); + const min = newUTC(1999, Month.July, 4); + const max = newUTC(2020, Month.July, 4); + const startValue = + key === 'arrowup' + ? newUTC(2019, Month.August, 1) + : newUTC(2000, Month.June, 30); + const newYearVal = getValueFormatter('year')( + key === 'arrowup' ? 2020 : 1999, + ); + const expectedMessage = + key === 'arrowup' + ? `Date must be before ${getFormattedDateString( + max, + 'iso8601', + )}` + : `Date must be after ${getFormattedDateString( + min, + 'iso8601', + )}`; + let renderResult: RenderDatePickerResult; + + beforeEach(() => { + onDateChange.mockReset(); + onSegmentChange.mockReset(); + handleValidation.mockReset(); + + renderResult = renderDatePicker({ min, max, - value, + value: startValue, + onDateChange, + onChange: onSegmentChange, + handleValidation, }); - userEvent.click(yearInput); + userEvent.click(renderResult.yearInput); userEvent.keyboard(`{${key}}`); - const expectedYearVal = getValueFormatter('year')( - key === 'arrowup' ? 2020 : 1999, - ); - expect(yearInput).toHaveValue(expectedYearVal); + }); + + test('updates the input', () => { + expect(renderResult.yearInput).toHaveValue(newYearVal); }); test('fires the change handler', () => { - const onDateChange = jest.fn(); - const min = newUTC(1999, Month.July, 4); - const max = newUTC(2020, Month.July, 4); - const value = - key === 'arrowup' - ? newUTC(2019, Month.August, 1) - : newUTC(2000, Month.June, 30); - const { yearInput } = renderDatePicker({ - min, - max, - onDateChange, - value, - }); - userEvent.click(yearInput); - userEvent.keyboard(`{${key}}`); - expect(onDateChange).toHaveBeenCalled(); + expect(onDateChange).toHaveBeenCalledWith( + setUTCYear(startValue, Number(newYearVal)), + ); }); test('fires the segment change handler', () => { - const onChange = jest.fn(); - const min = newUTC(1999, Month.July, 4); - const max = newUTC(2020, Month.July, 4); - const value = - key === 'arrowup' - ? newUTC(2019, Month.August, 1) - : newUTC(2000, Month.June, 30); - const { yearInput } = renderDatePicker({ - min, - max, - onChange, - value, - }); - userEvent.click(yearInput); - userEvent.keyboard(`{${key}}`); - expect(onChange).toHaveBeenCalled(); + expect(onSegmentChange).toHaveBeenCalledWith( + eventContainingTargetValue(newYearVal), + ); }); - test.skip('fires the validation handler', () => { - const handleValidation = jest.fn(); - const min = newUTC(1999, Month.July, 4); - const max = newUTC(2020, Month.July, 4); - const value = - key === 'arrowup' - ? newUTC(2019, Month.August, 1) - : newUTC(2000, Month.June, 30); - const { yearInput } = renderDatePicker({ - min, - max, - handleValidation, - value, - }); - userEvent.click(yearInput); - userEvent.keyboard(`{${key}}`); - expect(handleValidation).toHaveBeenCalled(); + test('fires the validation handler', () => { + expect(handleValidation).toHaveBeenCalledWith( + setUTCYear(startValue, Number(newYearVal)), + ); + }); + + test('sets aria-invalid', () => { + expect(renderResult.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + }); + + test('sets error message', () => { + const errorMessageElement = within( + renderResult.formField, + ).queryByText(expectedMessage); + expect(errorMessageElement).toBeInTheDocument(); }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx index 876d95e0b1..9f138e13e6 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.stories.tsx @@ -38,6 +38,7 @@ const meta: StoryMetaType = { 'handleValidation', 'initialValue', 'onChange', + 'onDateChange', 'onSegmentChange', 'value', ], @@ -79,7 +80,17 @@ export default meta; export const Basic: StoryFn = props => { const [value, setValue] = useState(); - return ; + return ( + + // eslint-disable-next-line no-console + console.log('Storybook: handleValidation', { date }) + } + /> + ); }; export const Uncontrolled: StoryFn = props => { diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index b8c62f35a6..79be8ecade 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -19,7 +19,7 @@ const withinElement = (element: HTMLElement | null) => { return element ? within(element) : null; }; -interface RenderDatePickerResult extends RenderResult { +export interface RenderDatePickerResult extends RenderResult { formField: HTMLElement; inputContainer: HTMLElement; dayInput: HTMLInputElement; diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 8263745973..785db86434 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -7,6 +7,7 @@ import { BaseFontSize } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; import { + ContextPropKeys, contextPropNames, DatePickerProvider, } from '../shared/components/DatePickerContext'; @@ -29,16 +30,17 @@ export const DatePicker = forwardRef( handleValidation, darkMode: darkModeProp, baseFontSize: basefontSizeProp, + state, ...props }: DatePickerProps, fwdRef, ) => { const { darkMode } = useDarkMode(darkModeProp); const baseFontSize = useUpdatedBaseFontSize(basefontSizeProp); - const [contextProps, restProps] = pickAndOmit( - { darkMode, baseFontSize, ...props }, - contextPropNames, - ); + const [contextProps, componentProps] = pickAndOmit< + DatePickerProps, + ContextPropKeys + >({ ...props }, contextPropNames); const { value, setValue } = useControlledValue( valueProp, @@ -47,7 +49,11 @@ export const DatePicker = forwardRef( ); return ( - + ( baseFontSize === BaseFontSize.Body1 ? 14 : baseFontSize } > - + diff --git a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx index 75c30c5134..69d2add4b8 100644 --- a/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerComponent/DatePickerComponent.tsx @@ -6,6 +6,7 @@ import React, { useRef, } from 'react'; import { ExitHandler } from 'react-transition-group/Transition'; +import isEqual from 'lodash/isEqual'; import { useBackdropClick, @@ -15,6 +16,7 @@ import { import { keyMap } from '@leafygreen-ui/lib'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { isSameUTCDay } from '../../shared/utils'; import { DatePickerInput } from '../DatePickerInput'; import { DatePickerMenu } from '../DatePickerMenu'; import { useSingleDateContext } from '../SingleDateContext'; @@ -35,6 +37,8 @@ export const DatePickerComponent = forwardRef< getHighlightedCell, } = useSingleDateContext(); + const prevValue = usePrevious(value); + const formFieldRef = useForwardedRef(fwdRef, null); const menuRef = useRef(null); const prevDisabledValue = usePrevious(disabled); @@ -106,6 +110,17 @@ export const DatePickerComponent = forwardRef< } }; + /** + * Side Effects + */ + + /** When value changes, validate it */ + useEffect(() => { + if (!isEqual(prevValue, value) && !isSameUTCDay(prevValue, value)) { + handleValidation(value); + } + }, [handleValidation, prevValue, value]); + return ( <> ( if (!isSameUTCDay(inputVal, value)) { handleValidation?.(inputVal); setValue(inputVal || null); - // TODO: update month? } }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index b55b036eb6..8cb5ac6313 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -199,7 +199,7 @@ describe('packages/date-picker/date-picker-menu', () => { expect(isEveryCellDisabled).toBe(true); }); - test('doest not highlight a cell', () => { + test("doesn't not highlight a cell", () => { const { calendarCells } = renderDatePickerMenu(null, { value: newUTC(2048, Month.December, 25), }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 833c4a3417..4fe86d820e 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -6,20 +6,22 @@ import { last, omit } from 'lodash'; import MockDate from 'mockdate'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { StoryMetaType } from '@leafygreen-ui/lib'; +import { type StoryMetaType } from '@leafygreen-ui/lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; import { InlineCode } from '@leafygreen-ui/typography'; import { + type ContextPropKeys, contextPropNames, - DatePickerContextProps, + type DatePickerContextProps, DatePickerProvider, defaultDatePickerContext, } from '../../shared/components/DatePickerContext'; import { Month } from '../../shared/constants'; import { newUTC, pickAndOmit } from '../../shared/utils'; +import { DatePickerProps } from '../DatePicker.types'; import { - SingleDateContextProps, + type SingleDateContextProps, SingleDateProvider, } from '../SingleDateContext'; @@ -32,10 +34,10 @@ type DecoratorArgs = DatePickerMenuProps & DatePickerContextProps; const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { - const [{ darkMode, ...contextProps }, { ...props }] = pickAndOmit( - ctx?.args as DecoratorArgs, - [...contextPropNames], - ); + const [{ darkMode, ...providerProps }, componentProps] = pickAndOmit< + DatePickerProps, + ContextPropKeys + >(ctx?.args, contextPropNames); // Force `new Date()` to return `mockToday` MockDate.set(mockToday); @@ -44,10 +46,10 @@ const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { - + ); diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx index cae6bcadda..45fc0d2611 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx @@ -17,7 +17,12 @@ import { setToUTCMidnight, useDatePickerContext, } from '../../shared'; -import { getISODate, isSameUTCDay } from '../../shared/utils'; +import { + getFormattedDateString, + getISODate, + isOnOrBefore, + isSameUTCDay, +} from '../../shared/utils'; import { getInitialHighlight } from '../utils/getInitialHighlight'; import { @@ -37,10 +42,20 @@ export const SingleDateProvider = ({ children, value, setValue: _setValue, - handleValidation, + handleValidation: _handleValidation, }: PropsWithChildren) => { const refs = useDateRangeComponentRefs(); - const { isOpen, setOpen, disabled } = useDatePickerContext(); + const { + isOpen, + setOpen, + disabled, + min, + max, + dateFormat, + setInternalErrorMessage, + clearInternalErrorMessage, + isInRange, + } = useDatePickerContext(); const prevValue = usePrevious(value); const today = useMemo(() => setToUTCMidnight(new Date(Date.now())), []); @@ -83,7 +98,32 @@ export const SingleDateProvider = ({ _setHighlight(newHighlight); }, []); - /** Track the event that last triggered the menu to open/close */ + /** + * Handles internal validation, + * and calls the provided `handleValidation` callback + */ + const handleValidation = (val?: DateType) => { + // Set an internal error state if necessary + if (val && !isInRange(val)) { + if (isOnOrBefore(val, min)) { + setInternalErrorMessage( + `Date must be after ${getFormattedDateString(min, dateFormat)}`, + ); + } else { + setInternalErrorMessage( + `Date must be before ${getFormattedDateString(max, dateFormat)}`, + ); + } + } else { + clearInternalErrorMessage(); + } + + _handleValidation?.(val); + }; + + /** + * Track the event that last triggered the menu to open/close + */ const [menuTriggerEvent, setMenuTriggerEvent] = useState(); /** diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts index 2291096044..70d746dc6c 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts @@ -27,9 +27,10 @@ export interface SingleDateContextProps { setValue: (newVal: DateType | undefined) => void; /** - * Calls the `handleValidation` function provided by the consumer + * Performs internal validation, and + * calls the `handleValidation` function provided by the consumer */ - handleValidation: DatePickerProps['handleValidation']; + handleValidation: Required['handleValidation']; /** * The current date, at UTC midnight diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx index 06a2194fa0..5f58e42c3c 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx @@ -30,7 +30,7 @@ const meta: StoryMetaType< darkMode: [false, true], label: ['Label', undefined], description: [undefined, 'Description'], - state: Object.values(DatePickerState), + // state: Object.values(DatePickerState), disabled: [false, true], }, excludeCombinations: [ @@ -72,7 +72,6 @@ const meta: StoryMetaType< args: { label: 'Label', description: 'Description', - state: DatePickerState.Error, errorMessage: 'This is an error message', }, argTypes: { diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx index b268f8451e..0d0f46dbd4 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; +import { DatePickerState } from '../../../types'; import { useDatePickerContext } from '../../DatePickerContext'; import { CalendarButton } from '../CalendarButton'; @@ -29,8 +30,7 @@ export const DateFormField = React.forwardRef< const { label, description, - state, - errorMessage, + stateNotification: { state, message: errorMessage }, disabled, isOpen, menuId, @@ -53,6 +53,7 @@ export const DateFormField = React.forwardRef< tabIndex={-1} aria-expanded={isOpen} aria-controls={menuId} + aria-invalid={state === DatePickerState.Error} onClick={onInputClick} className={iconButtonStyles} contentEnd={ diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx index a02b4e3002..18832e4711 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx @@ -13,6 +13,7 @@ import { defaultDatePickerContext, getContextProps, } from './DatePickerContext.utils'; +import { useDatePickerErrorNotifications } from './useDatePickerErrorNotifications'; /** Create the DatePickerContext */ export const DatePickerContext = createContext( @@ -27,28 +28,41 @@ export const DatePickerProvider = ({ children, initialOpen = false, disabled = false, + errorMessage, + state, autoComplete = AutoComplete.Off, ...rest }: PropsWithChildren) => { const isInitiallyOpen = disabled ? false : initialOpen; + const [isOpen, setOpen] = useState(isInitiallyOpen); const [isDirty, setIsDirty] = useState(false); const [isSelectOpen, setIsSelectOpen] = useState(false); const menuId = useIdAllocator({ prefix: 'lg-date-picker-menu' }); const contextValue = getContextProps(rest); + /** Error state handling */ + const { + stateNotification, + setInternalErrorMessage, + clearInternalErrorMessage, + } = useDatePickerErrorNotifications(state, errorMessage); + return ( diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts index 60025af5a2..4d14cf30c8 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts @@ -1,4 +1,11 @@ -import { BaseDatePickerProps } from '../../types'; +import { BaseDatePickerProps, DatePickerState } from '../../types'; + +import { UseDatePickerErrorNotificationsReturnObject } from './useDatePickerErrorNotifications'; + +export interface StateNotification { + state: DatePickerState; + message: string; +} /** The props expected to pass int the provider */ export interface DatePickerProviderProps extends BaseDatePickerProps {} @@ -7,7 +14,8 @@ export interface DatePickerProviderProps extends BaseDatePickerProps {} * The values in context */ export interface DatePickerContextProps - extends Required { + extends Omit, 'state'>, + UseDatePickerErrorNotificationsReturnObject { /** The earliest date accepted */ min: Date; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts index 0f4d09d5b1..d3e18aba67 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts @@ -22,7 +22,9 @@ export const TZ = Intl.DateTimeFormat().resolvedOptions().timeZone; export type ContextPropKeys = keyof DatePickerProviderProps & keyof BaseDatePickerProps; -/** Prop names that are in both DatePickerProps and DatePickerProviderProps */ +/** + * Prop names that are in both DatePickerProps and DatePickerProviderProps + * */ export const contextPropNames: Array = [ 'label', 'description', @@ -33,7 +35,6 @@ export const contextPropNames: Array = [ 'baseFontSize', 'disabled', 'size', - 'state', 'errorMessage', 'initialOpen', 'autoComplete', @@ -55,13 +56,18 @@ export const defaultDatePickerContext: DatePickerContextProps = { isInRange: () => true, disabled: false, size: Size.Default, - state: DatePickerState.None, errorMessage: '', baseFontSize: BaseFontSize.Body1, darkMode: false, menuId: '', isSelectOpen: false, setIsSelectOpen: () => {}, + stateNotification: { + state: DatePickerState.None, + message: '', + }, + setInternalErrorMessage: () => {}, + clearInternalErrorMessage: () => {}, autoComplete: AutoComplete.Off, }; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts b/packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts new file mode 100644 index 0000000000..1b4dce2bdd --- /dev/null +++ b/packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts @@ -0,0 +1,85 @@ +import { useMemo, useState } from 'react'; + +import { DatePickerState } from '../../types'; + +import { StateNotification } from './DatePickerContext.types'; + +export interface UseDatePickerErrorNotificationsReturnObject { + stateNotification: StateNotification; + setInternalErrorMessage: (msg: string) => void; + clearInternalErrorMessage: () => void; +} + +export const useDatePickerErrorNotifications = ( + externalState?: DatePickerState, + externalErrorMessage?: string, +): UseDatePickerErrorNotificationsReturnObject => { + /** + * An external state notification object, + * updated when the external message or state prop changes + */ + const externalStateNotification = useMemo(() => { + const state = externalState ?? DatePickerState.None; + const message = + externalState === DatePickerState.Error ? externalErrorMessage ?? '' : ''; + + return { + state, + message, + }; + }, [externalErrorMessage, externalState]); + + /** + * An internal state notification used to handle internal validation (e.g. if date is in range) + */ + const [internalStateNotification, setInternalStateNotification] = + useState({ + state: DatePickerState.None, + message: '', + }); + + /** + * Removes the internal error message + */ + const clearInternalErrorMessage = () => { + setInternalStateNotification({ + state: DatePickerState.None, + message: '', + }); + }; + + /** + * Sets an internal error message + */ + const setInternalErrorMessage = (msg: string) => { + setInternalStateNotification({ + state: DatePickerState.Error, + message: msg, + }); + }; + + /** + * Calculate the stateNotification to use based on external & internal states. + * External errors take precedence over internal errors. + */ + const stateNotification = useMemo(() => { + if (externalStateNotification.state === DatePickerState.Error) { + if ( + !externalStateNotification.message && + internalStateNotification.state === DatePickerState.Error + ) { + return internalStateNotification; + } else { + return externalStateNotification; + } + } else { + return internalStateNotification; + } + }, [externalStateNotification, internalStateNotification]); + + return { + stateNotification, + setInternalErrorMessage, + clearInternalErrorMessage, + }; +}; diff --git a/packages/date-picker/src/shared/types.ts b/packages/date-picker/src/shared/types.ts index 9554ca0e33..12ec6dd42c 100644 --- a/packages/date-picker/src/shared/types.ts +++ b/packages/date-picker/src/shared/types.ts @@ -80,7 +80,7 @@ export interface BaseDatePickerProps extends DarkModeProps { state?: DatePickerState; /** - * A message to show in red underneath the input when in an error state + * A message to show in red underneath the input when state is Error */ errorMessage?: string; diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts new file mode 100644 index 0000000000..e8ef14cd6e --- /dev/null +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts @@ -0,0 +1,20 @@ +import { Month } from '../../constants'; +import { newUTC } from '../newUTC'; + +import { getFormattedDateString } from '.'; + +describe('packages/date-picker/utils/getFormattedDateString', () => { + const testDate = newUTC(1967, Month.May, 2); + test('iso8601', () => { + expect(getFormattedDateString(testDate, 'iso8601')).toBe('1967-05-02'); + }); + test('en-US', () => { + expect(getFormattedDateString(testDate, 'en-US')).toBe('05/02/1967'); + }); + test('en-UK', () => { + expect(getFormattedDateString(testDate, 'en-UK')).toBe('02/05/1967'); + }); + test('invalid locale', () => { + expect(getFormattedDateString(testDate, 'asdf')).toBeUndefined(); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts new file mode 100644 index 0000000000..1b9e477976 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts @@ -0,0 +1,20 @@ +import { DateSegment } from '../../hooks'; +import { getFormatParts } from '../getFormatParts'; +import { getFormattedSegmentsFromDate } from '../getSegmentsFromDate'; + +export const getFormattedDateString = (date: Date, locale: string) => { + const formatParts = getFormatParts(locale); + const dateSegments = getFormattedSegmentsFromDate(date); + + // Note: looping through `formatParts`, instead of using `Intl.DateTimeFormat(locale).format(date)` + // since the locale `iso8601` does not return a valid formatter + const formattedDate = formatParts?.reduce((dateString, part) => { + const partString = + part.type === 'literal' + ? part.value + : dateSegments[part.type as DateSegment]; + return dateString + partString; + }, ''); + + return formattedDate; +}; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index 7af596c357..34d47e9cc5 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -7,6 +7,7 @@ export { getDaysInUTCMonth } from './getDaysInUTCMonth'; export { getFirstEmptySegment } from './getFirstEmptySegment'; export { getFirstOfMonth } from './getFirstOfMonth'; export { getFormatParts, getFormatter } from './getFormatParts'; +export { getFormattedDateString } from './getFormattedDateString'; export { getFullMonthLabel } from './getFullMonthLabel'; export { getISODate } from './getISODate'; export { getLastOfMonth } from './getLastOfMonth'; diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts index 25943036ab..132a6bdd3a 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts @@ -19,7 +19,6 @@ describe('packages/date=picker/utils/newDateFromSegments', () => { expect(newDate?.toISOString()).toEqual('2100-01-01T00:00:00.000Z'); }); - // FIXME: test.skip('returns undefined if month/day combo is invalid', () => { const newDate = newDateFromSegments({ day: '31', @@ -30,9 +29,9 @@ describe('packages/date=picker/utils/newDateFromSegments', () => { expect(newDate).toBeUndefined(); }); - test('returns undefined if any segment is undefined', () => { + test('returns undefined if any segment is empty', () => { const newDate = newDateFromSegments({ - day: undefined, + day: '', month: '1', year: '2023', }); diff --git a/packages/form-field/src/FormField/FormField.tsx b/packages/form-field/src/FormField/FormField.tsx index 72825b9903..042c6aa8b1 100644 --- a/packages/form-field/src/FormField/FormField.tsx +++ b/packages/form-field/src/FormField/FormField.tsx @@ -65,6 +65,7 @@ export const FormField = forwardRef(
{label && (
{range(daysPerWeek).map(i => { const dayIndex = (i + weekStartsOn) % daysPerWeek; - const day = DaysOfWeek[dayIndex]; + const weekday = getLocaleWeekdays(dateFormat)[dayIndex]; return ( ); diff --git a/packages/date-picker/src/shared/constants.ts b/packages/date-picker/src/shared/constants.ts index 5ca4e42369..37303805ec 100644 --- a/packages/date-picker/src/shared/constants.ts +++ b/packages/date-picker/src/shared/constants.ts @@ -1,6 +1,3 @@ -import padStart from 'lodash/padStart'; -import range from 'lodash/range'; - import { DropdownWidthBasis } from '@leafygreen-ui/select'; /** Days in a week */ @@ -22,22 +19,6 @@ export enum Month { December, } -/** An array of all English month names */ -export const MonthsArray = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; - /** * The default earliest selectable date * (Unix epoch start: https://en.wikipedia.org/wiki/Unix_time) @@ -50,36 +31,6 @@ export const MIN_DATE = new Date(Date.UTC(1970, Month.January, 1)); */ export const MAX_DATE = new Date(Date.UTC(2038, Month.January, 19)); -export interface MonthObject { - long: string; - short: string; -} -/** - * Long & short form of each month index. Updates based on locale - */ -export const Months: Array = range(12).map( - (monthIndex: number) => { - const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; - const month = new Date(str); - return { - long: month.toLocaleString('default', { month: 'long' }), - short: month.toLocaleString('default', { month: 'short' }), - }; - }, -); - -/** Long & short form for each Day of the week */ -export const DaysOfWeek = [ - { long: 'Sunday', short: 'su' }, - { long: 'Monday', short: 'mo' }, - { long: 'Tuesday', short: 'tu' }, - { long: 'Wednesday', short: 'we' }, - { long: 'Thursday', short: 'th' }, - { long: 'Friday', short: 'fr' }, - { long: 'Saturday', short: 'sa' }, -] as const; -export type DaysOfWeek = (typeof DaysOfWeek)[number]; - // TODO: Update how defaultMin & defaultMax are defined, // since day/month are constants, // but year is consumer-defined diff --git a/packages/date-picker/src/shared/testutils/testValues.ts b/packages/date-picker/src/shared/testutils/testValues.ts index c5ccd4233b..1712df4ed2 100644 --- a/packages/date-picker/src/shared/testutils/testValues.ts +++ b/packages/date-picker/src/shared/testutils/testValues.ts @@ -29,4 +29,16 @@ export const TimeZones = testTimeZones.map(({ tz }) => tz); * Farsi-Afghanistan (week starts on Sat) * English-Maldives (week starts on Fri.) */ -export const Locales = ['iso8601', 'en-US', 'en-UK', 'de-DE', 'fa-AF', 'en-MV']; +export const Locales = [ + 'iso8601', + 'de-DE', // German, Germany (uses `.` char separator) + 'en-US', // English, US (week starts on Sun.) + 'en-GB', // English, UK (week starts on Mon.) + 'en-MV', // English, Maldives (week starts on Fri.) + 'es-MX', // Spanish, Mexico + 'fa-AF', // Farsi, Afghanistan (week starts on Sat.) + 'fr-FR', // French, France + 'he-IL', // Hebrew, Israel + 'ja-JP', // Japanese, Japan + 'zh-CN', // Chinese, China +]; diff --git a/packages/date-picker/src/shared/types/index.ts b/packages/date-picker/src/shared/types/index.ts index b21c6d9b8f..75490239d7 100644 --- a/packages/date-picker/src/shared/types/index.ts +++ b/packages/date-picker/src/shared/types/index.ts @@ -18,6 +18,26 @@ export type DatePickerState = export type DateType = Date | null; export type DateRangeType = [DateType, DateType]; +export interface MonthObject { + long: string; + short: string; +} + +/** + * Object representing the abbreviations of a given weekday. + * Abbreviation formats defined in Unicode: https://www.unicode.org/reports/tr35/tr35-67/tr35-dates.html#dfst-weekday + */ +export interface WeekdayObject { + /** The long-form weekday name (e.g. Tuesday)*/ + long: string; + /** An abbreviated weekday name (e.g. Tue) */ + abbr: string; + /** A shorter weekday name (e.g. Tu)*/ + short?: string; + /** The shortest weekday name (e.g. T) */ + narrow: string; +} + export const AutoComplete = { Off: 'off', On: 'on', @@ -43,15 +63,13 @@ export interface BaseDatePickerProps extends DarkModeProps { * Sets the _presentation format_ for the displayed date. * Fallback to the user’s browser preference (if supported), otherwise ISO-8601. * - * Currently only the following values are officially supported. + * Currently only the following values are officially supported: 'en-US' | 'en-GB' | 'iso8601' * Other valid [Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) * strings may work, however no assurances are made. * - * @enum 'en-US' | 'en-UK' | 'iso8601' - * * @default 'iso8601' */ - dateFormat?: 'iso8601' | `${string}-${string}`; + dateFormat?: 'iso8601' | string; /** * A valid IANA timezone string, or UTC offset. diff --git a/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts b/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts index b9dd35adcf..1b40d5a032 100644 --- a/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts +++ b/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts @@ -1,6 +1,6 @@ -import { Months } from '../../constants'; +import { getMonthName } from '../getMonthName'; /** Returns a long month label (i.e. September 2023) */ export const getFullMonthLabel = (date: Date): string => { - return Months[date.getUTCMonth()].long + ' ' + date.getUTCFullYear(); + return getMonthName(date.getUTCMonth()).long + ' ' + date.getUTCFullYear(); }; diff --git a/packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts b/packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts new file mode 100644 index 0000000000..a78e119722 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts @@ -0,0 +1,25 @@ +import { getLocaleMonths } from '.'; + +describe('packages/date-picker/utils/getLocaleMonths', () => { + test('default (English)', () => { + expect(getLocaleMonths()).toHaveLength(12); + getLocaleMonths().forEach(mo => { + expect(mo).not.toBeUndefined(); + expect(mo).not.toBeNull(); + }); + }); + test('iso8601', () => { + expect(getLocaleMonths('iso8601')).toHaveLength(12); + getLocaleMonths('iso8601').forEach(mo => { + expect(mo).not.toBeUndefined(); + expect(mo).not.toBeNull(); + }); + }); + test('jp-JP', () => { + expect(getLocaleMonths('jp-JP')).toHaveLength(12); + getLocaleMonths('jp-JP').forEach(mo => { + expect(mo).not.toBeUndefined(); + expect(mo).not.toBeNull(); + }); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getLocaleMonths/index.ts b/packages/date-picker/src/shared/utils/getLocaleMonths/index.ts new file mode 100644 index 0000000000..850485d16d --- /dev/null +++ b/packages/date-picker/src/shared/utils/getLocaleMonths/index.ts @@ -0,0 +1,10 @@ +import range from 'lodash/range'; + +import { MonthObject } from '../../types'; +import { getMonthName } from '../getMonthName'; + +export const getLocaleMonths = (locale?: string): Array => { + return range(12).map(monthIndex => { + return getMonthName(monthIndex, locale); + }); +}; diff --git a/packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts b/packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts new file mode 100644 index 0000000000..6ad1124c70 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts @@ -0,0 +1,35 @@ +import { getLocaleWeekdays } from '.'; + +describe('packages/date-picker/utils/getLocaleWeekdays', () => { + test('English (default)', () => { + expect(getLocaleWeekdays()).toHaveLength(7); + getLocaleWeekdays().forEach(w => { + expect(w).not.toBeUndefined(); + expect(w).not.toBeNull(); + }); + }); + + test('iso8601', () => { + expect(getLocaleWeekdays('iso8601')).toHaveLength(7); + getLocaleWeekdays('iso8601').forEach(w => { + expect(w).not.toBeUndefined(); + expect(w).not.toBeNull(); + }); + }); + + test('es-MX', () => { + expect(getLocaleWeekdays('es-MX')).toHaveLength(7); + getLocaleWeekdays('es-MX').forEach(w => { + expect(w).not.toBeUndefined(); + expect(w).not.toBeNull(); + }); + }); + + test('fr-FR', () => { + expect(getLocaleWeekdays('fr-FR')).toHaveLength(7); + getLocaleWeekdays('fr-FR').forEach(w => { + expect(w).not.toBeUndefined(); + expect(w).not.toBeNull(); + }); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getLocaleWeekdays/index.ts b/packages/date-picker/src/shared/utils/getLocaleWeekdays/index.ts new file mode 100644 index 0000000000..9f9d3c4878 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getLocaleWeekdays/index.ts @@ -0,0 +1,11 @@ +import range from 'lodash/range'; + +import { getWeekdayName } from '../getWeekdayName'; + +/** + * Returns an array of weekdays for the provided locale. + * Defaults to system locale + */ +export const getLocaleWeekdays = (locale?: string) => { + return range(7).map(d => getWeekdayName(d, locale)); +}; diff --git a/packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts b/packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts new file mode 100644 index 0000000000..cb6d7968b2 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts @@ -0,0 +1,30 @@ +import { getMonthIndex } from '.'; + +describe('packages/date-picker/utils/getMonthIndex', () => { + test('Default (English) long', () => { + expect(getMonthIndex('January')).toBe(0); + expect(getMonthIndex('December')).toBe(11); + }); + test('Default (English) short', () => { + expect(getMonthIndex('Jan')).toBe(0); + expect(getMonthIndex('Dec')).toBe(11); + }); + + test('Spanish long', () => { + expect(getMonthIndex('enero', 'es-MX')).toBe(0); + expect(getMonthIndex('diciembre', 'es-MX')).toBe(11); + }); + test('Spanish short', () => { + expect(getMonthIndex('ene', 'es-MX')).toBe(0); + expect(getMonthIndex('dic', 'es-MX')).toBe(11); + }); + + test('French long', () => { + expect(getMonthIndex('janvier', 'fr-FR')).toBe(0); + expect(getMonthIndex('décembre', 'fr-FR')).toBe(11); + }); + test('French short', () => { + expect(getMonthIndex('janv.', 'fr-FR')).toBe(0); + expect(getMonthIndex('déc.', 'fr-FR')).toBe(11); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getMonthIndex/index.ts b/packages/date-picker/src/shared/utils/getMonthIndex/index.ts index fc5e94131b..a9889195a2 100644 --- a/packages/date-picker/src/shared/utils/getMonthIndex/index.ts +++ b/packages/date-picker/src/shared/utils/getMonthIndex/index.ts @@ -1,7 +1,14 @@ -import { MonthsArray } from '../../constants'; +import { getLocaleMonths } from '../getLocaleMonths'; -/** Returns the index of an English month name */ -export const getMonthIndex = (monthName: string): number | null => { - const index = MonthsArray.indexOf(monthName); +/** + * Returns the month index of a month name in the current locale + */ +export const getMonthIndex = ( + monthName: string, + locale?: string, +): number | null => { + const index = getLocaleMonths(locale).findIndex(({ long, short }) => + [long, short].includes(monthName), + ); return index >= 0 ? index : null; }; diff --git a/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts b/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts index 65a12944d5..d14c04afb5 100644 --- a/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts +++ b/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts @@ -1,64 +1,81 @@ import { getMonthName } from '.'; describe('packages/date-picker/utils/getMonthName', () => { - test('Jan', () => { + test('Default (English)', () => { expect(getMonthName(0)).toEqual( expect.objectContaining({ long: 'January', short: 'Jan' }), ); - }); - test('Feb', () => { + expect(getMonthName(1)).toEqual( expect.objectContaining({ long: 'February', short: 'Feb' }), ); - }); - test('Mar', () => { + expect(getMonthName(2)).toEqual( expect.objectContaining({ long: 'March', short: 'Mar' }), ); - }); - test('Apr', () => { + expect(getMonthName(3)).toEqual( expect.objectContaining({ long: 'April', short: 'Apr' }), ); - }); - test('May', () => { + expect(getMonthName(4)).toEqual( expect.objectContaining({ long: 'May', short: 'May' }), ); - }); - test('Jun', () => { + expect(getMonthName(5)).toEqual( expect.objectContaining({ long: 'June', short: 'Jun' }), ); - }); - test('Jul', () => { + expect(getMonthName(6)).toEqual( expect.objectContaining({ long: 'July', short: 'Jul' }), ); - }); - test('Aug', () => { + expect(getMonthName(7)).toEqual( expect.objectContaining({ long: 'August', short: 'Aug' }), ); - }); - test('Sep', () => { + expect(getMonthName(8)).toEqual( expect.objectContaining({ long: 'September', short: 'Sep' }), ); - }); - test('Oct', () => { + expect(getMonthName(9)).toEqual( expect.objectContaining({ long: 'October', short: 'Oct' }), ); - }); - test('Nov', () => { + expect(getMonthName(10)).toEqual( expect.objectContaining({ long: 'November', short: 'Nov' }), ); - }); - test('Dec', () => { + expect(getMonthName(11)).toEqual( expect.objectContaining({ long: 'December', short: 'Dec' }), ); }); + + test('iso8601', () => { + expect(getMonthName(0, 'iso8601')).toEqual( + expect.objectContaining({ long: 'January', short: 'Jan' }), + ); + + expect(getMonthName(11, 'iso8601')).toEqual( + expect.objectContaining({ long: 'December', short: 'Dec' }), + ); + }); + + test('es-MX (Spanish)', () => { + expect(getMonthName(0, 'es-MX')).toEqual( + expect.objectContaining({ long: 'enero', short: 'ene' }), + ); + expect(getMonthName(11, 'es-MX')).toEqual( + expect.objectContaining({ long: 'diciembre', short: 'dic' }), + ); + }); + + test('fr-FR (French)', () => { + expect(getMonthName(0, 'fr-FR')).toEqual( + expect.objectContaining({ long: 'janvier', short: 'janv.' }), + ); + expect(getMonthName(11, 'fr-FR')).toEqual( + expect.objectContaining({ long: 'décembre', short: 'déc.' }), + ); + }); }); diff --git a/packages/date-picker/src/shared/utils/getMonthName/index.ts b/packages/date-picker/src/shared/utils/getMonthName/index.ts index 4d50c0a282..b9f68045e8 100644 --- a/packages/date-picker/src/shared/utils/getMonthName/index.ts +++ b/packages/date-picker/src/shared/utils/getMonthName/index.ts @@ -1,18 +1,21 @@ -import padStart from 'lodash/padStart'; - -import { MonthObject } from '../../constants'; +import { MonthObject } from '../../types'; +import { normalizeLocale } from '../normalizeLocale'; /** * Returns the month name from a given index and optional locale */ export const getMonthName = ( monthIndex: number, - locale = 'default', + locale?: string, ): MonthObject => { - const str = `2023-${padStart((monthIndex + 1).toString(), 2, '0')}-15`; - const month = new Date(str); + // Use the default system locale if the provided value is invalid + locale = normalizeLocale(locale); return { - long: month.toLocaleString(locale, { month: 'long' }), - short: month.toLocaleString(locale, { month: 'short' }), + long: new Date(2020, monthIndex, 15).toLocaleString(locale, { + month: 'long', + }), + short: new Date(2020, monthIndex, 15).toLocaleString(locale, { + month: 'short', + }), }; }; diff --git a/packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts b/packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts new file mode 100644 index 0000000000..172db0dcef --- /dev/null +++ b/packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts @@ -0,0 +1,57 @@ +import { getWeekdayName } from '.'; +describe('packages/date-picker/utils/getWeekdayName', () => { + test('default (English)', () => { + expect(getWeekdayName(0)).toEqual( + expect.objectContaining({ + long: 'Sunday', + abbr: 'Sun', + short: 'Su', + narrow: 'S', + }), + ); + expect(getWeekdayName(6)).toEqual( + expect.objectContaining({ + long: 'Saturday', + abbr: 'Sat', + short: 'Sa', + narrow: 'S', + }), + ); + }); + + test('iso8601', () => { + expect(getWeekdayName(0, 'iso8601')).toEqual( + expect.objectContaining({ + long: 'Sunday', + short: 'Su', + }), + ); + }); + + test('es-MX (Spanish)', () => { + expect(getWeekdayName(0, 'es-MX')).toEqual( + expect.objectContaining({ + long: 'domingo', + short: 'do', + }), + ); + }); + + test('fr-FR (French)', () => { + expect(getWeekdayName(0, 'fr-FR')).toEqual( + expect.objectContaining({ + long: 'dimanche', + short: 'di', + }), + ); + }); + + test('de-DE (German)', () => { + expect(getWeekdayName(0, 'de-DE')).toEqual( + expect.objectContaining({ + long: 'Sonntag', + short: 'So', + }), + ); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getWeekdayName/index.ts b/packages/date-picker/src/shared/utils/getWeekdayName/index.ts new file mode 100644 index 0000000000..ab7b62bea4 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getWeekdayName/index.ts @@ -0,0 +1,35 @@ +import { truncate } from 'lodash'; + +import { Month } from '../../constants'; +import { WeekdayObject } from '../../types'; +import { normalizeLocale } from '../normalizeLocale'; + +export const getWeekdayName = ( + day: number, + localeStr?: string, +): WeekdayObject => { + // Use the default system locale if the provided value is invalid + localeStr = normalizeLocale(localeStr); + day = (day % 7) + 1; + const sampleDate = new Date(2000, Month.October, day); // October 1 2000 was a Sunday + + // TODO: format the sample date using `date-fns/format` + // and unicode date field symbols https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + return { + long: sampleDate.toLocaleDateString(localeStr, { + weekday: 'long', + }), + abbr: sampleDate.toLocaleDateString(localeStr, { + weekday: 'short', + }), + short: truncate( + sampleDate.toLocaleDateString(localeStr, { + weekday: 'short', + }), + { length: 2, omission: '' }, + ), + narrow: sampleDate.toLocaleDateString(localeStr, { + weekday: 'narrow', + }), + }; +}; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index 9226442032..a393a53b74 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -11,6 +11,8 @@ export { getFormattedDateString } from './getFormattedDateString'; export { getFullMonthLabel } from './getFullMonthLabel'; export { getISODate } from './getISODate'; export { getLastOfMonth } from './getLastOfMonth'; +export { getLocaleMonths } from './getLocaleMonths'; +export { getLocaleWeekdays } from './getLocaleWeekdays'; export { getMaxSegmentValue } from './getMaxSegmentValue'; export { getMinSegmentValue } from './getMinSegmentValue'; export { getMonthIndex } from './getMonthIndex'; @@ -27,6 +29,7 @@ export { } from './getSegmentsFromDate'; export { getUTCDateString } from './getUTCDateString'; export { getValueFormatter } from './getValueFormatter'; +export { getWeekdayName } from './getWeekdayName'; export { getWeeksArray } from './getWeeksArray'; export { isCurrentUTCDay } from './isCurrentUTCDay'; export { isDefined } from './isDefined'; diff --git a/packages/date-picker/src/shared/utils/isValidLocale/index.ts b/packages/date-picker/src/shared/utils/isValidLocale/index.ts index 987e2a9294..db244827a2 100644 --- a/packages/date-picker/src/shared/utils/isValidLocale/index.ts +++ b/packages/date-picker/src/shared/utils/isValidLocale/index.ts @@ -1,7 +1,7 @@ /** * Returns whether the provided string is a valid Intl.Locale string */ -export function isValidLocale(str?: string): boolean { +export function isValidLocale(str?: string): str is string { if (!str) return false; try { diff --git a/packages/date-picker/src/shared/utils/normalizeLocale/index.ts b/packages/date-picker/src/shared/utils/normalizeLocale/index.ts new file mode 100644 index 0000000000..4e0ce9c075 --- /dev/null +++ b/packages/date-picker/src/shared/utils/normalizeLocale/index.ts @@ -0,0 +1,10 @@ +import { isValidLocale } from '../isValidLocale'; + +/** + * Returns the provided locale, or the resolved locale from the Intl object + */ +export const normalizeLocale = (localeStr?: string): string => { + return isValidLocale(localeStr) + ? localeStr + : Intl.DateTimeFormat().resolvedOptions().locale; +}; diff --git a/packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts b/packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts new file mode 100644 index 0000000000..171c0d7ad6 --- /dev/null +++ b/packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts @@ -0,0 +1,15 @@ +import { normalizeLocale } from '.'; + +describe('packages/date-picker/utils/normalizeLocale', () => { + test('pass through valid locale', () => { + expect(normalizeLocale('de-DE')).toBe('de-DE'); + }); + + test('resolve invalid locale to system', () => { + expect(normalizeLocale('asdf')).toBe('en-US'); + }); + + test('resolve undefined to system', () => { + expect(normalizeLocale()).toBe('en-US'); + }); +}); From 787e9c8570a4948d0d2c20b6548a8cb70d103908 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 7 Dec 2023 18:00:00 -0500 Subject: [PATCH 313/351] mv main story --- .../{DatePicker => }/DatePicker.stories.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) rename packages/date-picker/src/{DatePicker => }/DatePicker.stories.tsx (84%) diff --git a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx similarity index 84% rename from packages/date-picker/src/DatePicker/DatePicker.stories.tsx rename to packages/date-picker/src/DatePicker.stories.tsx index ec197657ad..c9f16d4eec 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -11,24 +11,25 @@ import { Size } from '@leafygreen-ui/tokens'; import { DatePickerContextProps, DatePickerProvider, -} from '../shared/components/DatePickerContext'; -import { Month } from '../shared/constants'; -import { Locales, TimeZones } from '../shared/testutils'; -import { AutoComplete } from '../shared/types'; -import { newUTC } from '../shared/utils'; - +} from './shared/components/DatePickerContext'; +import { Month } from './shared/constants'; +import { + getProviderPropsFromStoryContext, + Locales, + TimeZones, +} from './shared/testutils'; +import { AutoComplete } from './shared/types'; +import { newUTC } from './shared/utils'; import { DatePicker } from './DatePicker'; -import { getProviderPropsFromStoryArgs } from './DatePicker.testutils'; const ProviderWrapper = (Story: StoryFn, ctx: any) => { - const { contextProps, componentProps } = getProviderPropsFromStoryArgs( - ctx?.args, - ); + const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = + getProviderPropsFromStoryContext(ctx?.args); return ( - - - + + + ); From aa511a789648495cd8230f68112a907a975b1e60 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 7 Dec 2023 18:00:03 -0500 Subject: [PATCH 314/351] Delete rollup.config.mjs --- packages/date-picker/rollup.config.mjs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 packages/date-picker/rollup.config.mjs diff --git a/packages/date-picker/rollup.config.mjs b/packages/date-picker/rollup.config.mjs deleted file mode 100644 index f287595312..0000000000 --- a/packages/date-picker/rollup.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { - esmConfig, - storiesConfig, - umdConfig, -} from '@lg-tools/build/config/rollup.config.mjs'; - -const sharedConfig = [esmConfig, umdConfig].map(config => ({ - ...config, - input: 'src/shared/index.ts', -})); - -const config = [esmConfig, umdConfig, ...sharedConfig]; - -export default config; From 6aec3e240fddb24ec1d8effd1f80308567b84ca7 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 7 Dec 2023 18:00:41 -0500 Subject: [PATCH 315/351] remove getProviderPropsFromStoryArgs --- .../src/DatePicker/DatePicker.testutils.tsx | 23 +------------------ .../DatePickerInput.stories.tsx | 15 ++++++------ .../DatePickerMenu/DatePickerMenu.stories.tsx | 23 ++++++++----------- .../getProviderPropsFromStoryContext/index.ts | 3 +-- 4 files changed, 19 insertions(+), 45 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index e57ca6fe76..299ec2e37d 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -9,12 +9,8 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - ContextPropKeys, - contextPropNames, -} from '../shared/components/DatePickerContext'; import { DateSegment } from '../shared/types'; -import { getISODate, pickAndOmit } from '../shared/utils'; +import { getISODate } from '../shared/utils'; import { DatePickerProps } from './DatePicker.types'; import { DatePicker } from '.'; @@ -220,20 +216,3 @@ export const findTabStopElementMap = async ( 'menu > right chevron': rightChevron, }; }; - -export const getProviderPropsFromStoryArgs = ( - args: any, -): { - contextProps: Pick; - componentProps: Omit; -} => { - const [contextProps, componentProps] = pickAndOmit< - DatePickerProps, - ContextPropKeys - >(args, contextPropNames); - - return { - contextProps, - componentProps, - }; -}; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index 11766a3757..ba17c78105 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -10,7 +10,7 @@ import { DatePickerContextProps, DatePickerProvider, } from '../../shared/components/DatePickerContext'; -import { getProviderPropsFromStoryArgs } from '../DatePicker.testutils'; +import { getProviderPropsFromStoryContext } from '../../shared/testutils'; import { DatePickerProps } from '../DatePicker.types'; import { SingleDateContextProps, @@ -20,15 +20,14 @@ import { import { DatePickerInput } from './DatePickerInput'; const ProviderWrapper = (Story: StoryFn, ctx: any) => { - const { contextProps, componentProps } = getProviderPropsFromStoryArgs( - ctx?.args, - ); + const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = + getProviderPropsFromStoryContext(ctx?.args); return ( - - - {}}> - + + + {}}> + diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 97b47949fe..38ea059b70 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -14,12 +14,14 @@ import { contextPropNames, type DatePickerContextProps, DatePickerProvider, - defaultDatePickerContext, } from '../../shared/components/DatePickerContext'; import { Month } from '../../shared/constants'; -import { Locales, TimeZones } from '../../shared/testutils'; +import { + getProviderPropsFromStoryContext, + Locales, + TimeZones, +} from '../../shared/testutils'; import { newUTC } from '../../shared/utils'; -import { getProviderPropsFromStoryArgs } from '../DatePicker.testutils'; import { type SingleDateContextProps, SingleDateProvider, @@ -34,18 +36,13 @@ type DecoratorArgs = DatePickerMenuProps & DatePickerContextProps; const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { - const { contextProps, componentProps } = getProviderPropsFromStoryArgs( - ctx.args, - ); + const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = + getProviderPropsFromStoryContext(ctx.args); return ( - - - + + + ); diff --git a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts index 6aa884e598..c98ce52b60 100644 --- a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts +++ b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts @@ -6,7 +6,6 @@ import { ContextPropKeys, contextPropNames, DatePickerProviderProps, - defaultDatePickerContext, } from '../../components/DatePickerContext'; import { BaseDatePickerProps } from '../../types'; import { pickAndOmit } from '../../utils'; @@ -31,7 +30,7 @@ export const getProviderPropsFromStoryContext =

( baseFontSize: baseFontSize === 13 ? 14 : baseFontSize, }, datePickerProviderProps: { - ...defaultDatePickerContext, + label: '', ...datePickerProviderProps, }, storyProps, From 436349a85cededf7e40195bd5d44d8323a890f14 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 7 Dec 2023 18:03:16 -0500 Subject: [PATCH 316/351] Update DatePicker.stories.tsx --- packages/date-picker/src/DatePicker.stories.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index c9f16d4eec..deec51aa8e 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -13,11 +13,8 @@ import { DatePickerProvider, } from './shared/components/DatePickerContext'; import { Month } from './shared/constants'; -import { - getProviderPropsFromStoryContext, - Locales, - TimeZones, -} from './shared/testutils'; +import { getProviderPropsFromStoryContext } from './shared/testutils/getProviderPropsFromStoryContext'; +import { Locales, TimeZones } from './shared/testutils/testValues'; import { AutoComplete } from './shared/types'; import { newUTC } from './shared/utils'; import { DatePicker } from './DatePicker'; From 5467d9b0aa7159153d9fb2b6342bef0a18fc5c3e Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:14:05 -0500 Subject: [PATCH 317/351] Date picker [LG-3873] rename locale prop (#2122) * reorg types * Update BaseDatePickerProps.types.ts * refactor to locale * Squashed commit of the following: commit 436349a85cededf7e40195bd5d44d8323a890f14 Author: Adam Thompson Date: Thu Dec 7 18:03:16 2023 -0500 Update DatePicker.stories.tsx commit 6aec3e240fddb24ec1d8effd1f80308567b84ca7 Author: Adam Thompson Date: Thu Dec 7 18:00:41 2023 -0500 remove getProviderPropsFromStoryArgs commit aa511a789648495cd8230f68112a907a975b1e60 Author: Adam Thompson Date: Thu Dec 7 18:00:03 2023 -0500 Delete rollup.config.mjs commit 787e9c8570a4948d0d2c20b6548a8cb70d103908 Author: Adam Thompson Date: Thu Dec 7 18:00:00 2023 -0500 mv main story --- .../date-picker/src/DatePicker.stories.tsx | 6 +- .../src/DatePicker/DatePicker.spec.tsx | 32 ++--- .../DatePickerInput.stories.tsx | 4 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 2 +- .../DatePickerMenuHeader.tsx | 8 +- .../SingleDateContext/SingleDateContext.tsx | 6 +- .../CalendarGrid/CalendarGrid.stories.tsx | 6 +- .../Calendar/CalendarGrid/CalendarGrid.tsx | 10 +- .../DateInputBox/DateInputBox.spec.tsx | 12 +- .../DateInputBox/DateInputBox.stories.tsx | 6 +- .../DateInput/DateInputBox/DateInputBox.tsx | 2 +- .../DatePickerContext.spec.tsx | 1 - .../DatePickerContext.utils.ts | 6 +- .../shared/types/BaseDatePickerProps.types.ts | 76 +++++++++++ .../date-picker/src/shared/types/index.ts | 123 ++---------------- .../date-picker/src/shared/types/types.ts | 38 ++++++ .../utils/getWeeksArray/getWeeksArray.spec.ts | 10 +- .../src/shared/utils/getWeeksArray/index.ts | 6 +- 18 files changed, 181 insertions(+), 173 deletions(-) create mode 100644 packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts create mode 100644 packages/date-picker/src/shared/types/types.ts diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index deec51aa8e..b2cba5171a 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -52,7 +52,7 @@ const meta: StoryMetaType = { combineArgs: { darkMode: [false, true], value: [newUTC(2023, Month.December, 26)], - dateFormat: ['iso8601', 'en-US', 'en-UK', 'de-DE'], + locale: ['iso8601', 'en-US', 'en-UK', 'de-DE'], timeZone: ['UTC', 'Europe/London', 'America/New_York', 'Asia/Seoul'], disabled: [false, true], }, @@ -60,14 +60,14 @@ const meta: StoryMetaType = { }, }, args: { - dateFormat: 'iso8601', + locale: 'iso8601', label: 'Pick a date', size: Size.Default, autoComplete: AutoComplete.Off, }, argTypes: { baseFontSize: { control: 'select' }, - dateFormat: { control: 'select', options: Locales }, + locale: { control: 'select', options: Locales }, description: { control: 'text' }, label: { control: 'text' }, min: { control: 'date' }, diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 0cd240b21f..507d475501 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1565,39 +1565,39 @@ describe('packages/date-picker', () => { describe('auto-formatting & auto-focus', () => { describe('for ISO format', () => { - const dateFormat = 'iso8601'; + const locale = 'iso8601'; test('when year value is explicit, focus advances to month', () => { const { yearInput, monthInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(yearInput, '1999'); expect(monthInput).toHaveFocus(); }); test('when year value is before MIN, focus still advances', () => { const { yearInput, monthInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(yearInput, '1944'); expect(monthInput).toHaveFocus(); }); test('when year value is after MAX, focus still advances', () => { const { yearInput, monthInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(yearInput, '2048'); expect(monthInput).toHaveFocus(); }); test('when month value is explicit, focus advances to day', () => { const { monthInput, dayInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(monthInput, '5'); expect(dayInput).toHaveFocus(); }); test('when day value is explicit, format the day', async () => { const { dayInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(dayInput, '5'); expect(dayInput).toHaveFocus(); @@ -1605,20 +1605,20 @@ describe('packages/date-picker', () => { }); test('when year value is ambiguous, focus does NOT advance', () => { - const { yearInput } = renderDatePicker({ dateFormat }); + const { yearInput } = renderDatePicker({ locale }); userEvent.type(yearInput, '200'); expect(yearInput).toHaveFocus(); }); test('when month value is ambiguous, focus does NOT advance', () => { const { monthInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(monthInput, '1'); expect(monthInput).toHaveFocus(); }); test('when day value is ambiguous, segment is NOT formatted', async () => { const { dayInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(dayInput, '2'); expect(dayInput).toHaveFocus(); @@ -1627,25 +1627,25 @@ describe('packages/date-picker', () => { }); describe('for en-US format', () => { - const dateFormat = 'en-US'; + const locale = 'en-US'; test('when month value is explicit, focus advances to day', () => { const { monthInput, dayInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(monthInput, '5'); expect(dayInput).toHaveFocus(); }); test('when day value is explicit, focus advances to year', () => { const { dayInput, yearInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(dayInput, '5'); expect(yearInput).toHaveFocus(); }); test('when year value is explicit, segment value is set', () => { const { yearInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(yearInput, '1999'); expect(yearInput).toHaveFocus(); @@ -1653,18 +1653,18 @@ describe('packages/date-picker', () => { }); test('when month value is ambiguous, focus does NOT advance', () => { - const { monthInput } = renderDatePicker({ dateFormat }); + const { monthInput } = renderDatePicker({ locale }); userEvent.type(monthInput, '1'); expect(monthInput).toHaveFocus(); }); test('when day value is ambiguous, focus does NOT advance', () => { - const { dayInput } = renderDatePicker({ dateFormat }); + const { dayInput } = renderDatePicker({ locale }); userEvent.type(dayInput, '2'); expect(dayInput).toHaveFocus(); }); test('when year value is ambiguous, segment does not format', async () => { const { yearInput } = renderDatePicker({ - dateFormat, + locale, }); userEvent.type(yearInput, '200'); expect(yearInput).toHaveFocus(); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index ba17c78105..0aabb2322e 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -50,7 +50,7 @@ const meta: StoryMetaType< combineArgs: { darkMode: [false, true], value: [null, new Date('1993-12-26')], - dateFormat: ['iso8601', 'en-US', 'en-UK', 'de-DE'], + locale: ['iso8601', 'en-US', 'en-UK', 'de-DE'], size: Object.values(Size), }, decorator: ProviderWrapper, @@ -58,7 +58,7 @@ const meta: StoryMetaType< }, args: { label: 'Label', - dateFormat: 'en-UK', + locale: 'en-UK', timeZone: 'Europe/London', }, argTypes: { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 38ea059b70..1929a94599 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -65,7 +65,7 @@ const meta: StoryMetaType = { }, argTypes: { value: { control: 'date' }, - dateFormat: { control: 'select', options: Locales }, + locale: { control: 'select', options: Locales }, timeZone: { control: 'select', options: [undefined, ...TimeZones] }, }, }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index 1b4dc02f01..39869a3905 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -37,10 +37,10 @@ export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps >(({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { - const { min, max, setIsSelectOpen, dateFormat } = useDatePickerContext(); + const { min, max, setIsSelectOpen, locale } = useDatePickerContext(); const { month } = useSingleDateContext(); - const monthOptions = getLocaleMonths(dateFormat); + const monthOptions = getLocaleMonths(locale); const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); const updateMonth = (newMonth: Date) => { @@ -66,8 +66,8 @@ export const DatePickerMenuHeader = forwardRef< /** Returns whether the provided month should be enabled */ const isMonthEnabled = useCallback( (monthName: string) => - shouldMonthBeEnabled(monthName, { month, min, max, locale: dateFormat }), - [dateFormat, max, min, month], + shouldMonthBeEnabled(monthName, { month, min, max, locale }), + [locale, max, min, month], ); return ( diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx index ddb62dab40..d0fc683bc4 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx @@ -46,7 +46,7 @@ export const SingleDateProvider = ({ disabled, min, max, - dateFormat, + locale, setInternalErrorMessage, clearInternalErrorMessage, isInRange, @@ -109,11 +109,11 @@ export const SingleDateProvider = ({ if (val && !isInRange(val)) { if (isOnOrBefore(val, min)) { setInternalErrorMessage( - `Date must be after ${getFormattedDateString(min, dateFormat)}`, + `Date must be after ${getFormattedDateString(min, locale)}`, ); } else { setInternalErrorMessage( - `Date must be before ${getFormattedDateString(max, dateFormat)}`, + `Date must be before ${getFormattedDateString(max, locale)}`, ); } } else { diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index e635d14dda..0c49d5ca26 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -33,19 +33,19 @@ const meta: StoryMetaType = { generate: { combineArgs: { darkMode: [false, true], - dateFormat: Locales, + locale: Locales, }, decorator: ProviderWrapper, }, }, decorators: [ProviderWrapper], args: { - dateFormat: 'en-US', + locale: 'en-US', timeZone: 'UTC', }, argTypes: { darkMode: { control: 'boolean' }, - dateFormat: { + locale: { control: 'select', options: Locales, }, diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx index 7c3a6c5e52..99f4211f47 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx @@ -36,11 +36,11 @@ import { CalendarGridProps } from './CalendarGrid.types'; */ export const CalendarGrid = forwardRef( ({ month, children, className, ...rest }: CalendarGridProps, fwdRef) => { - const { dateFormat } = useDatePickerContext(); - const weekStartsOn = getWeekStartByLocale(dateFormat); + const { locale } = useDatePickerContext(); + const weekStartsOn = getWeekStartByLocale(locale); const weeks = useMemo( - () => getWeeksArray(month, { dateFormat }), - [dateFormat, month], + () => getWeeksArray(month, { locale }), + [locale, month], ); return ( @@ -54,7 +54,7 @@ export const CalendarGrid = forwardRef(

{range(daysPerWeek).map(i => { const dayIndex = (i + weekStartsOn) % daysPerWeek; - const weekday = getLocaleWeekdays(dateFormat)[dayIndex]; + const weekday = getLocaleWeekdays(locale)[dayIndex]; return (
- {day.short} + {weekday.short ?? weekday.abbr}
{ const onSegmentChange = jest.fn(); const testContext: Partial = { - dateFormat: 'iso8601', + locale: 'iso8601', timeZone: 'UTC', }; @@ -71,7 +71,7 @@ describe('packages/date-picker/shared/date-input-box', () => { describe('renders segments in the correct order', () => { test('iso8601', () => { - const result = renderDateInputBox(undefined, { dateFormat: 'iso8601' }); + const result = renderDateInputBox(undefined, { locale: 'iso8601' }); const segments = result.getAllByRole('spinbutton'); expect(segments[0]).toHaveAttribute('aria-label', 'year'); expect(segments[1]).toHaveAttribute('aria-label', 'month'); @@ -79,7 +79,7 @@ describe('packages/date-picker/shared/date-input-box', () => { }); test('en-US', () => { - const result = renderDateInputBox(undefined, { dateFormat: 'en-US' }); + const result = renderDateInputBox(undefined, { locale: 'en-US' }); const segments = result.getAllByRole('spinbutton'); expect(segments[0]).toHaveAttribute('aria-label', 'month'); expect(segments[1]).toHaveAttribute('aria-label', 'day'); @@ -87,7 +87,7 @@ describe('packages/date-picker/shared/date-input-box', () => { }); test('en-UK', () => { - const result = renderDateInputBox(undefined, { dateFormat: 'en-UK' }); + const result = renderDateInputBox(undefined, { locale: 'en-UK' }); const segments = result.getAllByRole('spinbutton'); expect(segments[0]).toHaveAttribute('aria-label', 'day'); expect(segments[1]).toHaveAttribute('aria-label', 'month'); @@ -198,7 +198,7 @@ describe('packages/date-picker/shared/date-input-box', () => { describe('Mouse interaction', () => { test('click on segment focuses it', () => { const { dayInput } = renderDateInputBox(undefined, { - dateFormat: 'iso8601', + locale: 'iso8601', }); userEvent.click(dayInput); expect(dayInput).toHaveFocus(); @@ -209,7 +209,7 @@ describe('packages/date-picker/shared/date-input-box', () => { test('Tab moves focus to next segment', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( undefined, - { dateFormat: 'iso8601' }, + { locale: 'iso8601' }, ); userEvent.click(yearInput); userEvent.tab(); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 5ccdd5f5c5..bd06222bd7 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -54,12 +54,12 @@ const meta: StoryMetaType = { }, args: { label: 'Label', - dateFormat: 'iso8601', + locale: 'iso8601', timeZone: 'Europe/London', }, argTypes: { value: { control: 'date' }, - dateFormat: { control: 'select', options: Locales }, + locale: { control: 'select', options: Locales }, }, }; @@ -98,7 +98,7 @@ export const Formats: StoryType< Formats.parameters = { generate: { combineArgs: { - dateFormat: ['iso8601', 'en-US', 'en-UK', 'de-DE'], + locale: ['iso8601', 'en-US', 'en-UK', 'de-DE'], }, }, }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index f76f36c136..9107c261c3 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -38,7 +38,7 @@ import { DateInputBoxProps } from './DateInputBox.types'; * * Depends on {@link DateInputSegment} * - * Uses parameters `value` & `dateFormat` along with {@link Intl.DateTimeFormat.prototype.formatToParts} + * Uses parameters `value` & `locale` along with {@link Intl.DateTimeFormat.prototype.formatToParts} * to determine the segment order and separator characters. * * Provided value is assumed to be UTC. diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx index f0b5864540..32731dbba4 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx @@ -21,7 +21,6 @@ const renderDatePickerProvider = () => { return { result, rerender }; }; -// TODO: ADD MORE TESTS describe('packages/date-picker-context', () => { describe('useDatePickerContext', () => { describe('isOpen', () => { diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts index ec1344fbd8..3b0ea946e5 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts @@ -26,7 +26,7 @@ export type ContextPropKeys = keyof DatePickerProviderProps & export const contextPropNames: Array = [ 'label', 'description', - 'dateFormat', + 'locale', 'timeZone', 'min', 'max', @@ -43,7 +43,7 @@ export const contextPropNames: Array = [ export const defaultDatePickerContext: DatePickerContextProps = { label: '', description: '', - dateFormat: 'iso8601', + locale: 'iso8601', timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, min: MIN_DATE, max: MAX_DATE, @@ -105,7 +105,7 @@ export const getContextProps = ( const isInRange = getIsInRange(providerValue.min, providerValue.max); // Only used to track the _order_ of segments, not the value itself - const formatParts = getFormatParts(providerValue.dateFormat); + const formatParts = getFormatParts(providerValue.locale); return { ...providerValue, isInRange, formatParts }; }; diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts new file mode 100644 index 0000000000..e8b6055406 --- /dev/null +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -0,0 +1,76 @@ +import { DarkModeProps } from '@leafygreen-ui/lib'; +import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; + +import { AutoComplete, DatePickerState } from './types'; +export interface BaseDatePickerProps extends DarkModeProps { + /** + * A label for the input + */ + label: React.ReactNode; + + /** + * A description for the date picker. + * + * It's recommended to set a meaningful time zone representation as the description (e.g. "Coordinated Universal Time") + */ + description?: React.ReactNode; + + /** + * Sets the _presentation format_ for the displayed date. + * Fallback to the user’s browser preference (if supported), otherwise ISO-8601. + * + * Currently only the following values are officially supported: 'en-US' | 'en-GB' | 'iso8601' + * Other valid [Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) + * strings may work, however no assurances are made. + * + * @default 'iso8601' + */ + locale?: 'iso8601' | string; + + /** + * A valid IANA timezone string, or UTC offset. + * Sets the _presentation time zone_ for the displayed date. + * Fallback to the user’s browser preference (if available), otherwise UTC. + * + * @default 'utc' + */ + timeZone?: string; + + /** The earliest date accepted */ + min?: string | Date; + + /** The latest date accepted */ + max?: string | Date; + + /** + * The base font size of the input. Inherits from the nearest LeafyGreenProvider + */ + baseFontSize?: BaseFontSize; + + /** + * Whether the input is disabled. Note: will not set the `disabled` attribute on an input and the calendar menu will not open if disabled is set to true. + */ + disabled?: boolean; + + /** The size of the input */ + size?: Size; + + /** + * Whether to show an error message + */ + state?: DatePickerState; + + /** + * A message to show in red underneath the input when state is Error + */ + errorMessage?: string; + + /** Whether the calendar menu is initially open. Note: The calendar menu will not open if disabled is set to true. */ + initialOpen?: boolean; + + /** + * Whether the input should autofill + * @default 'off' + */ + autoComplete?: AutoComplete; +} diff --git a/packages/date-picker/src/shared/types/index.ts b/packages/date-picker/src/shared/types/index.ts index 75490239d7..74b3318b28 100644 --- a/packages/date-picker/src/shared/types/index.ts +++ b/packages/date-picker/src/shared/types/index.ts @@ -1,120 +1,15 @@ -import omit from 'lodash/omit'; - -import { FormFieldState } from '@leafygreen-ui/form-field'; -import { DarkModeProps } from '@leafygreen-ui/lib'; -import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; - +export { type BaseDatePickerProps } from './BaseDatePickerProps.types'; export { DateSegment, type DateSegmentsState, type DateSegmentValue, isDateSegment, } from './DateSegment.types'; - -export const DatePickerState = omit(FormFieldState, 'Valid'); -export type DatePickerState = - (typeof DatePickerState)[keyof typeof DatePickerState]; - -export type DateType = Date | null; -export type DateRangeType = [DateType, DateType]; - -export interface MonthObject { - long: string; - short: string; -} - -/** - * Object representing the abbreviations of a given weekday. - * Abbreviation formats defined in Unicode: https://www.unicode.org/reports/tr35/tr35-67/tr35-dates.html#dfst-weekday - */ -export interface WeekdayObject { - /** The long-form weekday name (e.g. Tuesday)*/ - long: string; - /** An abbreviated weekday name (e.g. Tue) */ - abbr: string; - /** A shorter weekday name (e.g. Tu)*/ - short?: string; - /** The shortest weekday name (e.g. T) */ - narrow: string; -} - -export const AutoComplete = { - Off: 'off', - On: 'on', - Bday: 'bday', -} as const; - -export type AutoComplete = (typeof AutoComplete)[keyof typeof AutoComplete]; - -export interface BaseDatePickerProps extends DarkModeProps { - /** - * A label for the input - */ - label: React.ReactNode; - - /** - * A description for the date picker. - * - * It's recommended to set a meaningful time zone representation as the description (e.g. "Coordinated Universal Time") - */ - description?: React.ReactNode; - - /** - * Sets the _presentation format_ for the displayed date. - * Fallback to the user’s browser preference (if supported), otherwise ISO-8601. - * - * Currently only the following values are officially supported: 'en-US' | 'en-GB' | 'iso8601' - * Other valid [Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) - * strings may work, however no assurances are made. - * - * @default 'iso8601' - */ - dateFormat?: 'iso8601' | string; - - /** - * A valid IANA timezone string, or UTC offset. - * Sets the _presentation time zone_ for the displayed date. - * Fallback to the user’s browser preference (if available), otherwise UTC. - * - * @default 'utc' - */ - timeZone?: string; - - /** The earliest date accepted */ - min?: string | Date; - - /** The latest date accepted */ - max?: string | Date; - - /** - * The base font size of the input. Inherits from the nearest LeafyGreenProvider - */ - baseFontSize?: BaseFontSize; - - /** - * Whether the input is disabled. Note: will not set the `disabled` attribute on an input and the calendar menu will not open if disabled is set to true. - */ - disabled?: boolean; - - /** The size of the input */ - size?: Size; - - /** - * Whether to show an error message - */ - state?: DatePickerState; - - /** - * A message to show in red underneath the input when state is Error - */ - errorMessage?: string; - - /** Whether the calendar menu is initially open. Note: The calendar menu will not open if disabled is set to true. */ - initialOpen?: boolean; - - /** - * Whether the input should autofill - * @default 'off' - */ - autoComplete?: AutoComplete; -} +export { + AutoComplete, + DatePickerState, + type DateRangeType, + type DateType, + type MonthObject, + type WeekdayObject, +} from './types'; diff --git a/packages/date-picker/src/shared/types/types.ts b/packages/date-picker/src/shared/types/types.ts new file mode 100644 index 0000000000..70875da515 --- /dev/null +++ b/packages/date-picker/src/shared/types/types.ts @@ -0,0 +1,38 @@ +import omit from 'lodash/omit'; + +import { FormFieldState } from '@leafygreen-ui/form-field'; + +export const DatePickerState = omit(FormFieldState, 'Valid'); +export type DatePickerState = + (typeof DatePickerState)[keyof typeof DatePickerState]; + +export type DateType = Date | null; +export type DateRangeType = [DateType, DateType]; + +export interface MonthObject { + long: string; + short: string; +} + +/** + * Object representing the abbreviations of a given weekday. + * Abbreviation formats defined in Unicode: https://www.unicode.org/reports/tr35/tr35-67/tr35-dates.html#dfst-weekday + */ +export interface WeekdayObject { + /** The long-form weekday name (e.g. Tuesday)*/ + long: string; + /** An abbreviated weekday name (e.g. Tue) */ + abbr: string; + /** A shorter weekday name (e.g. Tu)*/ + short?: string; + /** The shortest weekday name (e.g. T) */ + narrow: string; +} + +export const AutoComplete = { + Off: 'off', + On: 'on', + Bday: 'bday', +} as const; + +export type AutoComplete = (typeof AutoComplete)[keyof typeof AutoComplete]; diff --git a/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts b/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts index 8ddb766a50..e733758726 100644 --- a/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts +++ b/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts @@ -7,7 +7,7 @@ import { getWeeksArray } from '.'; describe('packages/date-picker/utils/getWeeksArray', () => { test('starts the week on the correct day for the locale', () => { const month = new Date(Date.UTC(2023, Month.August, 1)); - const arr = getWeeksArray(month, { dateFormat: 'en-US' }); + const arr = getWeeksArray(month, { locale: 'en-US' }); expect(arr[0][0]).toBeNull(); // Sun expect(arr[0][1]).toBeNull(); // Mon expect(arr[0][2]).not.toBeNull(); // Tues @@ -15,7 +15,7 @@ describe('packages/date-picker/utils/getWeeksArray', () => { describe('August 2023', () => { const aug23 = new Date(Date.UTC(2023, Month.August, 1)); - const arr = getWeeksArray(aug23, { dateFormat: 'iso-8601' }); + const arr = getWeeksArray(aug23, { locale: 'iso-8601' }); test('returned array has the correct number of elements', () => { const daysInAugust = 31; @@ -88,7 +88,7 @@ describe('packages/date-picker/utils/getWeeksArray', () => { describe('September 2023', () => { const sept23 = new Date(Date.UTC(2023, Month.September, 1)); - const arr = getWeeksArray(sept23, { dateFormat: 'iso8601' }); + const arr = getWeeksArray(sept23, { locale: 'iso8601' }); test('returned array has the correct number of elements', () => { const daysInSept = 30; @@ -166,7 +166,7 @@ describe('packages/date-picker/utils/getWeeksArray', () => { describe('February 2023', () => { const feb23 = new Date(Date.UTC(2023, Month.February, 1)); - const arr = getWeeksArray(feb23, { dateFormat: 'iso8601' }); + const arr = getWeeksArray(feb23, { locale: 'iso8601' }); test('returned array has the correct number of elements', () => { const daysInFeb23 = 28; @@ -238,7 +238,7 @@ describe('packages/date-picker/utils/getWeeksArray', () => { describe('February 2024 (leap-year)', () => { const feb23 = new Date(Date.UTC(2024, Month.February, 1)); - const arr = getWeeksArray(feb23, { dateFormat: 'iso8601' }); + const arr = getWeeksArray(feb23, { locale: 'iso8601' }); test('returned array has the correct number of elements', () => { const daysInFeb24 = 29; diff --git a/packages/date-picker/src/shared/utils/getWeeksArray/index.ts b/packages/date-picker/src/shared/utils/getWeeksArray/index.ts index 92f1e9f38e..98670fd0d9 100644 --- a/packages/date-picker/src/shared/utils/getWeeksArray/index.ts +++ b/packages/date-picker/src/shared/utils/getWeeksArray/index.ts @@ -9,7 +9,7 @@ import { getDaysInUTCMonth } from '../getDaysInUTCMonth'; import { setToUTCMidnight } from '../setToUTCMidnight'; interface GetWeeksArrayOptions - extends Required> {} + extends Required> {} /** * Returns a 7x5 (or 7x6) 2D array of Dates for the given month @@ -20,10 +20,10 @@ export const getWeeksArray = ( */ month: Date, - { dateFormat }: GetWeeksArrayOptions, + { locale }: GetWeeksArrayOptions, ): Array> => { // What day of the week do weeks start on for this locale? (Sun = 0) - const weekStartsOn = getWeekStartByLocale(dateFormat); + const weekStartsOn = getWeekStartByLocale(locale); // The first day of the month const firstOfMonth = setToUTCMidnight(month); From c6ad642b9c8073a899fa311fb73c8b1ca7e9b9ff Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 8 Dec 2023 11:42:44 -0500 Subject: [PATCH 318/351] Date Picker [LG-3844] add propTypes (#2124) * add propTypes * remove src --- .../date-picker/src/DatePicker/DatePicker.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index bfcb12b691..01eab92c42 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -1,11 +1,13 @@ import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; -import { BaseFontSize } from '@leafygreen-ui/tokens'; +import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; +import { AutoComplete, DatePickerState } from '../shared'; import { ContextPropKeys, contextPropNames, @@ -73,3 +75,25 @@ export const DatePicker = forwardRef( ); DatePicker.displayName = 'DatePicker'; + +DatePicker.propTypes = { + value: PropTypes.instanceOf(Date), + onDateChange: PropTypes.func, + initialValue: PropTypes.instanceOf(Date), + handleValidation: PropTypes.func, + onChange: PropTypes.func, + label: PropTypes.node, + description: PropTypes.node, + locale: PropTypes.string, + timeZone: PropTypes.string, + min: PropTypes.string || PropTypes.instanceOf(Date), + max: PropTypes.string || PropTypes.instanceOf(Date), + baseFontSize: PropTypes.oneOf(Object.values(BaseFontSize)), + disabled: PropTypes.bool, + size: PropTypes.oneOf(Object.values(Size)), + state: PropTypes.oneOf(Object.values(DatePickerState)), + errorMessage: PropTypes.string, + initialOpen: PropTypes.bool, + autoComplete: PropTypes.oneOf(Object.values(AutoComplete)), + darkMode: PropTypes.bool, +}; From 4fed5f83593e2cd1d9b8a56e57a469ccf1818175 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 8 Dec 2023 15:09:02 -0500 Subject: [PATCH 319/351] Date Picker [LG-3852] invalid chevron (#2123) * wip * clean up a little * move function to utils * add test for util * add tests * update arrow aria label * remove dup * add interaction test * address pr comments * remove comment --- .../src/DatePicker/DatePicker.spec.tsx | 163 +++++++++++++++--- .../src/DatePicker/DatePicker.testutils.tsx | 6 +- .../DatePickerMenuHeader.tsx | 60 +++++-- .../DatePickerMenuHeader/utils/index.ts | 2 + .../utils/shouldChevronBeDisabled/index.ts | 23 +++ .../isChevronDisabled.spec.ts | 146 ++++++++++++++++ 6 files changed, 366 insertions(+), 34 deletions(-) create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/index.ts create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts create mode 100644 packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 507d475501..9b9a1888a1 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -293,36 +293,127 @@ describe('packages/date-picker', () => { }); describe('Chevrons', () => { - test('Left is disabled if prev. month is entirely out of range', async () => { - const { openMenu } = renderDatePicker({ - min: new Date(Date.UTC(2023, Month.December, 1)), + describe('left', () => { + describe('is disabled', () => { + test('when the value is before the min', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 1)), + value: new Date(Date.UTC(2022, Month.December, 1)), + }); + + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'true'); + }); + test('when the value is the same as the min', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 10)), + value: new Date(Date.UTC(2023, Month.December, 1)), + }); + + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'true'); + }); + test('min and max are in the same month', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 1)), + max: new Date(Date.UTC(2023, Month.December, 20)), + value: new Date(Date.UTC(2023, Month.December, 5)), + }); + + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'true'); + }); }); - const { leftChevron } = await openMenu(); - expect(leftChevron).toHaveAttribute('aria-disabled', 'true'); - }); + describe('is not disabled', () => { + test('when the year and month is after the max', async () => { + const { openMenu } = renderDatePicker({ + max: new Date(Date.UTC(2024, Month.January, 2)), + value: new Date(Date.UTC(2025, Month.December, 1)), + }); - test('Right is disabled if next month is entirely out of range', async () => { - const { openMenu } = renderDatePicker({ - max: new Date(Date.UTC(2023, Month.December, 31)), + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'false'); + }); + test('when the year and month is the same as the max', async () => { + const { openMenu } = renderDatePicker({ + max: new Date(Date.UTC(2024, Month.January, 2)), + value: new Date(Date.UTC(2024, Month.January, 1)), + }); + + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'false'); + }); + test('when the year is the same as the max and the month is after the max', async () => { + const { openMenu } = renderDatePicker({ + max: new Date(Date.UTC(2024, Month.January, 2)), + value: new Date(Date.UTC(2024, Month.February, 1)), + }); + + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveAttribute('aria-disabled', 'false'); + }); }); - const { rightChevron } = await openMenu(); - expect(rightChevron).toHaveAttribute('aria-disabled', 'true'); }); + describe('right', () => { + describe('is disabled', () => { + test('when the value is after the max', async () => { + const { openMenu } = renderDatePicker({ + max: new Date(Date.UTC(2024, Month.January, 2)), + value: new Date(Date.UTC(2025, Month.December, 1)), + }); - test('Left is not disabled if only part of prev. month is in range', async () => { - const { openMenu } = renderDatePicker({ - min: new Date(Date.UTC(2023, Month.November, 29)), + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'true'); + }); + test('when the value is the same as the max', async () => { + const { openMenu } = renderDatePicker({ + max: new Date(Date.UTC(2024, Month.January, 2)), + value: new Date(Date.UTC(2024, Month.January, 1)), + }); + + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'true'); + }); + test('min and max are in the same month', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 1)), + max: new Date(Date.UTC(2023, Month.December, 20)), + value: new Date(Date.UTC(2023, Month.December, 5)), + }); + + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'true'); + }); }); - const { leftChevron } = await openMenu(); - expect(leftChevron).toHaveAttribute('aria-disabled', 'false'); - }); + describe('is not disabled', () => { + test('when the year and month is before the min', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 1)), + value: new Date(Date.UTC(2022, Month.December, 1)), + }); - test('Right is not disabled if only part of next month is in of range', async () => { - const { openMenu } = renderDatePicker({ - max: new Date(Date.UTC(2024, Month.January, 2)), + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'false'); + }); + test('when the year and month is the same as the min', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 10)), + value: new Date(Date.UTC(2023, Month.December, 1)), + }); + + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'false'); + }); + test('when the year is the same as the min and the month before the min', async () => { + const { openMenu } = renderDatePicker({ + min: new Date(Date.UTC(2023, Month.December, 1)), + value: new Date(Date.UTC(2023, Month.November, 1)), + }); + + const { rightChevron } = await openMenu(); + expect(rightChevron).toHaveAttribute('aria-disabled', 'false'); + }); }); - const { rightChevron } = await openMenu(); - expect(rightChevron).toHaveAttribute('aria-disabled', 'false'); }); }); }); @@ -514,6 +605,20 @@ describe('packages/date-picker', () => { expect(yearSelect).toHaveValue('2022'); }); + test('updates the displayed month to the max month and year when the value is after the max', async () => { + const { openMenu } = renderDatePicker({ + max: newUTC(2022, Month.January, 5), + value: newUTC(2023, Month.January, 5), + }); + const { leftChevron, monthSelect, yearSelect, calendarGrid } = + await openMenu(); + expect(calendarGrid).toHaveAttribute('aria-label', 'January 2023'); + userEvent.click(leftChevron!); + expect(calendarGrid).toHaveAttribute('aria-label', 'January 2022'); + expect(monthSelect).toHaveValue(Month.January.toString()); + expect(yearSelect).toHaveValue('2022'); + }); + test('keeps focus on chevron button', async () => { const { openMenu } = renderDatePicker(); const { leftChevron } = await openMenu(); @@ -553,6 +658,20 @@ describe('packages/date-picker', () => { expect(monthSelect).toHaveValue(Month.January.toString()); expect(yearSelect).toHaveValue('2024'); }); + + test('updates the displayed month to the min month and year when the value is before the min ', async () => { + const { openMenu } = renderDatePicker({ + min: newUTC(2023, Month.December, 26), + value: newUTC(2022, Month.November, 26), + }); + const { rightChevron, monthSelect, yearSelect, calendarGrid } = + await openMenu(); + expect(calendarGrid).toHaveAttribute('aria-label', 'November 2022'); + userEvent.click(rightChevron!); + expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); + expect(monthSelect).toHaveValue(Month.December.toString()); + expect(yearSelect).toHaveValue('2023'); + }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 299ec2e37d..83fe5e1294 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -112,9 +112,11 @@ export const renderDatePicker = ( const calendarCells = withinElement(menuContainerEl)?.getAllByRole('gridcell'); const leftChevron = - withinElement(menuContainerEl)?.queryByLabelText('Previous month'); + withinElement(menuContainerEl)?.queryByLabelText('Previous month') || + withinElement(menuContainerEl)?.queryByLabelText('Previous valid month'); const rightChevron = - withinElement(menuContainerEl)?.queryByLabelText('Next month'); + withinElement(menuContainerEl)?.queryByLabelText('Next month') || + withinElement(menuContainerEl)?.queryByLabelText('Next valid month'); const monthSelect = withinElement(menuContainerEl)?.queryByLabelText('Select month'); const yearSelect = diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index 39869a3905..12cb796526 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -22,7 +22,7 @@ import { selectTruncateStyles, } from '../DatePickerMenu.styles'; -import { shouldMonthBeEnabled } from './utils/shouldMonthBeEnabled'; +import { shouldChevronBeDisabled, shouldMonthBeEnabled } from './utils'; interface DatePickerMenuHeaderProps { setMonth: (newMonth: Date) => void; @@ -37,7 +37,8 @@ export const DatePickerMenuHeader = forwardRef< HTMLDivElement, DatePickerMenuHeaderProps >(({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { - const { min, max, setIsSelectOpen, locale } = useDatePickerContext(); + const { min, max, setIsSelectOpen, locale, isInRange } = + useDatePickerContext(); const { month } = useSingleDateContext(); const monthOptions = getLocaleMonths(locale); @@ -49,6 +50,27 @@ export const DatePickerMenuHeader = forwardRef< setMonth(newMonth); }; + /** + * If the month is not in range and is not the last valid month + * e.g. + * This is not in range and is not the last valid month + * min: new Date(Date.UTC(2038, Month.March, 19)); + * current month date: new Date(Date.UTC(2038, Month.Feburary, 19)); + * + * This is not in range but it is the last valid month + * min: new Date(Date.UTC(2038, Month.March, 19)); + * current month date: new Date(Date.UTC(2038, Month.March, 18)); + */ + const isMonthInValid = (dir: 'left' | 'right') => { + const isOnLastValidMonth = isSameUTCMonth( + month, + dir === 'left' ? max : min, + ); + const isDateInRange = isInRange(month); + + return !isDateInRange && !isOnLastValidMonth; + }; + /** * Calls the `updateMonth` helper with the appropriate month when a Chevron is clicked */ @@ -57,10 +79,26 @@ export const DatePickerMenuHeader = forwardRef< e => { e.stopPropagation(); e.preventDefault(); - const increment = dir === 'left' ? -1 : 1; - const newMonthIndex = month.getUTCMonth() + increment; - const newMonth = setUTCMonth(month, newMonthIndex); - updateMonth(newMonth); + + // e.g. + // max: new Date(Date.UTC(2038, Month.January, 19)); + // current month date: new Date(Date.UTC(2038, Month.March, 19)); + // left chevron will change the month back to January 2038 + // e.g. + // min: new Date(Date.UTC(1970, Month.January, 1)); + // current month date: new Date(Date.UTC(1969, Month.November, 19)); + // right chevron will change the month back to January 1970 + if (isMonthInValid(dir)) { + const closestValidDate = dir === 'left' ? max : min; + const newMonthIndex = closestValidDate.getUTCMonth(); + const newMonth = setUTCMonth(closestValidDate, newMonthIndex); + updateMonth(newMonth); + } else { + const increment = dir === 'left' ? -1 : 1; + const newMonthIndex = month.getUTCMonth() + increment; + const newMonth = setUTCMonth(month, newMonthIndex); + updateMonth(newMonth); + } }; /** Returns whether the provided month should be enabled */ @@ -73,8 +111,10 @@ export const DatePickerMenuHeader = forwardRef< return (
@@ -125,8 +165,8 @@ export const DatePickerMenuHeader = forwardRef<
diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/index.ts new file mode 100644 index 0000000000..de83ccca8a --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/index.ts @@ -0,0 +1,2 @@ +export { shouldChevronBeDisabled } from './shouldChevronBeDisabled'; +export { shouldMonthBeEnabled } from './shouldMonthBeEnabled'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts new file mode 100644 index 0000000000..102ee392d3 --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts @@ -0,0 +1,23 @@ +import { isSameUTCMonth } from '../../../../../shared/utils'; + +/** + * Checks if chevron should be disabled + * + * @param direction left or right chevron + * @param day1 the full date of the current month shown in the menu (month) + * @param day2 the full date that current menu date is compared against (min/max) + * @returns + */ +export const shouldChevronBeDisabled = ( + direction: 'left' | 'right', + day1: Date, + day2: Date, +): boolean => { + if (!day1 || !day2) return false; + + if (direction === 'right') { + return day1 >= day2 || isSameUTCMonth(day1, day2); + } + + return day1 <= day2 || isSameUTCMonth(day1, day2); +}; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts new file mode 100644 index 0000000000..92173d2feb --- /dev/null +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts @@ -0,0 +1,146 @@ +import { Month } from '../../../../../shared/constants'; + +import { shouldChevronBeDisabled } from '.'; + +const testMinDate = new Date(Date.UTC(1970, Month.February, 20)); +const testMaxDate = new Date(Date.UTC(2037, Month.February, 20)); + +const beforeMinDateDiffYear = new Date(Date.UTC(1969, Month.February, 20)); +const beforeMinDateSameMonth = new Date(Date.UTC(1970, Month.February, 19)); +const beforeMinDateSameYearDiffMonth = new Date( + Date.UTC(1970, Month.January, 20), +); + +const afterMinDateSameMonth = new Date(Date.UTC(1970, Month.February, 21)); +const afterMinDateSameYear = new Date(Date.UTC(1970, Month.March, 20)); +const afterMinDateDifferentYear = testMaxDate; + +const afterMaxDateSameMonth = new Date(Date.UTC(2037, Month.February, 21)); +const afterMaxDateDiffYear = new Date(Date.UTC(2038, Month.February, 20)); +const afterMaxDateSameYearDiffMonth = new Date(Date.UTC(2037, Month.March, 20)); + +const beforeMaxDateSameMonth = new Date(Date.UTC(2037, Month.February, 19)); +const beforeMaxDateSameYear = new Date(Date.UTC(2037, Month.January, 20)); +const beforeMaxDateDiffYear = testMinDate; + +describe('packages/date-picker/menu/utils/shouldMonthBeEnabled', () => { + describe('left chevron', () => { + describe('returns true', () => { + describe('when the menu date is before the minDate', () => { + test('but is in a month that has both valid and invalid dates', () => { + expect( + shouldChevronBeDisabled( + 'left', + beforeMinDateSameMonth, + testMinDate, + ), + ).toBeTruthy(); + }); + test('and is in a different year', () => { + expect( + shouldChevronBeDisabled('left', beforeMinDateDiffYear, testMinDate), + ).toBeTruthy(); + }); + test('and is in the same year and different month', () => { + expect( + shouldChevronBeDisabled( + 'left', + beforeMinDateSameYearDiffMonth, + testMinDate, + ), + ).toBeTruthy(); + }); + }); + describe('when the menu date is after the minDate', () => { + test('but is in a month that has both valid and invalid dates', () => { + expect( + shouldChevronBeDisabled('left', afterMinDateSameMonth, testMinDate), + ).toBeTruthy(); + }); + }); + }); + + describe('returns false', () => { + describe('when the menu date is after the minDate', () => { + test('and is in the same year', () => { + expect( + shouldChevronBeDisabled('left', afterMinDateSameYear, testMinDate), + ).toBeFalsy(); + }); + test('and is in a different year', () => { + expect( + shouldChevronBeDisabled( + 'left', + afterMinDateDifferentYear, + testMinDate, + ), + ).toBeFalsy(); + }); + }); + }); + }); + + describe('right chevron', () => { + describe('returns true', () => { + describe('when the menu date is after the maxDate', () => { + test('but is in a month that has both valid and invalid dates', () => { + expect( + shouldChevronBeDisabled( + 'right', + afterMaxDateSameMonth, + testMaxDate, + ), + ).toBeTruthy(); + }); + test('and is in a different year', () => { + expect( + shouldChevronBeDisabled('right', afterMaxDateDiffYear, testMaxDate), + ).toBeTruthy(); + }); + test('and is in the same year and different month', () => { + expect( + shouldChevronBeDisabled( + 'right', + afterMaxDateSameYearDiffMonth, + testMaxDate, + ), + ).toBeTruthy(); + }); + }); + describe('when the menu date is before the maxDate', () => { + test('but is in a month that has both valid and invalid dates', () => { + expect( + shouldChevronBeDisabled( + 'right', + beforeMaxDateSameMonth, + testMaxDate, + ), + ).toBeTruthy(); + }); + }); + }); + + describe('returns false', () => { + describe('when the menu date is before the maxDate', () => { + test('and is in the same year', () => { + expect( + shouldChevronBeDisabled( + 'right', + beforeMaxDateSameYear, + testMaxDate, + ), + ).toBeFalsy(); + }); + test('and is in a different year', () => { + expect( + shouldChevronBeDisabled( + 'right', + beforeMaxDateDiffYear, + testMaxDate, + ), + ).toBeFalsy(); + }); + }); + }); + }); +}); From 25cfd862d2ad4426f3a0521e3dc04e41514276cc Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Fri, 8 Dec 2023 15:24:03 -0500 Subject: [PATCH 320/351] Date picker [LG-3860, LG-3856] min/max bugs (#2125) * creates sortDates util * Sort min/max dates * adds sorting min/max tests * use default dates if min > defaultMax (& V-V) * add tests for updating min/max * listen for changes in min/max & revalidate * update re-validateion logic * use default if min > max --- .../src/DatePicker/DatePicker.spec.tsx | 38 ++++++++- .../DatePickerContent/DatePickerContent.tsx | 19 ++++- .../DatePickerContext.spec.tsx | 74 ++++++++++++++++- .../DatePickerContext.utils.ts | 80 ++++++++++++++++--- .../src/shared/utils/sortDates/index.ts | 20 +++++ .../shared/utils/sortDates/sortDates.spec.ts | 40 ++++++++++ 6 files changed, 257 insertions(+), 14 deletions(-) create mode 100644 packages/date-picker/src/shared/utils/sortDates/index.ts create mode 100644 packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 9b9a1888a1..97784e2e20 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -182,7 +182,7 @@ describe('packages/date-picker', () => { test('removing an external error displays an internal error when applicable', () => { const { inputContainer, rerenderDatePicker, getByTestId } = renderDatePicker({ - value: newUTC(2100, 1, 1), + value: newUTC(2100, Month.January, 1), }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( @@ -202,6 +202,42 @@ describe('packages/date-picker', () => { 'Date must be before 2038-01-19', ); }); + + test('internal error message updates when min value changes', () => { + const { inputContainer, rerenderDatePicker, getByTestId } = + renderDatePicker({ + value: newUTC(1967, Month.March, 10), + }); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + const errorElement = getByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + 'Date must be after 1970-01-01', + ); + + rerenderDatePicker({ min: newUTC(1968, Month.July, 5) }); + + expect(errorElement).toHaveTextContent( + 'Date must be after 1968-07-05', + ); + }); + + test('internal error message updates when max value changes', () => { + const { inputContainer, rerenderDatePicker, getByTestId } = + renderDatePicker({ + value: newUTC(2050, Month.January, 1), + }); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + const errorElement = getByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + 'Date must be before 2038-01-19', + ); + + rerenderDatePicker({ max: newUTC(2048, Month.July, 5) }); + + expect(errorElement).toHaveTextContent( + 'Date must be before 2048-07-05', + ); + }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx index 18b2f10739..0d38a4050d 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx @@ -27,7 +27,8 @@ export const DatePickerContent = forwardRef< HTMLDivElement, DatePickerContentProps >(({ ...rest }: DatePickerContentProps, fwdRef) => { - const { isOpen, menuId, disabled, isSelectOpen } = useDatePickerContext(); + const { min, max, isOpen, menuId, disabled, isSelectOpen } = + useDatePickerContext(); const { refs, value, @@ -38,6 +39,8 @@ export const DatePickerContent = forwardRef< } = useSingleDateContext(); const prevValue = usePrevious(value); + const prevMin = usePrevious(min); + const prevMax = usePrevious(max); const formFieldRef = useForwardedRef(fwdRef, null); const menuRef = useRef(null); @@ -116,11 +119,23 @@ export const DatePickerContent = forwardRef< /** When value changes, validate it */ useEffect(() => { - if (!isEqual(prevValue, value) && !isSameUTCDay(prevValue, value)) { + if (!isEqual(prevValue, value) && !isSameUTCDay(value, prevValue)) { handleValidation(value); } }, [handleValidation, prevValue, value]); + /** + * If min/max changes, re-validate the value + */ + useEffect(() => { + if ( + (prevMin && !isSameUTCDay(min, prevMin)) || + (prevMax && !isSameUTCDay(max, prevMax)) + ) { + handleValidation(value); + } + }, [min, max, value, prevMin, prevMax, handleValidation]); + return ( <> { +const renderDatePickerProvider = (props?: Partial) => { const { result, rerender } = renderHook< PropsWithChildren<{}>, DatePickerContextProps >(useDatePickerContext, { wrapper: ({ children }) => ( - {children} + + {children} + ), }); @@ -23,6 +31,68 @@ const renderDatePickerProvider = () => { describe('packages/date-picker-context', () => { describe('useDatePickerContext', () => { + describe('min/max', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + test('uses default min/max values when not provided', () => { + const { result } = renderDatePickerProvider(); + expect(result.current.min).toEqual(MIN_DATE); + expect(result.current.max).toEqual(MAX_DATE); + }); + + test('uses provided min/max values', () => { + const testMin = newUTC(1999, Month.September, 2); + const testMax = newUTC(2011, Month.June, 22); + + const { result } = renderDatePickerProvider({ + min: testMin, + max: testMax, + }); + expect(result.current.min).toEqual(testMin); + expect(result.current.max).toEqual(testMax); + }); + + test('if min is after max, uses default & console errors', () => { + const errorSpy = jest.spyOn(consoleOnce, 'error'); + + const testMax = newUTC(1999, Month.September, 2); + const testMin = newUTC(2011, Month.June, 22); + + const { result } = renderDatePickerProvider({ + min: testMin, + max: testMax, + }); + expect(result.current.min).toEqual(MIN_DATE); + expect(result.current.max).toEqual(MAX_DATE); + expect(errorSpy).toHaveBeenCalled(); + }); + + test('if max is before default min, uses default & console errors', () => { + const errorSpy = jest.spyOn(consoleOnce, 'error'); + const testMax = newUTC(1967, Month.March, 10); + + const { result } = renderDatePickerProvider({ + max: testMax, + }); + expect(result.current.min).toEqual(MIN_DATE); + expect(result.current.max).toEqual(MAX_DATE); + expect(errorSpy).toHaveBeenCalled(); + }); + + test('if min is after default max, uses default & console errors', () => { + const errorSpy = jest.spyOn(consoleOnce, 'error'); + const testMin = newUTC(2067, Month.March, 10); + + const { result } = renderDatePickerProvider({ + min: testMin, + }); + expect(result.current.min).toEqual(MIN_DATE); + expect(result.current.max).toEqual(MAX_DATE); + expect(errorSpy).toHaveBeenCalled(); + }); + }); + describe('isOpen', () => { test('is `false` by default', () => { const { result } = renderDatePickerProvider(); diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts index 3b0ea946e5..8c1bdb4aa5 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts @@ -1,7 +1,8 @@ -import { isWithinInterval } from 'date-fns'; +import { isBefore, isWithinInterval } from 'date-fns'; import defaults from 'lodash/defaults'; import defaultTo from 'lodash/defaultTo'; +import { consoleOnce } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; import { MAX_DATE, MIN_DATE } from '../../constants'; @@ -10,7 +11,7 @@ import { BaseDatePickerProps, DatePickerState, } from '../../types'; -import { getFormatParts, toDate } from '../../utils'; +import { getFormatParts, getISODate, toDate } from '../../utils'; import { DatePickerContextProps, @@ -91,15 +92,25 @@ export const getIsInRange = export const getContextProps = ( providerProps: DatePickerProviderProps, ): DatePickerContextProps => { - const { min, max, timeZone, ...rest } = providerProps; + const { + min: minProp, + max: maxProp, + timeZone: tzProp, + ...rest + } = providerProps; + + const timeZone = defaultTo( + tzProp, + Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + + const [min, max] = getMinMax(toDate(minProp), toDate(maxProp)); + const providerValue: DatePickerContextProps = { ...defaults(rest, defaultDatePickerContext), - timeZone: defaultTo( - timeZone, - Intl.DateTimeFormat().resolvedOptions().timeZone, - ), - min: defaultTo(toDate(min), defaultDatePickerContext.min), - max: defaultTo(toDate(max), defaultDatePickerContext.max), + timeZone, + min, + max, }; const isInRange = getIsInRange(providerValue.min, providerValue.max); @@ -109,3 +120,54 @@ export const getContextProps = ( return { ...providerValue, isInRange, formatParts }; }; + +const getMinMax = (min: Date | null, max: Date | null): [Date, Date] => { + const defaultRange: [Date, Date] = [ + defaultDatePickerContext.min, + defaultDatePickerContext.max, + ]; + + // if both are defined + if (min && max) { + if (isBefore(max, min)) { + consoleOnce.error( + `LeafyGreen DatePicker: Provided max date (${getISODate( + max, + )}) is before provided min date (${getISODate( + min, + )}). Using default values.`, + ); + return defaultRange; + } + + return [min, max]; + } else if (min) { + if (isBefore(defaultDatePickerContext.max, min)) { + consoleOnce.error( + `LeafyGreen DatePicker: Provided min date (${getISODate( + min, + )}) is after the default max date (${getISODate( + defaultDatePickerContext.max, + )}). Using default values.`, + ); + return defaultRange; + } + + return [min, defaultDatePickerContext.max]; + } else if (max) { + if (isBefore(max, defaultDatePickerContext.min)) { + consoleOnce.error( + `LeafyGreen DatePicker: Provided max date (${getISODate( + max, + )}) is before the default min date (${getISODate( + defaultDatePickerContext.min, + )}). Using default values.`, + ); + return defaultRange; + } + + return [defaultDatePickerContext.min, max]; + } + + return defaultRange; +}; diff --git a/packages/date-picker/src/shared/utils/sortDates/index.ts b/packages/date-picker/src/shared/utils/sortDates/index.ts new file mode 100644 index 0000000000..a67f0d2a24 --- /dev/null +++ b/packages/date-picker/src/shared/utils/sortDates/index.ts @@ -0,0 +1,20 @@ +import { isBefore, isSameDay } from 'date-fns'; + +/** + * Sorts an array of dates. + * Sorts ascending by default + * + * @param dates `Array` + * @param direction `'ascending' | 'descending'` + * @default 'ascending' + * @returns Sorted `Array` + */ +export const sortDates = ( + dates: Array, + direction: 'ascending' | 'descending' = 'ascending', +): Array => { + const dir = direction === 'ascending' ? -1 : 1; + return dates.sort((a, b) => { + return isBefore(a, b) ? dir : isSameDay(a, b) ? 0 : -dir; + }); +}; diff --git a/packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts b/packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts new file mode 100644 index 0000000000..b86b1ae3fb --- /dev/null +++ b/packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts @@ -0,0 +1,40 @@ +import { Month } from '../../constants'; +import { newUTC } from '../newUTC'; + +import { sortDates } from '.'; + +describe('packages/date-picker/utils/sortDates', () => { + const testDates = [ + newUTC(2023, Month.September, 10), + newUTC(2020, Month.March, 13), + newUTC(2023, Month.September, 10), + newUTC(2020, Month.March, 10), + ]; + + test('ascending', () => { + expect(sortDates(testDates, 'ascending')).toEqual([ + newUTC(2020, Month.March, 10), + newUTC(2020, Month.March, 13), + newUTC(2023, Month.September, 10), + newUTC(2023, Month.September, 10), + ]); + }); + + test('descending', () => { + expect(sortDates(testDates, 'descending')).toEqual([ + newUTC(2023, Month.September, 10), + newUTC(2023, Month.September, 10), + newUTC(2020, Month.March, 13), + newUTC(2020, Month.March, 10), + ]); + }); + + test('ascending by default', () => { + expect(sortDates(testDates)).toEqual([ + newUTC(2020, Month.March, 10), + newUTC(2020, Month.March, 13), + newUTC(2023, Month.September, 10), + newUTC(2023, Month.September, 10), + ]); + }); +}); From 8ea3c0d4e705b5ffe4048190b7250101966ad7f4 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 8 Dec 2023 15:47:39 -0500 Subject: [PATCH 321/351] Delete DateFormField.spec.tsx --- .../DateInput/DateFormField/DateFormField.spec.tsx | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx deleted file mode 100644 index eb56af637e..0000000000 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.spec.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { DateFormField } from '.'; - -describe('packages/date-picker/shared/date-form-field', () => { - test('renders', () => {}); -}); From b08a5c419454e21eb38d347ace5a2e872020c8b2 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 8 Dec 2023 17:14:53 -0500 Subject: [PATCH 322/351] Create eighty-kings-mix.md --- .changeset/eighty-kings-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eighty-kings-mix.md diff --git a/.changeset/eighty-kings-mix.md b/.changeset/eighty-kings-mix.md new file mode 100644 index 0000000000..64910cab2c --- /dev/null +++ b/.changeset/eighty-kings-mix.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/date-picker': major +--- + +Inital release of `date-picker`. Use DatePicker to allow users to input a date From 7001dff8a289fe324dde44e1140dc6f52c94e9cb Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:12:25 -0500 Subject: [PATCH 323/351] DatePicker: DateUtils (#2126) * scaffolds new package * mv utils * update utils index * mv getFormattedDateString * package dependencies * move common types & constants * mv normalizeLocale * update utils imports * mv generic utils to lib * export & import from lib * rm duplicated pickAndOmit * Update README.md * update utils imports * hooks & test utils * shared components * Update tsconfig.json * update component imports * constants imports * fix internal utils * updates changeset * consolidate meta changes * Create smart-steaks-care.md * rm date-fns-tz * update test suite names * Adds coverage reporting for untested sub-modules * mv utils to named file * Adds missing test files * Adds missing utils tests * rm setToMidnight * adds min/max date tests * lint --- .changeset/big-carpets-sleep.md | 3 +- .changeset/eleven-donkeys-tickle.md | 5 ++ .changeset/proud-doors-smoke.md | 8 +- .changeset/slimy-camels-dance.md | 5 -- .changeset/smart-steaks-care.md | 5 ++ .changeset/wild-feet-press.md | 5 -- packages/date-picker/package.json | 2 +- .../date-picker/src/DatePicker.stories.tsx | 3 +- .../src/DatePicker/DatePicker.spec.tsx | 14 ++-- .../src/DatePicker/DatePicker.testutils.tsx | 3 +- .../date-picker/src/DatePicker/DatePicker.tsx | 2 +- .../src/DatePicker/DatePicker.types.ts | 4 +- .../DatePickerContent/DatePickerContent.tsx | 2 +- .../DatePickerInput/DatePickerInput.spec.tsx | 4 +- .../DatePickerInput/DatePickerInput.tsx | 4 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 9 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 3 +- .../DatePickerMenu/DatePickerMenu.tsx | 20 ++--- .../DatePickerMenuHeader.spec.tsx | 2 +- .../DatePickerMenuHeader.tsx | 12 +-- .../utils/shouldChevronBeDisabled/index.ts | 2 +- .../isChevronDisabled.spec.ts | 2 +- .../utils/shouldMonthBeEnabled/index.ts | 3 +- .../shouldMonthBeEnabled.spec.ts | 2 +- .../utils/getNewHighlight/index.ts | 2 +- .../SingleDateContext/SingleDateContext.tsx | 12 +-- .../SingleDateContext.types.ts | 3 +- .../getInitialHighlight.spec.ts | 2 +- .../utils/getInitialHighlight/index.ts | 2 +- .../CalendarGrid/CalendarGrid.stories.tsx | 8 +- .../Calendar/CalendarGrid/CalendarGrid.tsx | 7 +- .../DateInputBox/DateInputBox.spec.tsx | 4 +- .../DateInputBox/DateInputBox.stories.tsx | 3 +- .../DateInputBox/DateInputBox.types.ts | 2 +- .../DatePickerContext.spec.tsx | 4 +- .../DatePickerContext.utils.ts | 3 +- packages/date-picker/src/shared/constants.ts | 20 +---- .../hooks/useDateSegments/useDateSegments.ts | 8 +- .../getProviderPropsFromStoryContext/index.ts | 2 +- .../mockTimeZone/mockTimeZone.spec.ts | 3 +- .../shared/types/BaseDatePickerProps.types.ts | 3 +- .../date-picker/src/shared/types/index.ts | 9 +- .../date-picker/src/shared/types/types.ts | 23 ----- .../utils/doesSomeSegmentExist/index.ts | 3 +- .../src/shared/utils/getFormatParts/index.ts | 2 +- .../getFormattedDateString.spec.ts | 3 +- .../getMaxSegmentValue.spec.ts | 3 +- .../shared/utils/getMaxSegmentValue/index.ts | 4 +- .../getMinSegmentValue.spec.ts | 3 +- .../shared/utils/getMinSegmentValue/index.ts | 4 +- .../shared/utils/getRelativeSegment/index.ts | 6 +- .../shared/utils/getRemainingParts/index.ts | 2 +- .../shared/utils/getValueFormatter/index.ts | 3 +- .../date-picker/src/shared/utils/index.ts | 36 -------- .../shared/utils/newDateFromSegments/index.ts | 3 +- .../src/shared/utils/pickAndOmit/index.ts | 23 ----- packages/date-picker/tsconfig.json | 3 + packages/date-utils/README.md | 21 +++++ packages/date-utils/package.json | 41 +++++++++ .../src}/addDaysUTC/addDaysUTC.spec.ts | 4 +- .../src/addDaysUTC/addDaysUTC.ts} | 0 packages/date-utils/src/addDaysUTC/index.ts | 1 + .../src/addMonthsUTC/addMonthsUTC.spec.ts | 54 ++++++++++++ .../src/addMonthsUTC/addMonthsUTC.ts} | 0 packages/date-utils/src/addMonthsUTC/index.ts | 1 + packages/date-utils/src/constants.ts | 18 ++++ .../getDaysInUTCMonth.spec.ts | 2 +- .../getDaysInUTCMonth/getDaysInUTCMonth.ts} | 0 .../date-utils/src/getDaysInUTCMonth/index.ts | 1 + .../getFirstOfMonth/getFirstOfMonth.spec.ts | 2 +- .../src/getFirstOfMonth/getFirstOfMonth.ts} | 0 .../date-utils/src/getFirstOfMonth/index.ts | 1 + .../getFullMonthLabel.spec.ts | 4 +- .../getFullMonthLabel/getFullMonthLabel.ts} | 0 .../date-utils/src/getFullMonthLabel/index.ts | 1 + .../src}/getISODate/getISODate.spec.ts | 4 +- .../src/getISODate/getISODate.ts} | 2 +- packages/date-utils/src/getISODate/index.ts | 1 + .../getLastOfMonth/getLastOfMonth.spec.ts | 4 +- .../src/getLastOfMonth/getLastOfMonth.ts} | 0 .../date-utils/src/getLastOfMonth/index.ts | 1 + .../getLocaleMonths/getLocaleMonths.spec.ts | 2 +- .../src/getLocaleMonths/getLocaleMonths.ts} | 2 +- .../date-utils/src/getLocaleMonths/index.ts | 1 + .../getLocaleWeekdays.spec.ts | 2 +- .../getLocaleWeekdays/getLocaleWeekdays.ts} | 0 .../date-utils/src/getLocaleWeekdays/index.ts | 1 + .../src}/getMonthIndex/getMonthIndex.spec.ts | 2 +- .../src/getMonthIndex/getMonthIndex.ts} | 0 .../date-utils/src/getMonthIndex/index.ts | 1 + .../src}/getMonthName/getMonthName.spec.ts | 2 +- .../src/getMonthName/getMonthName.ts} | 2 +- packages/date-utils/src/getMonthName/index.ts | 1 + .../getUTCDateString/getUTCDateString.spec.ts | 4 +- .../src/getUTCDateString/getUTCDateString.ts} | 0 .../date-utils/src/getUTCDateString/index.ts | 1 + .../getWeekdayName/getWeekdayName.spec.ts | 2 +- .../src/getWeekdayName/getWeekdayName.ts} | 4 +- .../date-utils/src/getWeekdayName/index.ts | 1 + .../src}/getWeeksArray/getWeeksArray.spec.ts | 4 +- .../src/getWeeksArray/getWeeksArray.ts} | 9 +- .../date-utils/src/getWeeksArray/index.ts | 1 + packages/date-utils/src/index.ts | 34 ++++++++ .../date-utils/src/isCurrentUTCDay/index.ts | 1 + .../isCurrentUTCDay/isCurrentUTCDay.spec.ts | 24 ++++++ .../src/isCurrentUTCDay/isCurrentUTCDay.ts} | 0 packages/date-utils/src/isOnOrAfter/index.ts | 1 + .../src/isOnOrAfter/isOnOrAfter.spec.ts | 22 +++++ .../src/isOnOrAfter/isOnOrAfter.ts} | 0 packages/date-utils/src/isOnOrBefore/index.ts | 1 + .../src/isOnOrBefore/isOnOrBefore.spec.ts | 22 +++++ .../src/isOnOrBefore/isOnOrBefore.ts} | 1 - packages/date-utils/src/isSameTZDay/index.ts | 1 + .../src}/isSameTZDay/isSameTZDay.spec.ts | 4 +- .../src/isSameTZDay/isSameTZDay.ts} | 0 packages/date-utils/src/isSameUTCDay/index.ts | 1 + .../src}/isSameUTCDay/isSameUTCDay.spec.ts | 2 +- .../src/isSameUTCDay/isSameUTCDay.ts} | 0 .../date-utils/src/isSameUTCMonth/index.ts | 1 + .../isSameUTCMonth/isSameUTCMonth.spec.ts | 4 +- .../src/isSameUTCMonth/isSameUTCMonth.ts} | 0 .../date-utils/src/isSameUTCRange/index.ts | 1 + .../src/isSameUTCRange/isSameUTCRange.spec.ts | 84 +++++++++++++++++++ .../src/isSameUTCRange/isSameUTCRange.ts} | 0 packages/date-utils/src/isTodayTZ/index.ts | 1 + .../src}/isTodayTZ/isTodayTZ.spec.ts | 4 +- .../src/isTodayTZ/isTodayTZ.ts} | 0 packages/date-utils/src/isValidDate/index.ts | 5 ++ .../src}/isValidDate/isValidDate.spec.ts | 2 +- .../src/isValidDate/isValidDate.ts} | 0 .../date-utils/src/isValidLocale/index.ts | 1 + .../src}/isValidLocale/isValidLocale.spec.ts | 2 +- .../src/isValidLocale/isValidLocale.ts} | 0 packages/date-utils/src/maxDate/index.ts | 1 + .../date-utils/src/maxDate/maxDate.spec.ts | 42 ++++++++++ .../src/maxDate/maxDate.ts} | 6 +- packages/date-utils/src/minDate/index.ts | 1 + .../date-utils/src/minDate/minDate.spec.ts | 42 ++++++++++ .../src/minDate/minDate.ts} | 6 +- packages/date-utils/src/newUTC/index.ts | 1 + .../src}/newUTC/newUTC.spec.ts | 2 +- .../src/newUTC/newUTC.ts} | 0 .../date-utils/src/normalizeLocale/index.ts | 1 + .../normalizeLocale/normalizeLocale.spec.ts | 2 +- .../src/normalizeLocale/normalizeLocale.ts} | 0 .../date-utils/src/setToUTCMidnight/index.ts | 1 + .../setToUTCMidnight/setToUTCMidnight.spec.ts | 2 +- .../src/setToUTCMidnight/setToUTCMidnight.ts} | 10 --- packages/date-utils/src/setUTCDate/index.ts | 1 + .../src/setUTCDate/setUTCDate.spec.ts | 22 +++++ .../src/setUTCDate/setUTCDate.ts} | 0 packages/date-utils/src/setUTCMonth/index.ts | 1 + .../src}/setUTCMonth/setUTCMonth.spec.ts | 4 +- .../src/setUTCMonth/setUTCMonth.ts} | 0 packages/date-utils/src/setUTCYear/index.ts | 1 + .../src/setUTCYear/setUTCYear.spec.ts | 22 +++++ .../src/setUTCYear/setUTCYear.ts} | 0 packages/date-utils/src/sortDates/index.ts | 1 + .../src}/sortDates/sortDates.spec.ts | 4 +- .../src/sortDates/sortDates.ts} | 0 packages/date-utils/src/toDate/index.ts | 1 + .../src}/toDate/toDate.spec.ts | 2 +- .../src/toDate/toDate.ts} | 0 packages/date-utils/src/types.ts | 24 ++++++ packages/date-utils/tsconfig.json | 25 ++++++ .../cloneReverse/cloneReverse.spec.ts | 0 .../src/helpers}/cloneReverse/index.ts | 0 packages/lib/src/helpers/index.ts | 7 +- .../src/helpers}/isDefined/index.ts | 0 .../src/helpers}/isZeroLike/index.ts | 0 tools/test/config/jest.config.js | 12 ++- tools/test/src/index.ts | 9 ++ 172 files changed, 742 insertions(+), 277 deletions(-) create mode 100644 .changeset/eleven-donkeys-tickle.md delete mode 100644 .changeset/slimy-camels-dance.md create mode 100644 .changeset/smart-steaks-care.md delete mode 100644 .changeset/wild-feet-press.md delete mode 100644 packages/date-picker/src/shared/utils/pickAndOmit/index.ts create mode 100644 packages/date-utils/README.md create mode 100644 packages/date-utils/package.json rename packages/{date-picker/src/shared/utils => date-utils/src}/addDaysUTC/addDaysUTC.spec.ts (95%) rename packages/{date-picker/src/shared/utils/addDaysUTC/index.ts => date-utils/src/addDaysUTC/addDaysUTC.ts} (100%) create mode 100644 packages/date-utils/src/addDaysUTC/index.ts create mode 100644 packages/date-utils/src/addMonthsUTC/addMonthsUTC.spec.ts rename packages/{date-picker/src/shared/utils/addMonthsUTC/index.ts => date-utils/src/addMonthsUTC/addMonthsUTC.ts} (100%) create mode 100644 packages/date-utils/src/addMonthsUTC/index.ts create mode 100644 packages/date-utils/src/constants.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts (94%) rename packages/{date-picker/src/shared/utils/getDaysInUTCMonth/index.ts => date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.ts} (100%) create mode 100644 packages/date-utils/src/getDaysInUTCMonth/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getFirstOfMonth/getFirstOfMonth.spec.ts (91%) rename packages/{date-picker/src/shared/utils/getFirstOfMonth/index.ts => date-utils/src/getFirstOfMonth/getFirstOfMonth.ts} (100%) create mode 100644 packages/date-utils/src/getFirstOfMonth/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getFullMonthLabel/getFullMonthLabel.spec.ts (67%) rename packages/{date-picker/src/shared/utils/getFullMonthLabel/index.ts => date-utils/src/getFullMonthLabel/getFullMonthLabel.ts} (100%) create mode 100644 packages/date-utils/src/getFullMonthLabel/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getISODate/getISODate.spec.ts (76%) rename packages/{date-picker/src/shared/utils/getISODate/index.ts => date-utils/src/getISODate/getISODate.ts} (90%) create mode 100644 packages/date-utils/src/getISODate/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getLastOfMonth/getLastOfMonth.spec.ts (91%) rename packages/{date-picker/src/shared/utils/getLastOfMonth/index.ts => date-utils/src/getLastOfMonth/getLastOfMonth.ts} (100%) create mode 100644 packages/date-utils/src/getLastOfMonth/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getLocaleMonths/getLocaleMonths.spec.ts (91%) rename packages/{date-picker/src/shared/utils/getLocaleMonths/index.ts => date-utils/src/getLocaleMonths/getLocaleMonths.ts} (85%) create mode 100644 packages/date-utils/src/getLocaleMonths/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getLocaleWeekdays/getLocaleWeekdays.spec.ts (93%) rename packages/{date-picker/src/shared/utils/getLocaleWeekdays/index.ts => date-utils/src/getLocaleWeekdays/getLocaleWeekdays.ts} (100%) create mode 100644 packages/date-utils/src/getLocaleWeekdays/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getMonthIndex/getMonthIndex.spec.ts (93%) rename packages/{date-picker/src/shared/utils/getMonthIndex/index.ts => date-utils/src/getMonthIndex/getMonthIndex.ts} (100%) create mode 100644 packages/date-utils/src/getMonthIndex/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getMonthName/getMonthName.spec.ts (97%) rename packages/{date-picker/src/shared/utils/getMonthName/index.ts => date-utils/src/getMonthName/getMonthName.ts} (92%) create mode 100644 packages/date-utils/src/getMonthName/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getUTCDateString/getUTCDateString.spec.ts (83%) rename packages/{date-picker/src/shared/utils/getUTCDateString/index.ts => date-utils/src/getUTCDateString/getUTCDateString.ts} (100%) create mode 100644 packages/date-utils/src/getUTCDateString/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getWeekdayName/getWeekdayName.spec.ts (94%) rename packages/{date-picker/src/shared/utils/getWeekdayName/index.ts => date-utils/src/getWeekdayName/getWeekdayName.ts} (92%) create mode 100644 packages/date-utils/src/getWeekdayName/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/getWeeksArray/getWeeksArray.spec.ts (99%) rename packages/{date-picker/src/shared/utils/getWeeksArray/index.ts => date-utils/src/getWeeksArray/getWeeksArray.ts} (89%) create mode 100644 packages/date-utils/src/getWeeksArray/index.ts create mode 100644 packages/date-utils/src/index.ts create mode 100644 packages/date-utils/src/isCurrentUTCDay/index.ts create mode 100644 packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts rename packages/{date-picker/src/shared/utils/isCurrentUTCDay/index.ts => date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts} (100%) create mode 100644 packages/date-utils/src/isOnOrAfter/index.ts create mode 100644 packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts rename packages/{date-picker/src/shared/utils/isOnOrAfter/index.ts => date-utils/src/isOnOrAfter/isOnOrAfter.ts} (100%) create mode 100644 packages/date-utils/src/isOnOrBefore/index.ts create mode 100644 packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts rename packages/{date-picker/src/shared/utils/isOnOrBefore/index.ts => date-utils/src/isOnOrBefore/isOnOrBefore.ts} (93%) create mode 100644 packages/date-utils/src/isSameTZDay/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isSameTZDay/isSameTZDay.spec.ts (97%) rename packages/{date-picker/src/shared/utils/isSameTZDay/index.ts => date-utils/src/isSameTZDay/isSameTZDay.ts} (100%) create mode 100644 packages/date-utils/src/isSameUTCDay/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isSameUTCDay/isSameUTCDay.spec.ts (96%) rename packages/{date-picker/src/shared/utils/isSameUTCDay/index.ts => date-utils/src/isSameUTCDay/isSameUTCDay.ts} (100%) create mode 100644 packages/date-utils/src/isSameUTCMonth/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isSameUTCMonth/isSameUTCMonth.spec.ts (95%) rename packages/{date-picker/src/shared/utils/isSameUTCMonth/index.ts => date-utils/src/isSameUTCMonth/isSameUTCMonth.ts} (100%) create mode 100644 packages/date-utils/src/isSameUTCRange/index.ts create mode 100644 packages/date-utils/src/isSameUTCRange/isSameUTCRange.spec.ts rename packages/{date-picker/src/shared/utils/isSameUTCRange/index.ts => date-utils/src/isSameUTCRange/isSameUTCRange.ts} (100%) create mode 100644 packages/date-utils/src/isTodayTZ/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isTodayTZ/isTodayTZ.spec.ts (97%) rename packages/{date-picker/src/shared/utils/isTodayTZ/index.ts => date-utils/src/isTodayTZ/isTodayTZ.ts} (100%) create mode 100644 packages/date-utils/src/isValidDate/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isValidDate/isValidDate.spec.ts (94%) rename packages/{date-picker/src/shared/utils/isValidDate/index.ts => date-utils/src/isValidDate/isValidDate.ts} (100%) create mode 100644 packages/date-utils/src/isValidLocale/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/isValidLocale/isValidLocale.spec.ts (88%) rename packages/{date-picker/src/shared/utils/isValidLocale/index.ts => date-utils/src/isValidLocale/isValidLocale.ts} (100%) create mode 100644 packages/date-utils/src/maxDate/index.ts create mode 100644 packages/date-utils/src/maxDate/maxDate.spec.ts rename packages/{date-picker/src/shared/utils/maxDate/index.ts => date-utils/src/maxDate/maxDate.ts} (72%) create mode 100644 packages/date-utils/src/minDate/index.ts create mode 100644 packages/date-utils/src/minDate/minDate.spec.ts rename packages/{date-picker/src/shared/utils/minDate/index.ts => date-utils/src/minDate/minDate.ts} (72%) create mode 100644 packages/date-utils/src/newUTC/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/newUTC/newUTC.spec.ts (88%) rename packages/{date-picker/src/shared/utils/newUTC/index.ts => date-utils/src/newUTC/newUTC.ts} (100%) create mode 100644 packages/date-utils/src/normalizeLocale/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/normalizeLocale/normalizeLocale.spec.ts (84%) rename packages/{date-picker/src/shared/utils/normalizeLocale/index.ts => date-utils/src/normalizeLocale/normalizeLocale.ts} (100%) create mode 100644 packages/date-utils/src/setToUTCMidnight/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/setToUTCMidnight/setToUTCMidnight.spec.ts (84%) rename packages/{date-picker/src/shared/utils/setToUTCMidnight/index.ts => date-utils/src/setToUTCMidnight/setToUTCMidnight.ts} (53%) create mode 100644 packages/date-utils/src/setUTCDate/index.ts create mode 100644 packages/date-utils/src/setUTCDate/setUTCDate.spec.ts rename packages/{date-picker/src/shared/utils/setUTCDate/index.ts => date-utils/src/setUTCDate/setUTCDate.ts} (100%) create mode 100644 packages/date-utils/src/setUTCMonth/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/setUTCMonth/setUTCMonth.spec.ts (92%) rename packages/{date-picker/src/shared/utils/setUTCMonth/index.ts => date-utils/src/setUTCMonth/setUTCMonth.ts} (100%) create mode 100644 packages/date-utils/src/setUTCYear/index.ts create mode 100644 packages/date-utils/src/setUTCYear/setUTCYear.spec.ts rename packages/{date-picker/src/shared/utils/setUTCYear/index.ts => date-utils/src/setUTCYear/setUTCYear.ts} (100%) create mode 100644 packages/date-utils/src/sortDates/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/sortDates/sortDates.spec.ts (91%) rename packages/{date-picker/src/shared/utils/sortDates/index.ts => date-utils/src/sortDates/sortDates.ts} (100%) create mode 100644 packages/date-utils/src/toDate/index.ts rename packages/{date-picker/src/shared/utils => date-utils/src}/toDate/toDate.spec.ts (93%) rename packages/{date-picker/src/shared/utils/toDate/index.ts => date-utils/src/toDate/toDate.ts} (100%) create mode 100644 packages/date-utils/src/types.ts create mode 100644 packages/date-utils/tsconfig.json rename packages/{date-picker/src/shared/utils => lib/src/helpers}/cloneReverse/cloneReverse.spec.ts (100%) rename packages/{date-picker/src/shared/utils => lib/src/helpers}/cloneReverse/index.ts (100%) rename packages/{date-picker/src/shared/utils => lib/src/helpers}/isDefined/index.ts (100%) rename packages/{date-picker/src/shared/utils => lib/src/helpers}/isZeroLike/index.ts (100%) diff --git a/.changeset/big-carpets-sleep.md b/.changeset/big-carpets-sleep.md index 9069a8ddfd..256de82ac7 100644 --- a/.changeset/big-carpets-sleep.md +++ b/.changeset/big-carpets-sleep.md @@ -2,4 +2,5 @@ '@lg-tools/meta': minor --- -Adds `exitWithErrorMessage` util +- Adds `exitWithErrorMessage` util +- Fixes recursion in `findPackageJson` diff --git a/.changeset/eleven-donkeys-tickle.md b/.changeset/eleven-donkeys-tickle.md new file mode 100644 index 0000000000..9a37937d1e --- /dev/null +++ b/.changeset/eleven-donkeys-tickle.md @@ -0,0 +1,5 @@ +--- +'@lg-tools/test': minor +--- + +Adds coverage reporting for untested sub-modules diff --git a/.changeset/proud-doors-smoke.md b/.changeset/proud-doors-smoke.md index 95f3ac743d..7345c7fa57 100644 --- a/.changeset/proud-doors-smoke.md +++ b/.changeset/proud-doors-smoke.md @@ -1,5 +1,9 @@ --- '@leafygreen-ui/lib': minor --- - -Creates `rollover` utility function +- Creates new utility functions + - `rollover` + - `truncateStart` + - `cloneReverse` + - `isDefined` + - `isZeroLike` & `isNotZeroLike` diff --git a/.changeset/slimy-camels-dance.md b/.changeset/slimy-camels-dance.md deleted file mode 100644 index e31f8c5988..0000000000 --- a/.changeset/slimy-camels-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@leafygreen-ui/lib': minor ---- - -Creates `truncateStart` utility function diff --git a/.changeset/smart-steaks-care.md b/.changeset/smart-steaks-care.md new file mode 100644 index 0000000000..0f43ab654c --- /dev/null +++ b/.changeset/smart-steaks-care.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/date-utils': major +--- + +Initial release of `date-utils`. DateUtils contains utility functions for managing and manipulating JS Date objects diff --git a/.changeset/wild-feet-press.md b/.changeset/wild-feet-press.md deleted file mode 100644 index d1c258cdbc..0000000000 --- a/.changeset/wild-feet-press.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@lg-tools/meta': patch ---- - -Fixes recursion in `findPackageJson` diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 87317a2e1d..9acdc0a6f7 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -15,6 +15,7 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/date-utils": "^0.1.0", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/form-field": "^0.2.0", "@leafygreen-ui/hooks": "^8.0.0", @@ -27,7 +28,6 @@ "@leafygreen-ui/tokens": "^2.2.0", "@leafygreen-ui/typography": "^18.0.0", "date-fns": "^2.30.0", - "date-fns-tz": "^2.0.0", "lodash": "^4.17.21", "polished": "^4.2.2", "weekstart": "^2.0.0" diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index b2cba5171a..3cb5098433 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { StoryFn } from '@storybook/react'; import Button from '@leafygreen-ui/button'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import Modal from '@leafygreen-ui/modal'; @@ -12,11 +13,9 @@ import { DatePickerContextProps, DatePickerProvider, } from './shared/components/DatePickerContext'; -import { Month } from './shared/constants'; import { getProviderPropsFromStoryContext } from './shared/testutils/getProviderPropsFromStoryContext'; import { Locales, TimeZones } from './shared/testutils/testValues'; import { AutoComplete } from './shared/types'; -import { newUTC } from './shared/utils'; import { DatePicker } from './DatePicker'; const ProviderWrapper = (Story: StoryFn, ctx: any) => { diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 97784e2e20..06b6de4dde 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -10,18 +10,18 @@ import { import userEvent from '@testing-library/user-event'; import { addDays, subDays } from 'date-fns'; -import { transitionDuration } from '@leafygreen-ui/tokens'; - -import { defaultMax, defaultMin, Month } from '../shared/constants'; -import { eventContainingTargetValue, tabNTimes } from '../shared/testutils'; import { - getFormattedDateString, getISODate, - getValueFormatter, + Month, newUTC, setUTCMonth, setUTCYear, -} from '../shared/utils'; +} from '@leafygreen-ui/date-utils'; +import { transitionDuration } from '@leafygreen-ui/tokens'; + +import { defaultMax, defaultMin } from '../shared/constants'; +import { eventContainingTargetValue, tabNTimes } from '../shared/testutils'; +import { getFormattedDateString, getValueFormatter } from '../shared/utils'; import { expectedTabStopLabels, diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 83fe5e1294..7325c57271 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -9,8 +9,9 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { getISODate } from '@leafygreen-ui/date-utils'; + import { DateSegment } from '../shared/types'; -import { getISODate } from '../shared/utils'; import { DatePickerProps } from './DatePicker.types'; import { DatePicker } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 01eab92c42..c3d0cbf0a8 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import LeafyGreenProvider, { useDarkMode, } from '@leafygreen-ui/leafygreen-provider'; +import { pickAndOmit } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; @@ -14,7 +15,6 @@ import { DatePickerProvider, } from '../shared/components/DatePickerContext'; import { useControlledValue } from '../shared/hooks'; -import { pickAndOmit } from '../shared/utils'; import { DatePickerProps } from './DatePicker.types'; import { DatePickerContent } from './DatePickerContent'; diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index d74f8799ae..dbb839297a 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -1,6 +1,8 @@ import { ChangeEvent } from 'react'; -import { BaseDatePickerProps, DateType } from '../shared/types'; +import { DateType } from '@leafygreen-ui/date-utils'; + +import { BaseDatePickerProps } from '../shared/types'; export interface DatePickerProps extends BaseDatePickerProps { /** diff --git a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx index 0d38a4050d..e0e8521c9c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx @@ -8,6 +8,7 @@ import React, { import { ExitHandler } from 'react-transition-group/Transition'; import isEqual from 'lodash/isEqual'; +import { isSameUTCDay } from '@leafygreen-ui/date-utils'; import { useBackdropClick, useForwardedRef, @@ -16,7 +17,6 @@ import { import { keyMap } from '@leafygreen-ui/lib'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; -import { isSameUTCDay } from '../../shared/utils'; import { DatePickerInput } from '../DatePickerInput'; import { DatePickerMenu } from '../DatePickerMenu'; import { useSingleDateContext } from '../SingleDateContext'; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index 3dd964f9f3..06f054266d 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { newUTC } from '../../shared'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; + import { DatePickerProvider, DatePickerProviderProps, defaultDatePickerContext, } from '../../shared/components/DatePickerContext'; -import { Month } from '../../shared/constants'; import { SingleDateProvider, SingleDateProviderProps, diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 7d6c2f6070..4eb165e297 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -6,6 +6,8 @@ import React, { MouseEventHandler, } from 'react'; +import { isSameUTCDay } from '@leafygreen-ui/date-utils'; +import { isZeroLike } from '@leafygreen-ui/lib'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; @@ -14,8 +16,6 @@ import { useDatePickerContext } from '../../shared/components/DatePickerContext' import { getRelativeSegmentRef, isElementInputSegment, - isSameUTCDay, - isZeroLike, } from '../../shared/utils'; import { useSingleDateContext } from '../SingleDateContext'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index b606a2e00f..f4f881784c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -3,13 +3,18 @@ import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { addDays } from 'date-fns'; +import { + getISODate, + Month, + newUTC, + setUTCDate, +} from '@leafygreen-ui/date-utils'; + import { DatePickerProvider, DatePickerProviderProps, } from '../../shared/components/DatePickerContext'; -import { Month } from '../../shared/constants'; import { mockTimeZone, testTimeZones } from '../../shared/testutils'; -import { getISODate, newUTC, setUTCDate } from '../../shared/utils'; import { SingleDateProvider, SingleDateProviderProps, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 1929a94599..a5875c0171 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -5,6 +5,7 @@ import { userEvent, within } from '@storybook/testing-library'; import { last, omit } from 'lodash'; import MockDate from 'mockdate'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { type StoryMetaType } from '@leafygreen-ui/lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; @@ -15,13 +16,11 @@ import { type DatePickerContextProps, DatePickerProvider, } from '../../shared/components/DatePickerContext'; -import { Month } from '../../shared/constants'; import { getProviderPropsFromStoryContext, Locales, TimeZones, } from '../../shared/testutils'; -import { newUTC } from '../../shared/utils'; import { type SingleDateContextProps, SingleDateProvider, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index c463affb3e..1f9ecc000c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -5,6 +5,16 @@ import React, { useRef, } from 'react'; +import { + addDaysUTC, + getFirstOfMonth, + getFullMonthLabel, + getISODate, + getUTCDateString, + isSameTZDay, + isSameUTCDay, + isSameUTCMonth, +} from '@leafygreen-ui/date-utils'; import { useForwardedRef, usePrevious } from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; import { spacing } from '@leafygreen-ui/tokens'; @@ -16,16 +26,6 @@ import { } from '../../shared/components/Calendar'; import { useDatePickerContext } from '../../shared/components/DatePickerContext'; import { MenuWrapper } from '../../shared/components/MenuWrapper'; -import { - addDaysUTC, - getFirstOfMonth, - getFullMonthLabel, - getISODate, - getUTCDateString, - isSameTZDay, - isSameUTCDay, - isSameUTCMonth, -} from '../../shared/utils'; import { useSingleDateContext } from '../SingleDateContext'; import { getNewHighlight } from './utils/getNewHighlight'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx index 2e4cf0eeee..2da2cd7e29 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx @@ -2,9 +2,9 @@ import React, { PropsWithChildren, useState } from 'react'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { transitionDuration } from '@leafygreen-ui/tokens'; -import { Month, newUTC } from '../../../shared'; import { DatePickerContext, defaultDatePickerContext, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index 12cb796526..350126a56f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -1,6 +1,12 @@ import React, { forwardRef, MouseEventHandler, useCallback } from 'react'; import range from 'lodash/range'; +import { + getLocaleMonths, + isSameUTCMonth, + setUTCMonth, + setUTCYear, +} from '@leafygreen-ui/date-utils'; import { cx } from '@leafygreen-ui/emotion'; import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; @@ -8,12 +14,6 @@ import { Option, Select } from '@leafygreen-ui/select'; import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; import { selectElementProps } from '../../../shared/constants'; -import { - getLocaleMonths, - isSameUTCMonth, - setUTCMonth, - setUTCYear, -} from '../../../shared/utils'; import { useSingleDateContext } from '../../SingleDateContext'; import { menuHeaderSelectContainerStyles, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts index 102ee392d3..78bd1dbfbd 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/index.ts @@ -1,4 +1,4 @@ -import { isSameUTCMonth } from '../../../../../shared/utils'; +import { isSameUTCMonth } from '@leafygreen-ui/date-utils'; /** * Checks if chevron should be disabled diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts index 92173d2feb..a19486324c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldChevronBeDisabled/isChevronDisabled.spec.ts @@ -1,4 +1,4 @@ -import { Month } from '../../../../../shared/constants'; +import { Month } from '@leafygreen-ui/date-utils'; import { shouldChevronBeDisabled } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/index.ts index d9271f28cc..e5002e632a 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/index.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/index.ts @@ -1,7 +1,6 @@ import isNull from 'lodash/isNull'; -import { DateType } from '../../../../../shared/types'; -import { getMonthIndex } from '../../../../../shared/utils'; +import { DateType, getMonthIndex } from '@leafygreen-ui/date-utils'; export const shouldMonthBeEnabled = ( monthName: string, diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/shouldMonthBeEnabled.spec.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/shouldMonthBeEnabled.spec.ts index 26e9f9f17d..6e4cd5ee92 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/shouldMonthBeEnabled.spec.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/utils/shouldMonthBeEnabled/shouldMonthBeEnabled.spec.ts @@ -1,4 +1,4 @@ -import { Month, newUTC } from '../../../../../shared'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { shouldMonthBeEnabled } from '.'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts b/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts index 1717dd6c1d..6ec42ee3aa 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/utils/getNewHighlight/index.ts @@ -4,7 +4,7 @@ import { getFirstOfMonth, getLastOfMonth, isSameUTCMonth, -} from '../../../../shared/utils'; +} from '@leafygreen-ui/date-utils'; export const getNewHighlight = ( currentHighlight: Date | null, diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx index d0fc683bc4..ffb0a0efe4 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx @@ -9,15 +9,17 @@ import React, { useState, } from 'react'; -import { usePrevious } from '@leafygreen-ui/hooks'; - -import { DateType, getFirstOfMonth, useDatePickerContext } from '../../shared'; import { - getFormattedDateString, + DateType, + getFirstOfMonth, getISODate, isOnOrBefore, isSameUTCDay, -} from '../../shared/utils'; +} from '@leafygreen-ui/date-utils'; +import { usePrevious } from '@leafygreen-ui/hooks'; + +import { useDatePickerContext } from '../../shared/components'; +import { getFormattedDateString } from '../../shared/utils'; import { getInitialHighlight } from '../utils/getInitialHighlight'; import { diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts index 70d746dc6c..5c6eb9c62a 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts +++ b/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts @@ -1,8 +1,9 @@ import { SyntheticEvent } from 'react'; +import { DateType } from '@leafygreen-ui/date-utils'; import { DynamicRefGetter } from '@leafygreen-ui/hooks'; -import { DateType, SegmentRefs } from '../../shared'; +import { SegmentRefs } from '../../shared'; import { DatePickerProps } from '../DatePicker.types'; export interface DatePickerComponentRefs { diff --git a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts index 09e9c278b3..b73d127ce3 100644 --- a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts +++ b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/getInitialHighlight.spec.ts @@ -1,4 +1,4 @@ -import { Month, newUTC } from '../../../shared'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { getInitialHighlight } from '.'; diff --git a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts index 56f453f6d0..1f79e7d695 100644 --- a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts @@ -1,4 +1,4 @@ -import { DateType, isValidDate } from '../../../shared'; +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; /** Returns the initial highlight value when the date picker is opened */ export const getInitialHighlight = ( diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index 0c49d5ca26..80225564db 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -2,12 +2,16 @@ import React, { useState } from 'react'; import { StoryFn } from '@storybook/react'; +import { + getISODate, + isTodayTZ, + Month, + newUTC, +} from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; -import { Month } from '../../../constants'; import { Locales, TimeZones } from '../../../testutils'; -import { getISODate, isTodayTZ, newUTC } from '../../../utils'; import { DatePickerContextProps, DatePickerProvider, diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx index 99f4211f47..ab4546a85a 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx @@ -2,11 +2,14 @@ import React, { forwardRef, useMemo } from 'react'; import range from 'lodash/range'; import { getWeekStartByLocale } from 'weekstart'; +import { + daysPerWeek, + getLocaleWeekdays, + getWeeksArray, +} from '@leafygreen-ui/date-utils'; import { cx } from '@leafygreen-ui/emotion'; import { Disclaimer } from '@leafygreen-ui/typography'; -import { daysPerWeek } from '../../../constants'; -import { getLocaleWeekdays, getWeeksArray } from '../../../utils'; import { useDatePickerContext } from '../../DatePickerContext'; import { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index 32a2efcb6b..69baaf110d 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -3,9 +3,9 @@ import { jest } from '@jest/globals'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Month } from '../../../constants'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; + import { segmentRefsMock } from '../../../testutils'; -import { newUTC } from '../../../utils'; import { DatePickerProvider, DatePickerProviderProps, diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index bd06222bd7..19ae1110bd 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -3,12 +3,11 @@ import React, { useEffect, useState } from 'react'; import { StoryFn } from '@storybook/react'; import { isValid } from 'date-fns'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { pickAndOmit, StoryMetaType, StoryType } from '@leafygreen-ui/lib'; -import { Month } from '../../../constants'; import { Locales, segmentRefsMock } from '../../../testutils'; -import { newUTC } from '../../../utils'; import { contextPropNames, DatePickerContextProps, diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts index eeab5f2df7..5c1bcede61 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts @@ -1,7 +1,7 @@ +import { DateType } from '@leafygreen-ui/date-utils'; import { HTMLElementProps } from '@leafygreen-ui/lib'; import { SegmentRefs } from '../../../hooks'; -import { DateType } from '../../../types'; import { DateInputSegmentChangeEventHandler } from '../DateInputSegment/DateInputSegment.types'; export interface DateInputBoxProps diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx index 362ddaf719..af42ab597c 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx @@ -2,10 +2,10 @@ import React, { PropsWithChildren } from 'react'; import { act, waitFor } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; -import { MAX_DATE, MIN_DATE, Month } from '../../constants'; -import { newUTC } from '../../utils'; +import { MAX_DATE, MIN_DATE } from '../../constants'; import { DatePickerContextProps, diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts index 8c1bdb4aa5..cca2045ceb 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts @@ -2,6 +2,7 @@ import { isBefore, isWithinInterval } from 'date-fns'; import defaults from 'lodash/defaults'; import defaultTo from 'lodash/defaultTo'; +import { getISODate, toDate } from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; @@ -11,7 +12,7 @@ import { BaseDatePickerProps, DatePickerState, } from '../../types'; -import { getFormatParts, getISODate, toDate } from '../../utils'; +import { getFormatParts } from '../../utils'; import { DatePickerContextProps, diff --git a/packages/date-picker/src/shared/constants.ts b/packages/date-picker/src/shared/constants.ts index 37303805ec..96f78df967 100644 --- a/packages/date-picker/src/shared/constants.ts +++ b/packages/date-picker/src/shared/constants.ts @@ -1,24 +1,6 @@ +import { Month } from '@leafygreen-ui/date-utils'; import { DropdownWidthBasis } from '@leafygreen-ui/select'; -/** Days in a week */ -export const daysPerWeek = 7 as const; - -/** Enumerates month names to the 0-indexed value */ -export enum Month { - January, - February, - March, - April, - May, - June, - July, - August, - September, - October, - November, - December, -} - /** * The default earliest selectable date * (Unix epoch start: https://en.wikipedia.org/wiki/Unix_time) diff --git a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts index dd3df4c725..e28eecc541 100644 --- a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts +++ b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts @@ -1,14 +1,10 @@ import { useEffect, useReducer } from 'react'; import { isSameDay } from 'date-fns'; +import { DateType } from '@leafygreen-ui/date-utils'; import { usePrevious } from '@leafygreen-ui/hooks'; -import { - DateSegment, - DateSegmentsState, - DateSegmentValue, - DateType, -} from '../../types'; +import { DateSegment, DateSegmentsState, DateSegmentValue } from '../../types'; import { getFormattedSegmentsFromDate } from '../../utils'; import { diff --git a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts index c98ce52b60..52fdcdfefc 100644 --- a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts +++ b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts @@ -1,6 +1,7 @@ import { StoryContext } from '@storybook/react'; import { LeafyGreenProviderProps } from '@leafygreen-ui/leafygreen-provider'; +import { pickAndOmit } from '@leafygreen-ui/lib'; import { ContextPropKeys, @@ -8,7 +9,6 @@ import { DatePickerProviderProps, } from '../../components/DatePickerContext'; import { BaseDatePickerProps } from '../../types'; -import { pickAndOmit } from '../../utils'; export interface ProviderPropsObject { leafyGreenProviderProps: LeafyGreenProviderProps; diff --git a/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts b/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts index bd9a0eac2a..27634baa53 100644 --- a/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts +++ b/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts @@ -1,4 +1,5 @@ -import { Month } from '../../constants'; +import { Month } from '@leafygreen-ui/date-utils'; + import { testTimeZones } from '../testValues'; import { mockTimeZone } from '.'; diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts index e8b6055406..f4e0b00a24 100644 --- a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -1,3 +1,4 @@ +import { LocaleString } from '@leafygreen-ui/date-utils'; import { DarkModeProps } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; @@ -25,7 +26,7 @@ export interface BaseDatePickerProps extends DarkModeProps { * * @default 'iso8601' */ - locale?: 'iso8601' | string; + locale?: LocaleString; /** * A valid IANA timezone string, or UTC offset. diff --git a/packages/date-picker/src/shared/types/index.ts b/packages/date-picker/src/shared/types/index.ts index 74b3318b28..d7e58585dd 100644 --- a/packages/date-picker/src/shared/types/index.ts +++ b/packages/date-picker/src/shared/types/index.ts @@ -5,11 +5,4 @@ export { type DateSegmentValue, isDateSegment, } from './DateSegment.types'; -export { - AutoComplete, - DatePickerState, - type DateRangeType, - type DateType, - type MonthObject, - type WeekdayObject, -} from './types'; +export { AutoComplete, DatePickerState } from './types'; diff --git a/packages/date-picker/src/shared/types/types.ts b/packages/date-picker/src/shared/types/types.ts index 70875da515..bfa625e962 100644 --- a/packages/date-picker/src/shared/types/types.ts +++ b/packages/date-picker/src/shared/types/types.ts @@ -6,29 +6,6 @@ export const DatePickerState = omit(FormFieldState, 'Valid'); export type DatePickerState = (typeof DatePickerState)[keyof typeof DatePickerState]; -export type DateType = Date | null; -export type DateRangeType = [DateType, DateType]; - -export interface MonthObject { - long: string; - short: string; -} - -/** - * Object representing the abbreviations of a given weekday. - * Abbreviation formats defined in Unicode: https://www.unicode.org/reports/tr35/tr35-67/tr35-dates.html#dfst-weekday - */ -export interface WeekdayObject { - /** The long-form weekday name (e.g. Tuesday)*/ - long: string; - /** An abbreviated weekday name (e.g. Tue) */ - abbr: string; - /** A shorter weekday name (e.g. Tu)*/ - short?: string; - /** The shortest weekday name (e.g. T) */ - narrow: string; -} - export const AutoComplete = { Off: 'off', On: 'on', diff --git a/packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts b/packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts index ebad5b2104..a6de65d909 100644 --- a/packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts +++ b/packages/date-picker/src/shared/utils/doesSomeSegmentExist/index.ts @@ -1,5 +1,6 @@ +import { isNotZeroLike } from '@leafygreen-ui/lib'; + import { DateSegmentsState } from '../../types'; -import { isNotZeroLike } from '../isZeroLike'; /** * Returns whether at least one segment has a value diff --git a/packages/date-picker/src/shared/utils/getFormatParts/index.ts b/packages/date-picker/src/shared/utils/getFormatParts/index.ts index 0cc969df4c..bbd77226bc 100644 --- a/packages/date-picker/src/shared/utils/getFormatParts/index.ts +++ b/packages/date-picker/src/shared/utils/getFormatParts/index.ts @@ -1,4 +1,4 @@ -import { isValidLocale } from '../../utils'; +import { isValidLocale } from '@leafygreen-ui/date-utils'; const now = new Date(); const ISO = 'iso8601'; diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts index e8ef14cd6e..c1d43ccce3 100644 --- a/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.spec.ts @@ -1,5 +1,4 @@ -import { Month } from '../../constants'; -import { newUTC } from '../newUTC'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { getFormattedDateString } from '.'; diff --git a/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts b/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts index f1ae0ecd36..8fe7cbbf80 100644 --- a/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts +++ b/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts @@ -1,5 +1,4 @@ -import { Month } from '../../constants'; -import { newUTC } from '../newUTC'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { getMaxSegmentValue } from '.'; diff --git a/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts b/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts index a317719ed9..96882e0db0 100644 --- a/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts +++ b/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts @@ -1,5 +1,7 @@ +import { DateType } from '@leafygreen-ui/date-utils'; + import { defaultMax } from '../../constants'; -import { DateSegment, DateType } from '../../types'; +import { DateSegment } from '../../types'; import { getSegmentsFromDate } from '../getSegmentsFromDate'; /** Returns the maximum value for a segment, given a context */ diff --git a/packages/date-picker/src/shared/utils/getMinSegmentValue/getMinSegmentValue.spec.ts b/packages/date-picker/src/shared/utils/getMinSegmentValue/getMinSegmentValue.spec.ts index 49d22b890c..dd86f762d4 100644 --- a/packages/date-picker/src/shared/utils/getMinSegmentValue/getMinSegmentValue.spec.ts +++ b/packages/date-picker/src/shared/utils/getMinSegmentValue/getMinSegmentValue.spec.ts @@ -1,5 +1,4 @@ -import { Month } from '../../constants'; -import { newUTC } from '../newUTC'; +import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { getMinSegmentValue } from '.'; diff --git a/packages/date-picker/src/shared/utils/getMinSegmentValue/index.ts b/packages/date-picker/src/shared/utils/getMinSegmentValue/index.ts index 8f76846bd5..5d4daa71e4 100644 --- a/packages/date-picker/src/shared/utils/getMinSegmentValue/index.ts +++ b/packages/date-picker/src/shared/utils/getMinSegmentValue/index.ts @@ -1,5 +1,7 @@ +import { DateType } from '@leafygreen-ui/date-utils'; + import { defaultMin } from '../../constants'; -import { DateSegment, DateType } from '../../types'; +import { DateSegment } from '../../types'; import { getSegmentsFromDate } from '../getSegmentsFromDate'; /** Returns the minimum value for a segment, given a context */ diff --git a/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts b/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts index a55f0d63b4..b52d90eb64 100644 --- a/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts @@ -1,9 +1,9 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { DatePickerContextProps } from '../../../shared/components/DatePickerContext'; -import { SegmentRefs } from '../../../shared/hooks'; -import { DateSegment } from '../../../shared/types'; +import { DatePickerContextProps } from '../../components/DatePickerContext'; +import { SegmentRefs } from '../../hooks'; +import { DateSegment } from '../../types'; type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; interface GetRelativeSegmentContext { diff --git a/packages/date-picker/src/shared/utils/getRemainingParts/index.ts b/packages/date-picker/src/shared/utils/getRemainingParts/index.ts index f1abc48b2c..a643ff2905 100644 --- a/packages/date-picker/src/shared/utils/getRemainingParts/index.ts +++ b/packages/date-picker/src/shared/utils/getRemainingParts/index.ts @@ -1,6 +1,6 @@ import isUndefined from 'lodash/isUndefined'; -import { cloneReverse } from '../cloneReverse'; +import { cloneReverse } from '@leafygreen-ui/lib'; /** Returns the ordered parts to the left or right of the provided index * e.g: diff --git a/packages/date-picker/src/shared/utils/getValueFormatter/index.ts b/packages/date-picker/src/shared/utils/getValueFormatter/index.ts index 6539832d1a..bf759d62bc 100644 --- a/packages/date-picker/src/shared/utils/getValueFormatter/index.ts +++ b/packages/date-picker/src/shared/utils/getValueFormatter/index.ts @@ -1,8 +1,9 @@ import padStart from 'lodash/padStart'; +import { isZeroLike } from '@leafygreen-ui/lib'; + import { charsPerSegment } from '../../constants'; import { DateSegment } from '../../types'; -import { isZeroLike } from '../isZeroLike'; /** * @returns a value formatter function for the provided date segment diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index a393a53b74..924d0cea24 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -1,22 +1,10 @@ -export { addDaysUTC } from './addDaysUTC'; -export { addMonthsUTC } from './addMonthsUTC'; -export { cloneReverse } from './cloneReverse'; export { doesSomeSegmentExist } from './doesSomeSegmentExist'; export { getAutoComplete } from './getAutoComplete'; -export { getDaysInUTCMonth } from './getDaysInUTCMonth'; export { getFirstEmptySegment } from './getFirstEmptySegment'; -export { getFirstOfMonth } from './getFirstOfMonth'; export { getFormatParts, getFormatter } from './getFormatParts'; export { getFormattedDateString } from './getFormattedDateString'; -export { getFullMonthLabel } from './getFullMonthLabel'; -export { getISODate } from './getISODate'; -export { getLastOfMonth } from './getLastOfMonth'; -export { getLocaleMonths } from './getLocaleMonths'; -export { getLocaleWeekdays } from './getLocaleWeekdays'; export { getMaxSegmentValue } from './getMaxSegmentValue'; export { getMinSegmentValue } from './getMinSegmentValue'; -export { getMonthIndex } from './getMonthIndex'; -export { getMonthName } from './getMonthName'; export { getRelativeSegment, getRelativeSegmentRef, @@ -27,33 +15,9 @@ export { getFormattedSegmentsFromDate, getSegmentsFromDate, } from './getSegmentsFromDate'; -export { getUTCDateString } from './getUTCDateString'; export { getValueFormatter } from './getValueFormatter'; -export { getWeekdayName } from './getWeekdayName'; -export { getWeeksArray } from './getWeeksArray'; -export { isCurrentUTCDay } from './isCurrentUTCDay'; -export { isDefined } from './isDefined'; export { isElementInputSegment } from './isElementInputSegment'; export { isExplicitSegmentValue } from './isExplicitSegmentValue'; -export { isOnOrAfter } from './isOnOrAfter'; -export { isOnOrBefore } from './isOnOrBefore'; -export { isSameTZDay } from './isSameTZDay'; -export { isSameUTCDay } from './isSameUTCDay'; -export { isSameUTCMonth } from './isSameUTCMonth'; -export { isSameUTCRange } from './isSameUTCRange'; -export { isTodayTZ } from './isTodayTZ'; -export { isValidDate } from './isValidDate'; -export { isValidLocale } from './isValidLocale'; export { isValidSegmentName, isValidSegmentValue } from './isValidSegment'; export { isValidValueForSegment } from './isValidValueForSegment'; -export { isNotZeroLike, isZeroLike } from './isZeroLike'; -export { maxDate } from './maxDate'; -export { minDate } from './minDate'; export { newDateFromSegments } from './newDateFromSegments'; -export { newUTC } from './newUTC'; -export { pickAndOmit } from './pickAndOmit'; -export { setToUTCMidnight } from './setToUTCMidnight'; -export { setUTCDate } from './setUTCDate'; -export { setUTCMonth } from './setUTCMonth'; -export { setUTCYear } from './setUTCYear'; -export { toDate } from './toDate'; diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts index 6a244f9abe..cc70fdcf9d 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts @@ -1,7 +1,8 @@ +import { newUTC } from '@leafygreen-ui/date-utils'; + import { DateSegmentsState } from '../../types'; import { isValidSegmentName } from '../isValidSegment'; import { isValidValueForSegment } from '../isValidValueForSegment'; -import { newUTC } from '../newUTC'; /** Constructs a date object in UTC from day, month, year segments */ export const newDateFromSegments = ( diff --git a/packages/date-picker/src/shared/utils/pickAndOmit/index.ts b/packages/date-picker/src/shared/utils/pickAndOmit/index.ts deleted file mode 100644 index 62f9f35dfc..0000000000 --- a/packages/date-picker/src/shared/utils/pickAndOmit/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import omit from 'lodash/omit'; -import pick from 'lodash/pick'; - -/** - * Returns an array of 2 objects, - * first, the result of calling `_.pick`, - * second, the result of calling `_.omit` - * - * e.g. - * ```js - * const obj = { a: 'A', b: 'B', c: 'C', d: 'D' } - * pickAndOmit(obj, ['a', 'b']) // [{a: "A", b: "B"}, {c: "C", d: "D"}] - * ``` - */ -export const pickAndOmit = ( - object: T, - keys: Array, -): [Pick, Omit] => { - const picked = pick(object, keys); - const omitted = omit(object, keys); - - return [picked, omitted]; -}; diff --git a/packages/date-picker/tsconfig.json b/packages/date-picker/tsconfig.json index 6d3c68274c..8423fe8c40 100644 --- a/packages/date-picker/tsconfig.json +++ b/packages/date-picker/tsconfig.json @@ -25,6 +25,9 @@ { "path": "../button" }, + { + "path": "../date-utils" + }, { "path": "../emotion" }, diff --git a/packages/date-utils/README.md b/packages/date-utils/README.md new file mode 100644 index 0000000000..2aaa004a24 --- /dev/null +++ b/packages/date-utils/README.md @@ -0,0 +1,21 @@ +# Date Utils + +![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/date-utils.svg) + +LeafyGreen DateUtils provides date manipulation & comparison utilities not included in the [`date-fns`](https://date-fns.org/) library. + +Note, this package only contains generic Date utilities. For implementation-specific utilities, see `date-picker/src/shared/utils` + +## Installation + +### Yarn + +```shell +yarn add @leafygreen-ui/date-utils +``` + +### NPM + +```shell +npm install @leafygreen-ui/date-utils +``` diff --git a/packages/date-utils/package.json b/packages/date-utils/package.json new file mode 100644 index 0000000000..5bc1e19f71 --- /dev/null +++ b/packages/date-utils/package.json @@ -0,0 +1,41 @@ + +{ + "name": "@leafygreen-ui/date-utils", + "version": "0.1.0", + "description": "LeafyGreen UI Kit Date Utils", + "main": "./dist/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "license": "Apache-2.0", + "scripts": { + "build": "lg build-package", + "tsc": "lg build-ts", + "docs": "lg build-tsdoc" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@leafygreen-ui/lib": "^13.1.0", + "date-fns": "^2.30.0", + "date-fns-tz": "^2.0.0", + "lodash": "^4.17.21", + "weekstart": "^2.0.0" + }, + "devDependencies": { + "timezone-mock": "^1.3.6" + }, + "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/date-utils", + "repository": { + "type": "git", + "url": "https://github.com/mongodb/leafygreen-ui" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/PD/summary" + }, + "keywords": [ + "leafygreen", + "date", + "date-fns" + ] +} diff --git a/packages/date-picker/src/shared/utils/addDaysUTC/addDaysUTC.spec.ts b/packages/date-utils/src/addDaysUTC/addDaysUTC.spec.ts similarity index 95% rename from packages/date-picker/src/shared/utils/addDaysUTC/addDaysUTC.spec.ts rename to packages/date-utils/src/addDaysUTC/addDaysUTC.spec.ts index 9a7eb47fe3..1aa17335f3 100644 --- a/packages/date-picker/src/shared/utils/addDaysUTC/addDaysUTC.spec.ts +++ b/packages/date-utils/src/addDaysUTC/addDaysUTC.spec.ts @@ -1,9 +1,9 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { newUTC } from '../newUTC'; import { addDaysUTC } from '.'; -describe('packages/date-picker/utils/addDaysUTC', () => { +describe('packages/date-utils/addDaysUTC', () => { const june30 = newUTC(2023, Month.June, 30); const july1 = newUTC(2023, Month.July, 1); const july2 = newUTC(2023, Month.July, 2); diff --git a/packages/date-picker/src/shared/utils/addDaysUTC/index.ts b/packages/date-utils/src/addDaysUTC/addDaysUTC.ts similarity index 100% rename from packages/date-picker/src/shared/utils/addDaysUTC/index.ts rename to packages/date-utils/src/addDaysUTC/addDaysUTC.ts diff --git a/packages/date-utils/src/addDaysUTC/index.ts b/packages/date-utils/src/addDaysUTC/index.ts new file mode 100644 index 0000000000..315f6b5fd8 --- /dev/null +++ b/packages/date-utils/src/addDaysUTC/index.ts @@ -0,0 +1 @@ +export { addDaysUTC } from './addDaysUTC'; diff --git a/packages/date-utils/src/addMonthsUTC/addMonthsUTC.spec.ts b/packages/date-utils/src/addMonthsUTC/addMonthsUTC.spec.ts new file mode 100644 index 0000000000..59f729a6e8 --- /dev/null +++ b/packages/date-utils/src/addMonthsUTC/addMonthsUTC.spec.ts @@ -0,0 +1,54 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { addMonthsUTC } from '.'; + +describe('packages/date-utils/addMonthsUTC', () => { + test('returns the same date for 0', () => { + const july4 = newUTC(2020, Month.July, 4); + expect(addMonthsUTC(july4, 0)).toEqual(july4); + }); + + test('adds a single month', () => { + const july4 = newUTC(2020, Month.July, 4); + const aug4 = newUTC(2020, Month.August, 4); + expect(addMonthsUTC(july4, 1)).toEqual(aug4); + }); + test('adds multiple months', () => { + const july4 = newUTC(2020, Month.July, 4); + const sept4 = newUTC(2020, Month.September, 4); + expect(addMonthsUTC(july4, 2)).toEqual(sept4); + }); + test('adds months across years', () => { + const dec5 = newUTC(2020, Month.December, 5); + const jan5 = newUTC(2021, Month.January, 5); + expect(addMonthsUTC(dec5, 1)).toEqual(jan5); + }); + test('adds months during leap year', () => { + const jan29 = newUTC(2020, Month.January, 29); + const feb29 = newUTC(2020, Month.February, 29); + expect(addMonthsUTC(jan29, 1)).toEqual(feb29); + }); + + /** Subtracts */ + test('subtracts a single month', () => { + const july4 = newUTC(2020, Month.July, 4); + const aug4 = newUTC(2020, Month.August, 4); + expect(addMonthsUTC(aug4, -1)).toEqual(july4); + }); + test('subtracts multiple months', () => { + const july4 = newUTC(2020, Month.July, 4); + const sept4 = newUTC(2020, Month.September, 4); + expect(addMonthsUTC(sept4, -2)).toEqual(july4); + }); + test('subtracts months across years', () => { + const dec5 = newUTC(2020, Month.December, 5); + const jan5 = newUTC(2021, Month.January, 5); + expect(addMonthsUTC(jan5, -1)).toEqual(dec5); + }); + test('subtracts months during leap year', () => { + const feb29 = newUTC(2020, Month.February, 29); + const mar29 = newUTC(2020, Month.March, 29); + expect(addMonthsUTC(mar29, -1)).toEqual(feb29); + }); +}); diff --git a/packages/date-picker/src/shared/utils/addMonthsUTC/index.ts b/packages/date-utils/src/addMonthsUTC/addMonthsUTC.ts similarity index 100% rename from packages/date-picker/src/shared/utils/addMonthsUTC/index.ts rename to packages/date-utils/src/addMonthsUTC/addMonthsUTC.ts diff --git a/packages/date-utils/src/addMonthsUTC/index.ts b/packages/date-utils/src/addMonthsUTC/index.ts new file mode 100644 index 0000000000..82d1467f37 --- /dev/null +++ b/packages/date-utils/src/addMonthsUTC/index.ts @@ -0,0 +1 @@ +export { addMonthsUTC } from './addMonthsUTC'; diff --git a/packages/date-utils/src/constants.ts b/packages/date-utils/src/constants.ts new file mode 100644 index 0000000000..507f192021 --- /dev/null +++ b/packages/date-utils/src/constants.ts @@ -0,0 +1,18 @@ +/** Days in a week */ +export const daysPerWeek = 7 as const; + +/** Enumerates month names to the 0-indexed value */ +export enum Month { + January, + February, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, +} diff --git a/packages/date-picker/src/shared/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts b/packages/date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts similarity index 94% rename from packages/date-picker/src/shared/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts rename to packages/date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts index 7b66d21992..bbf90b770a 100644 --- a/packages/date-picker/src/shared/utils/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts +++ b/packages/date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.spec.ts @@ -1,6 +1,6 @@ import { getDaysInUTCMonth } from '.'; -describe('getDaysInUTCMonth', () => { +describe('packages/date-utils/getDaysInUTCMonth', () => { test('returns the number of days in the month', () => { const result = getDaysInUTCMonth(new Date(2100, 1 /* Feb */, 11)); expect(result).toEqual(28); diff --git a/packages/date-picker/src/shared/utils/getDaysInUTCMonth/index.ts b/packages/date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getDaysInUTCMonth/index.ts rename to packages/date-utils/src/getDaysInUTCMonth/getDaysInUTCMonth.ts diff --git a/packages/date-utils/src/getDaysInUTCMonth/index.ts b/packages/date-utils/src/getDaysInUTCMonth/index.ts new file mode 100644 index 0000000000..7dc2ab8753 --- /dev/null +++ b/packages/date-utils/src/getDaysInUTCMonth/index.ts @@ -0,0 +1 @@ +export { getDaysInUTCMonth } from './getDaysInUTCMonth'; diff --git a/packages/date-picker/src/shared/utils/getFirstOfMonth/getFirstOfMonth.spec.ts b/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts similarity index 91% rename from packages/date-picker/src/shared/utils/getFirstOfMonth/getFirstOfMonth.spec.ts rename to packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts index ae7c819d89..d030d61bc4 100644 --- a/packages/date-picker/src/shared/utils/getFirstOfMonth/getFirstOfMonth.spec.ts +++ b/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts @@ -1,6 +1,6 @@ import { getFirstOfMonth } from '.'; -describe('packages/date-picker/utils/getFirstOfMonth', () => { +describe('packages/date-utils/getFirstOfMonth', () => { test('returns the first day of the provided month', () => { expect(getFirstOfMonth(new Date(Date.UTC(2023, 0, 31)))).toEqual( new Date(Date.UTC(2023, 0, 1)), diff --git a/packages/date-picker/src/shared/utils/getFirstOfMonth/index.ts b/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getFirstOfMonth/index.ts rename to packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.ts diff --git a/packages/date-utils/src/getFirstOfMonth/index.ts b/packages/date-utils/src/getFirstOfMonth/index.ts new file mode 100644 index 0000000000..9e8562233d --- /dev/null +++ b/packages/date-utils/src/getFirstOfMonth/index.ts @@ -0,0 +1 @@ +export { getFirstOfMonth } from './getFirstOfMonth'; diff --git a/packages/date-picker/src/shared/utils/getFullMonthLabel/getFullMonthLabel.spec.ts b/packages/date-utils/src/getFullMonthLabel/getFullMonthLabel.spec.ts similarity index 67% rename from packages/date-picker/src/shared/utils/getFullMonthLabel/getFullMonthLabel.spec.ts rename to packages/date-utils/src/getFullMonthLabel/getFullMonthLabel.spec.ts index e525fcf604..b1817a99de 100644 --- a/packages/date-picker/src/shared/utils/getFullMonthLabel/getFullMonthLabel.spec.ts +++ b/packages/date-utils/src/getFullMonthLabel/getFullMonthLabel.spec.ts @@ -1,9 +1,9 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { newUTC } from '../newUTC'; import { getFullMonthLabel } from '.'; -describe('packages/date-picker/utils/getMonthName', () => { +describe('packages/date-utils/getMonthName', () => { test('Jan', () => { expect(getFullMonthLabel(newUTC(2023, Month.January, 5))).toEqual( 'January 2023', diff --git a/packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts b/packages/date-utils/src/getFullMonthLabel/getFullMonthLabel.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getFullMonthLabel/index.ts rename to packages/date-utils/src/getFullMonthLabel/getFullMonthLabel.ts diff --git a/packages/date-utils/src/getFullMonthLabel/index.ts b/packages/date-utils/src/getFullMonthLabel/index.ts new file mode 100644 index 0000000000..d2b221167f --- /dev/null +++ b/packages/date-utils/src/getFullMonthLabel/index.ts @@ -0,0 +1 @@ +export { getFullMonthLabel } from './getFullMonthLabel'; diff --git a/packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts b/packages/date-utils/src/getISODate/getISODate.spec.ts similarity index 76% rename from packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts rename to packages/date-utils/src/getISODate/getISODate.spec.ts index deb7189971..5168471b7f 100644 --- a/packages/date-picker/src/shared/utils/getISODate/getISODate.spec.ts +++ b/packages/date-utils/src/getISODate/getISODate.spec.ts @@ -1,9 +1,9 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { newUTC } from '../newUTC'; import { getISODate } from '.'; -describe('packages/date-picker/utils/getISODate', () => { +describe('packages/date-utils/getISODate', () => { test('returns an ISO Date string', () => { const d1 = newUTC(2023, Month.July, 10); expect(getISODate(d1)).toEqual('2023-07-10'); diff --git a/packages/date-picker/src/shared/utils/getISODate/index.ts b/packages/date-utils/src/getISODate/getISODate.ts similarity index 90% rename from packages/date-picker/src/shared/utils/getISODate/index.ts rename to packages/date-utils/src/getISODate/getISODate.ts index 6872036238..7d40c7a8b2 100644 --- a/packages/date-picker/src/shared/utils/getISODate/index.ts +++ b/packages/date-utils/src/getISODate/getISODate.ts @@ -1,5 +1,5 @@ -import { DateType } from '../../types'; import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; /** * Returns only the Date portion of the ISOString for a given date diff --git a/packages/date-utils/src/getISODate/index.ts b/packages/date-utils/src/getISODate/index.ts new file mode 100644 index 0000000000..369faacad7 --- /dev/null +++ b/packages/date-utils/src/getISODate/index.ts @@ -0,0 +1 @@ +export { getISODate } from './getISODate'; diff --git a/packages/date-picker/src/shared/utils/getLastOfMonth/getLastOfMonth.spec.ts b/packages/date-utils/src/getLastOfMonth/getLastOfMonth.spec.ts similarity index 91% rename from packages/date-picker/src/shared/utils/getLastOfMonth/getLastOfMonth.spec.ts rename to packages/date-utils/src/getLastOfMonth/getLastOfMonth.spec.ts index 603186c927..02898a04a5 100644 --- a/packages/date-picker/src/shared/utils/getLastOfMonth/getLastOfMonth.spec.ts +++ b/packages/date-utils/src/getLastOfMonth/getLastOfMonth.spec.ts @@ -1,8 +1,8 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { getLastOfMonth } from '.'; -describe('packages/date-picker/utils/getLastOfMonth', () => { +describe('packages/date-utils/getLastOfMonth', () => { test('sets to last of month', () => { expect(getLastOfMonth(new Date(Date.UTC(2023, Month.January, 5)))).toEqual( new Date(Date.UTC(2023, Month.January, 31)), diff --git a/packages/date-picker/src/shared/utils/getLastOfMonth/index.ts b/packages/date-utils/src/getLastOfMonth/getLastOfMonth.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getLastOfMonth/index.ts rename to packages/date-utils/src/getLastOfMonth/getLastOfMonth.ts diff --git a/packages/date-utils/src/getLastOfMonth/index.ts b/packages/date-utils/src/getLastOfMonth/index.ts new file mode 100644 index 0000000000..efd4446dac --- /dev/null +++ b/packages/date-utils/src/getLastOfMonth/index.ts @@ -0,0 +1 @@ +export { getLastOfMonth } from './getLastOfMonth'; diff --git a/packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts b/packages/date-utils/src/getLocaleMonths/getLocaleMonths.spec.ts similarity index 91% rename from packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts rename to packages/date-utils/src/getLocaleMonths/getLocaleMonths.spec.ts index a78e119722..bf3e1af3f1 100644 --- a/packages/date-picker/src/shared/utils/getLocaleMonths/getLocaleMonths.spec.ts +++ b/packages/date-utils/src/getLocaleMonths/getLocaleMonths.spec.ts @@ -1,6 +1,6 @@ import { getLocaleMonths } from '.'; -describe('packages/date-picker/utils/getLocaleMonths', () => { +describe('packages/date-utils/getLocaleMonths', () => { test('default (English)', () => { expect(getLocaleMonths()).toHaveLength(12); getLocaleMonths().forEach(mo => { diff --git a/packages/date-picker/src/shared/utils/getLocaleMonths/index.ts b/packages/date-utils/src/getLocaleMonths/getLocaleMonths.ts similarity index 85% rename from packages/date-picker/src/shared/utils/getLocaleMonths/index.ts rename to packages/date-utils/src/getLocaleMonths/getLocaleMonths.ts index 850485d16d..2c79729053 100644 --- a/packages/date-picker/src/shared/utils/getLocaleMonths/index.ts +++ b/packages/date-utils/src/getLocaleMonths/getLocaleMonths.ts @@ -1,7 +1,7 @@ import range from 'lodash/range'; -import { MonthObject } from '../../types'; import { getMonthName } from '../getMonthName'; +import { MonthObject } from '../types'; export const getLocaleMonths = (locale?: string): Array => { return range(12).map(monthIndex => { diff --git a/packages/date-utils/src/getLocaleMonths/index.ts b/packages/date-utils/src/getLocaleMonths/index.ts new file mode 100644 index 0000000000..70b75d0daa --- /dev/null +++ b/packages/date-utils/src/getLocaleMonths/index.ts @@ -0,0 +1 @@ +export { getLocaleMonths } from './getLocaleMonths'; diff --git a/packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts b/packages/date-utils/src/getLocaleWeekdays/getLocaleWeekdays.spec.ts similarity index 93% rename from packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts rename to packages/date-utils/src/getLocaleWeekdays/getLocaleWeekdays.spec.ts index 6ad1124c70..3ee2a527d3 100644 --- a/packages/date-picker/src/shared/utils/getLocaleWeekdays/getLocaleWeekdays.spec.ts +++ b/packages/date-utils/src/getLocaleWeekdays/getLocaleWeekdays.spec.ts @@ -1,6 +1,6 @@ import { getLocaleWeekdays } from '.'; -describe('packages/date-picker/utils/getLocaleWeekdays', () => { +describe('packages/date-utils/getLocaleWeekdays', () => { test('English (default)', () => { expect(getLocaleWeekdays()).toHaveLength(7); getLocaleWeekdays().forEach(w => { diff --git a/packages/date-picker/src/shared/utils/getLocaleWeekdays/index.ts b/packages/date-utils/src/getLocaleWeekdays/getLocaleWeekdays.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getLocaleWeekdays/index.ts rename to packages/date-utils/src/getLocaleWeekdays/getLocaleWeekdays.ts diff --git a/packages/date-utils/src/getLocaleWeekdays/index.ts b/packages/date-utils/src/getLocaleWeekdays/index.ts new file mode 100644 index 0000000000..12ff84fca7 --- /dev/null +++ b/packages/date-utils/src/getLocaleWeekdays/index.ts @@ -0,0 +1 @@ +export { getLocaleWeekdays } from './getLocaleWeekdays'; diff --git a/packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts b/packages/date-utils/src/getMonthIndex/getMonthIndex.spec.ts similarity index 93% rename from packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts rename to packages/date-utils/src/getMonthIndex/getMonthIndex.spec.ts index cb6d7968b2..a23d62883e 100644 --- a/packages/date-picker/src/shared/utils/getMonthIndex/getMonthIndex.spec.ts +++ b/packages/date-utils/src/getMonthIndex/getMonthIndex.spec.ts @@ -1,6 +1,6 @@ import { getMonthIndex } from '.'; -describe('packages/date-picker/utils/getMonthIndex', () => { +describe('packages/date-utils/getMonthIndex', () => { test('Default (English) long', () => { expect(getMonthIndex('January')).toBe(0); expect(getMonthIndex('December')).toBe(11); diff --git a/packages/date-picker/src/shared/utils/getMonthIndex/index.ts b/packages/date-utils/src/getMonthIndex/getMonthIndex.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getMonthIndex/index.ts rename to packages/date-utils/src/getMonthIndex/getMonthIndex.ts diff --git a/packages/date-utils/src/getMonthIndex/index.ts b/packages/date-utils/src/getMonthIndex/index.ts new file mode 100644 index 0000000000..0e076cce67 --- /dev/null +++ b/packages/date-utils/src/getMonthIndex/index.ts @@ -0,0 +1 @@ +export { getMonthIndex } from './getMonthIndex'; diff --git a/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts b/packages/date-utils/src/getMonthName/getMonthName.spec.ts similarity index 97% rename from packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts rename to packages/date-utils/src/getMonthName/getMonthName.spec.ts index d14c04afb5..b157287dc9 100644 --- a/packages/date-picker/src/shared/utils/getMonthName/getMonthName.spec.ts +++ b/packages/date-utils/src/getMonthName/getMonthName.spec.ts @@ -1,6 +1,6 @@ import { getMonthName } from '.'; -describe('packages/date-picker/utils/getMonthName', () => { +describe('packages/date-utils/getMonthName', () => { test('Default (English)', () => { expect(getMonthName(0)).toEqual( expect.objectContaining({ long: 'January', short: 'Jan' }), diff --git a/packages/date-picker/src/shared/utils/getMonthName/index.ts b/packages/date-utils/src/getMonthName/getMonthName.ts similarity index 92% rename from packages/date-picker/src/shared/utils/getMonthName/index.ts rename to packages/date-utils/src/getMonthName/getMonthName.ts index b9f68045e8..ba7d669339 100644 --- a/packages/date-picker/src/shared/utils/getMonthName/index.ts +++ b/packages/date-utils/src/getMonthName/getMonthName.ts @@ -1,5 +1,5 @@ -import { MonthObject } from '../../types'; import { normalizeLocale } from '../normalizeLocale'; +import { MonthObject } from '../types'; /** * Returns the month name from a given index and optional locale diff --git a/packages/date-utils/src/getMonthName/index.ts b/packages/date-utils/src/getMonthName/index.ts new file mode 100644 index 0000000000..33e4ee828f --- /dev/null +++ b/packages/date-utils/src/getMonthName/index.ts @@ -0,0 +1 @@ +export { getMonthName } from './getMonthName'; diff --git a/packages/date-picker/src/shared/utils/getUTCDateString/getUTCDateString.spec.ts b/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts similarity index 83% rename from packages/date-picker/src/shared/utils/getUTCDateString/getUTCDateString.spec.ts rename to packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts index 9fa68e52af..4839f22045 100644 --- a/packages/date-picker/src/shared/utils/getUTCDateString/getUTCDateString.spec.ts +++ b/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts @@ -1,8 +1,8 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { getUTCDateString } from '.'; -describe('date-picker/utils/getUTCDateString', () => { +describe('packages/date-utils/getUTCDateString', () => { test('returns date string relative to UTC', () => { const date = new Date(Date.UTC(2023, Month.September, 10)); const str = getUTCDateString(date); diff --git a/packages/date-picker/src/shared/utils/getUTCDateString/index.ts b/packages/date-utils/src/getUTCDateString/getUTCDateString.ts similarity index 100% rename from packages/date-picker/src/shared/utils/getUTCDateString/index.ts rename to packages/date-utils/src/getUTCDateString/getUTCDateString.ts diff --git a/packages/date-utils/src/getUTCDateString/index.ts b/packages/date-utils/src/getUTCDateString/index.ts new file mode 100644 index 0000000000..d9dc1b871e --- /dev/null +++ b/packages/date-utils/src/getUTCDateString/index.ts @@ -0,0 +1 @@ +export { getUTCDateString } from './getUTCDateString'; diff --git a/packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts b/packages/date-utils/src/getWeekdayName/getWeekdayName.spec.ts similarity index 94% rename from packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts rename to packages/date-utils/src/getWeekdayName/getWeekdayName.spec.ts index 172db0dcef..f20940822f 100644 --- a/packages/date-picker/src/shared/utils/getWeekdayName/getWeekdayName.spec.ts +++ b/packages/date-utils/src/getWeekdayName/getWeekdayName.spec.ts @@ -1,5 +1,5 @@ import { getWeekdayName } from '.'; -describe('packages/date-picker/utils/getWeekdayName', () => { +describe('packages/date-utils/getWeekdayName', () => { test('default (English)', () => { expect(getWeekdayName(0)).toEqual( expect.objectContaining({ diff --git a/packages/date-picker/src/shared/utils/getWeekdayName/index.ts b/packages/date-utils/src/getWeekdayName/getWeekdayName.ts similarity index 92% rename from packages/date-picker/src/shared/utils/getWeekdayName/index.ts rename to packages/date-utils/src/getWeekdayName/getWeekdayName.ts index ab7b62bea4..08b8dfddc7 100644 --- a/packages/date-picker/src/shared/utils/getWeekdayName/index.ts +++ b/packages/date-utils/src/getWeekdayName/getWeekdayName.ts @@ -1,8 +1,8 @@ import { truncate } from 'lodash'; -import { Month } from '../../constants'; -import { WeekdayObject } from '../../types'; +import { Month } from '../constants'; import { normalizeLocale } from '../normalizeLocale'; +import { WeekdayObject } from '../types'; export const getWeekdayName = ( day: number, diff --git a/packages/date-utils/src/getWeekdayName/index.ts b/packages/date-utils/src/getWeekdayName/index.ts new file mode 100644 index 0000000000..4613d86ab2 --- /dev/null +++ b/packages/date-utils/src/getWeekdayName/index.ts @@ -0,0 +1 @@ +export { getWeekdayName } from './getWeekdayName'; diff --git a/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts b/packages/date-utils/src/getWeeksArray/getWeeksArray.spec.ts similarity index 99% rename from packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts rename to packages/date-utils/src/getWeeksArray/getWeeksArray.spec.ts index e733758726..3db53666c8 100644 --- a/packages/date-picker/src/shared/utils/getWeeksArray/getWeeksArray.spec.ts +++ b/packages/date-utils/src/getWeeksArray/getWeeksArray.spec.ts @@ -1,10 +1,10 @@ import { isNull } from 'lodash'; -import { Month } from '../../constants'; +import { Month } from '../constants'; import { getWeeksArray } from '.'; -describe('packages/date-picker/utils/getWeeksArray', () => { +describe('packages/date-utils/getWeeksArray', () => { test('starts the week on the correct day for the locale', () => { const month = new Date(Date.UTC(2023, Month.August, 1)); const arr = getWeeksArray(month, { locale: 'en-US' }); diff --git a/packages/date-picker/src/shared/utils/getWeeksArray/index.ts b/packages/date-utils/src/getWeeksArray/getWeeksArray.ts similarity index 89% rename from packages/date-picker/src/shared/utils/getWeeksArray/index.ts rename to packages/date-utils/src/getWeeksArray/getWeeksArray.ts index 98670fd0d9..c62018256a 100644 --- a/packages/date-picker/src/shared/utils/getWeeksArray/index.ts +++ b/packages/date-utils/src/getWeeksArray/getWeeksArray.ts @@ -3,13 +3,14 @@ import fill from 'lodash/fill'; import range from 'lodash/range'; import { getWeekStartByLocale } from 'weekstart'; -import { daysPerWeek } from '../../constants'; -import { BaseDatePickerProps } from '../../types'; +import { daysPerWeek } from '../constants'; import { getDaysInUTCMonth } from '../getDaysInUTCMonth'; import { setToUTCMidnight } from '../setToUTCMidnight'; +import { LocaleString } from '../types'; -interface GetWeeksArrayOptions - extends Required> {} +interface GetWeeksArrayOptions { + locale: LocaleString; +} /** * Returns a 7x5 (or 7x6) 2D array of Dates for the given month diff --git a/packages/date-utils/src/getWeeksArray/index.ts b/packages/date-utils/src/getWeeksArray/index.ts new file mode 100644 index 0000000000..c01cda8ebc --- /dev/null +++ b/packages/date-utils/src/getWeeksArray/index.ts @@ -0,0 +1 @@ +export { getWeeksArray } from './getWeeksArray'; diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts new file mode 100644 index 0000000000..c58f5210e8 --- /dev/null +++ b/packages/date-utils/src/index.ts @@ -0,0 +1,34 @@ +export { addDaysUTC } from './addDaysUTC'; +export { addMonthsUTC } from './addMonthsUTC'; +export * from './constants'; +export { getDaysInUTCMonth } from './getDaysInUTCMonth'; +export { getFirstOfMonth } from './getFirstOfMonth'; +export { getFullMonthLabel } from './getFullMonthLabel'; +export { getISODate } from './getISODate'; +export { getLastOfMonth } from './getLastOfMonth'; +export { getLocaleMonths } from './getLocaleMonths'; +export { getLocaleWeekdays } from './getLocaleWeekdays'; +export { getMonthIndex } from './getMonthIndex'; +export { getMonthName } from './getMonthName'; +export { getUTCDateString } from './getUTCDateString'; +export { getWeekdayName } from './getWeekdayName'; +export { getWeeksArray } from './getWeeksArray'; +export { isCurrentUTCDay } from './isCurrentUTCDay'; +export { isOnOrAfter } from './isOnOrAfter'; +export { isOnOrBefore } from './isOnOrBefore'; +export { isSameTZDay } from './isSameTZDay'; +export { isSameUTCDay } from './isSameUTCDay'; +export { isSameUTCMonth } from './isSameUTCMonth'; +export { isSameUTCRange } from './isSameUTCRange'; +export { isTodayTZ } from './isTodayTZ'; +export { isValidDate } from './isValidDate'; +export { isValidLocale } from './isValidLocale'; +export { maxDate } from './maxDate'; +export { minDate } from './minDate'; +export { newUTC } from './newUTC'; +export { setToUTCMidnight } from './setToUTCMidnight'; +export { setUTCDate } from './setUTCDate'; +export { setUTCMonth } from './setUTCMonth'; +export { setUTCYear } from './setUTCYear'; +export { toDate } from './toDate'; +export * from './types'; diff --git a/packages/date-utils/src/isCurrentUTCDay/index.ts b/packages/date-utils/src/isCurrentUTCDay/index.ts new file mode 100644 index 0000000000..9a89a3cdeb --- /dev/null +++ b/packages/date-utils/src/isCurrentUTCDay/index.ts @@ -0,0 +1 @@ +export { isCurrentUTCDay } from './isCurrentUTCDay'; diff --git a/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts new file mode 100644 index 0000000000..698fcb4364 --- /dev/null +++ b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts @@ -0,0 +1,24 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { isCurrentUTCDay } from '.'; + +describe('packages/date-utils/isCurrentUTCDay', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + test('returns true when given UTC dates', () => { + const midnightUTC = newUTC(2020, Month.December, 25, 0, 0); + const elevenUTC = newUTC(2020, Month.December, 25, 23, 59); + jest.setSystemTime(midnightUTC); + expect(isCurrentUTCDay(elevenUTC)).toBe(true); + }); + + test('returns false when different UTC dates', () => { + const midnightUTC = newUTC(2020, Month.December, 25, 0, 0); + const elevenUTC_prev = newUTC(2020, Month.December, 24, 23, 59); + jest.setSystemTime(midnightUTC); + expect(isCurrentUTCDay(elevenUTC_prev)).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/isCurrentUTCDay/index.ts b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isCurrentUTCDay/index.ts rename to packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts diff --git a/packages/date-utils/src/isOnOrAfter/index.ts b/packages/date-utils/src/isOnOrAfter/index.ts new file mode 100644 index 0000000000..e557e99838 --- /dev/null +++ b/packages/date-utils/src/isOnOrAfter/index.ts @@ -0,0 +1 @@ +export { isOnOrAfter } from './isOnOrAfter'; diff --git a/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts new file mode 100644 index 0000000000..9d744b0dcc --- /dev/null +++ b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts @@ -0,0 +1,22 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { isOnOrAfter } from '.'; + +describe('packages/date-utils/isOnOrAfter', () => { + const jan = newUTC(2023, Month.January, 1); + const jul = newUTC(2023, Month.July, 5); + const dec = newUTC(2023, Month.December, 26); + + test('d1 is after as d2', () => { + expect(isOnOrAfter(dec, jul)).toBe(true); + }); + + test('d1 is same as d2', () => { + expect(isOnOrAfter(jul, jul)).toBe(true); + }); + + test('d1 is before d2', () => { + expect(isOnOrAfter(jan, jul)).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/isOnOrAfter/index.ts b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isOnOrAfter/index.ts rename to packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts diff --git a/packages/date-utils/src/isOnOrBefore/index.ts b/packages/date-utils/src/isOnOrBefore/index.ts new file mode 100644 index 0000000000..b31747156b --- /dev/null +++ b/packages/date-utils/src/isOnOrBefore/index.ts @@ -0,0 +1 @@ +export { isOnOrBefore } from './isOnOrBefore'; diff --git a/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts new file mode 100644 index 0000000000..d0135000f5 --- /dev/null +++ b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts @@ -0,0 +1,22 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { isOnOrBefore } from '.'; + +describe('packages/date-utils/isOnOrBefore', () => { + const jan = newUTC(2023, Month.January, 1); + const jul = newUTC(2023, Month.July, 5); + const dec = newUTC(2023, Month.December, 26); + + test('d1 is before d2', () => { + expect(isOnOrBefore(jan, jul)).toBe(true); + }); + + test('d1 is same as d2', () => { + expect(isOnOrBefore(jul, jul)).toBe(true); + }); + + test('d1 is after as d2', () => { + expect(isOnOrBefore(dec, jul)).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/isOnOrBefore/index.ts b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts similarity index 93% rename from packages/date-picker/src/shared/utils/isOnOrBefore/index.ts rename to packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts index 46d816fdf0..3fcc704466 100644 --- a/packages/date-picker/src/shared/utils/isOnOrBefore/index.ts +++ b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts @@ -2,7 +2,6 @@ import { isBefore } from 'date-fns'; import { isSameUTCDay } from '../isSameUTCDay'; -// TODO: tests export const isOnOrBefore = (day: Date, dayToCompare: Date) => { return isSameUTCDay(day, dayToCompare) || isBefore(day, dayToCompare); }; diff --git a/packages/date-utils/src/isSameTZDay/index.ts b/packages/date-utils/src/isSameTZDay/index.ts new file mode 100644 index 0000000000..4a813b593c --- /dev/null +++ b/packages/date-utils/src/isSameTZDay/index.ts @@ -0,0 +1 @@ +export { isSameTZDay } from './isSameTZDay'; diff --git a/packages/date-picker/src/shared/utils/isSameTZDay/isSameTZDay.spec.ts b/packages/date-utils/src/isSameTZDay/isSameTZDay.spec.ts similarity index 97% rename from packages/date-picker/src/shared/utils/isSameTZDay/isSameTZDay.spec.ts rename to packages/date-utils/src/isSameTZDay/isSameTZDay.spec.ts index 38ba902462..ad74ecefd5 100644 --- a/packages/date-picker/src/shared/utils/isSameTZDay/isSameTZDay.spec.ts +++ b/packages/date-utils/src/isSameTZDay/isSameTZDay.spec.ts @@ -1,8 +1,8 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { isSameTZDay } from './index'; -describe('packages/date-picker/utils/isSameTZDay', () => { +describe('packages/date-utils/isSameTZDay', () => { const utc24 = new Date(Date.UTC(2023, Month.December, 24, 0, 0, 0)); const utc25 = new Date(Date.UTC(2023, Month.December, 25, 0, 0, 0)); const utc26 = new Date(Date.UTC(2023, Month.December, 26, 0, 0, 0)); diff --git a/packages/date-picker/src/shared/utils/isSameTZDay/index.ts b/packages/date-utils/src/isSameTZDay/isSameTZDay.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isSameTZDay/index.ts rename to packages/date-utils/src/isSameTZDay/isSameTZDay.ts diff --git a/packages/date-utils/src/isSameUTCDay/index.ts b/packages/date-utils/src/isSameUTCDay/index.ts new file mode 100644 index 0000000000..e345ca51dd --- /dev/null +++ b/packages/date-utils/src/isSameUTCDay/index.ts @@ -0,0 +1 @@ +export { isSameUTCDay } from './isSameUTCDay'; diff --git a/packages/date-picker/src/shared/utils/isSameUTCDay/isSameUTCDay.spec.ts b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts similarity index 96% rename from packages/date-picker/src/shared/utils/isSameUTCDay/isSameUTCDay.spec.ts rename to packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts index b3df30bb3a..256bda59f9 100644 --- a/packages/date-picker/src/shared/utils/isSameUTCDay/isSameUTCDay.spec.ts +++ b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts @@ -2,7 +2,7 @@ import tzMock from 'timezone-mock'; import { isSameUTCDay } from '.'; -describe('packages/date-picker/utils/isSameUTCDay', () => { +describe('packages/date-utils/isSameUTCDay', () => { beforeEach(() => { tzMock.register('US/Eastern'); }); diff --git a/packages/date-picker/src/shared/utils/isSameUTCDay/index.ts b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isSameUTCDay/index.ts rename to packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts diff --git a/packages/date-utils/src/isSameUTCMonth/index.ts b/packages/date-utils/src/isSameUTCMonth/index.ts new file mode 100644 index 0000000000..c6482178a7 --- /dev/null +++ b/packages/date-utils/src/isSameUTCMonth/index.ts @@ -0,0 +1 @@ +export { isSameUTCMonth } from './isSameUTCMonth'; diff --git a/packages/date-picker/src/shared/utils/isSameUTCMonth/isSameUTCMonth.spec.ts b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts similarity index 95% rename from packages/date-picker/src/shared/utils/isSameUTCMonth/isSameUTCMonth.spec.ts rename to packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts index f90c1cac57..8e648e00b9 100644 --- a/packages/date-picker/src/shared/utils/isSameUTCMonth/isSameUTCMonth.spec.ts +++ b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts @@ -1,10 +1,10 @@ import tzMock from 'timezone-mock'; -import { Month } from '../../constants'; +import { Month } from '../constants'; import { isSameUTCMonth } from '.'; -describe('packages/date-picker/utils/isSameUTCMonth', () => { +describe('packages/date-utils/isSameUTCMonth', () => { beforeEach(() => { tzMock.register('US/Eastern'); }); diff --git a/packages/date-picker/src/shared/utils/isSameUTCMonth/index.ts b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isSameUTCMonth/index.ts rename to packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts diff --git a/packages/date-utils/src/isSameUTCRange/index.ts b/packages/date-utils/src/isSameUTCRange/index.ts new file mode 100644 index 0000000000..014a4a4a2d --- /dev/null +++ b/packages/date-utils/src/isSameUTCRange/index.ts @@ -0,0 +1 @@ +export { isSameUTCRange } from './isSameUTCRange'; diff --git a/packages/date-utils/src/isSameUTCRange/isSameUTCRange.spec.ts b/packages/date-utils/src/isSameUTCRange/isSameUTCRange.spec.ts new file mode 100644 index 0000000000..efb237182b --- /dev/null +++ b/packages/date-utils/src/isSameUTCRange/isSameUTCRange.spec.ts @@ -0,0 +1,84 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { isSameUTCRange } from '.'; + +describe('packages/date-utils/isSameUTCRange', () => { + test('same start UTC, same end UTC', () => { + expect( + isSameUTCRange( + [ + newUTC(2020, Month.June, 15, 0, 0), + newUTC(2020, Month.July, 15, 0, 0), + ], + [ + newUTC(2020, Month.June, 15, 23, 0), + newUTC(2020, Month.July, 15, 23, 0), + ], + ), + ).toBe(true); + }); + test('same start UTC, different end UTC', () => { + expect( + isSameUTCRange( + [ + newUTC(2020, Month.June, 15, 0, 0), + newUTC(2020, Month.July, 15, 0, 0), + ], + [ + newUTC(2020, Month.June, 15, 23, 0), + newUTC(2020, Month.July, 14, 23, 0), + ], + ), + ).toBe(false); + }); + test('different start UTC, same end UTC', () => { + expect( + isSameUTCRange( + [ + newUTC(2020, Month.June, 15, 0, 0), + newUTC(2020, Month.July, 15, 0, 0), + ], + [ + newUTC(2020, Month.June, 14, 23, 0), + newUTC(2020, Month.July, 15, 23, 0), + ], + ), + ).toBe(false); + }); + test('different start UTC, different end UTC', () => { + expect( + isSameUTCRange( + [ + newUTC(2020, Month.June, 15, 0, 0), + newUTC(2020, Month.July, 15, 0, 0), + ], + [ + newUTC(2020, Month.June, 14, 23, 0), + newUTC(2020, Month.July, 14, 23, 0), + ], + ), + ).toBe(false); + }); + + test('undefined start', () => { + expect( + isSameUTCRange(undefined, [ + newUTC(2020, Month.June, 15, 23, 0), + newUTC(2020, Month.July, 14, 23, 0), + ]), + ).toBe(false); + }); + + test('undefined end', () => { + expect( + isSameUTCRange( + [ + newUTC(2020, Month.June, 15, 23, 0), + newUTC(2020, Month.July, 14, 23, 0), + ], + undefined, + ), + ).toBe(false); + }); +}); diff --git a/packages/date-picker/src/shared/utils/isSameUTCRange/index.ts b/packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isSameUTCRange/index.ts rename to packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts diff --git a/packages/date-utils/src/isTodayTZ/index.ts b/packages/date-utils/src/isTodayTZ/index.ts new file mode 100644 index 0000000000..f8f3937d4a --- /dev/null +++ b/packages/date-utils/src/isTodayTZ/index.ts @@ -0,0 +1 @@ +export { isTodayTZ } from './isTodayTZ'; diff --git a/packages/date-picker/src/shared/utils/isTodayTZ/isTodayTZ.spec.ts b/packages/date-utils/src/isTodayTZ/isTodayTZ.spec.ts similarity index 97% rename from packages/date-picker/src/shared/utils/isTodayTZ/isTodayTZ.spec.ts rename to packages/date-utils/src/isTodayTZ/isTodayTZ.spec.ts index b627b827bb..e49ad73bbe 100644 --- a/packages/date-picker/src/shared/utils/isTodayTZ/isTodayTZ.spec.ts +++ b/packages/date-utils/src/isTodayTZ/isTodayTZ.spec.ts @@ -1,8 +1,8 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { isTodayTZ } from '.'; -describe('packages/date-picker/utils/isTodayTZ', () => { +describe('packages/date-utils/isTodayTZ', () => { const utc25 = new Date(Date.UTC(2023, Month.December, 25, 0, 0, 0)); describe('NYC Client', () => { diff --git a/packages/date-picker/src/shared/utils/isTodayTZ/index.ts b/packages/date-utils/src/isTodayTZ/isTodayTZ.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isTodayTZ/index.ts rename to packages/date-utils/src/isTodayTZ/isTodayTZ.ts diff --git a/packages/date-utils/src/isValidDate/index.ts b/packages/date-utils/src/isValidDate/index.ts new file mode 100644 index 0000000000..960580c147 --- /dev/null +++ b/packages/date-utils/src/isValidDate/index.ts @@ -0,0 +1,5 @@ +export { + isValidDate, + isValidDateObject, + isValidDateString, +} from './isValidDate'; diff --git a/packages/date-picker/src/shared/utils/isValidDate/isValidDate.spec.ts b/packages/date-utils/src/isValidDate/isValidDate.spec.ts similarity index 94% rename from packages/date-picker/src/shared/utils/isValidDate/isValidDate.spec.ts rename to packages/date-utils/src/isValidDate/isValidDate.spec.ts index 97873594e8..854974cd24 100644 --- a/packages/date-picker/src/shared/utils/isValidDate/isValidDate.spec.ts +++ b/packages/date-utils/src/isValidDate/isValidDate.spec.ts @@ -1,6 +1,6 @@ import { isValidDate, isValidDateString } from '.'; -describe('packages/date-picker/utils/isValidDate', () => { +describe('packages/date-utils/isValidDate', () => { test('accepts Date objects', () => { expect(isValidDate(new Date())).toBe(true); expect(isValidDate(new Date(Date.UTC(2023, 1, 1)))).toBe(true); diff --git a/packages/date-picker/src/shared/utils/isValidDate/index.ts b/packages/date-utils/src/isValidDate/isValidDate.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isValidDate/index.ts rename to packages/date-utils/src/isValidDate/isValidDate.ts diff --git a/packages/date-utils/src/isValidLocale/index.ts b/packages/date-utils/src/isValidLocale/index.ts new file mode 100644 index 0000000000..0ec39a386a --- /dev/null +++ b/packages/date-utils/src/isValidLocale/index.ts @@ -0,0 +1 @@ +export { isValidLocale } from './isValidLocale'; diff --git a/packages/date-picker/src/shared/utils/isValidLocale/isValidLocale.spec.ts b/packages/date-utils/src/isValidLocale/isValidLocale.spec.ts similarity index 88% rename from packages/date-picker/src/shared/utils/isValidLocale/isValidLocale.spec.ts rename to packages/date-utils/src/isValidLocale/isValidLocale.spec.ts index 785281f658..0bb8201c4e 100644 --- a/packages/date-picker/src/shared/utils/isValidLocale/isValidLocale.spec.ts +++ b/packages/date-utils/src/isValidLocale/isValidLocale.spec.ts @@ -1,6 +1,6 @@ import { isValidLocale } from '.'; -describe('packages/date-picker/utils/isValidLocale', () => { +describe('packages/date-utils/isValidLocale', () => { test('en-US is valid', () => { expect(isValidLocale('en-US')).toBeTruthy(); }); diff --git a/packages/date-picker/src/shared/utils/isValidLocale/index.ts b/packages/date-utils/src/isValidLocale/isValidLocale.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isValidLocale/index.ts rename to packages/date-utils/src/isValidLocale/isValidLocale.ts diff --git a/packages/date-utils/src/maxDate/index.ts b/packages/date-utils/src/maxDate/index.ts new file mode 100644 index 0000000000..a192be531d --- /dev/null +++ b/packages/date-utils/src/maxDate/index.ts @@ -0,0 +1 @@ +export { maxDate } from './maxDate'; diff --git a/packages/date-utils/src/maxDate/maxDate.spec.ts b/packages/date-utils/src/maxDate/maxDate.spec.ts new file mode 100644 index 0000000000..84f93cd79b --- /dev/null +++ b/packages/date-utils/src/maxDate/maxDate.spec.ts @@ -0,0 +1,42 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { maxDate } from '.'; + +describe('packages/date-utils/maxDate', () => { + test('returns the last date', () => { + expect( + maxDate([ + newUTC(2020, Month.January, 1), + newUTC(2020, Month.January, 2), + newUTC(2020, Month.January, 3), + ]), + ).toEqual(newUTC(2020, Month.January, 3)); + }); + + test('returns the last date within seconds', () => { + expect( + maxDate([ + newUTC(2020, Month.January, 1, 12, 0, 0), + newUTC(2020, Month.January, 1, 12, 0, 1), + newUTC(2020, Month.January, 1, 11, 59, 59), + ]), + ).toEqual(newUTC(2020, Month.January, 1, 12, 0, 1)); + }); + + test('when array elements are null/undefined', () => { + expect( + maxDate([ + newUTC(2020, Month.January, 1), + null, + newUTC(2020, Month.January, 2), + undefined, + newUTC(2020, Month.January, 3), + ]), + ).toEqual(newUTC(2020, Month.January, 3)); + }); + + test('returns undefined for invalid array', () => { + expect(maxDate([])).toBeUndefined(); + }); +}); diff --git a/packages/date-picker/src/shared/utils/maxDate/index.ts b/packages/date-utils/src/maxDate/maxDate.ts similarity index 72% rename from packages/date-picker/src/shared/utils/maxDate/index.ts rename to packages/date-utils/src/maxDate/maxDate.ts index e8e7eb023a..1c03db1073 100644 --- a/packages/date-picker/src/shared/utils/maxDate/index.ts +++ b/packages/date-utils/src/maxDate/maxDate.ts @@ -1,8 +1,10 @@ import { max } from 'date-fns'; -import { isDefined } from '../isDefined'; +import { isDefined } from '@leafygreen-ui/lib'; -// TODO: tests +/** + * Returns the fist date of an array of dates + */ export const maxDate = ( datesArray: Array, ): Date | undefined => { diff --git a/packages/date-utils/src/minDate/index.ts b/packages/date-utils/src/minDate/index.ts new file mode 100644 index 0000000000..134b93e96c --- /dev/null +++ b/packages/date-utils/src/minDate/index.ts @@ -0,0 +1 @@ +export { minDate } from './minDate'; diff --git a/packages/date-utils/src/minDate/minDate.spec.ts b/packages/date-utils/src/minDate/minDate.spec.ts new file mode 100644 index 0000000000..c66db2a908 --- /dev/null +++ b/packages/date-utils/src/minDate/minDate.spec.ts @@ -0,0 +1,42 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { minDate } from '.'; + +describe('packages/date-utils/minDate', () => { + test('returns the first date', () => { + expect( + minDate([ + newUTC(2020, Month.January, 1), + newUTC(2020, Month.January, 2), + newUTC(2020, Month.January, 3), + ]), + ).toEqual(newUTC(2020, Month.January, 1)); + }); + + test('returns the first date within seconds', () => { + expect( + minDate([ + newUTC(2020, Month.January, 1, 12, 0, 0), + newUTC(2020, Month.January, 1, 12, 0, 1), + newUTC(2020, Month.January, 1, 11, 59, 59), + ]), + ).toEqual(newUTC(2020, Month.January, 1, 11, 59, 59)); + }); + + test('when array elements are null/undefined', () => { + expect( + minDate([ + newUTC(2020, Month.January, 1), + null, + newUTC(2020, Month.January, 2), + undefined, + newUTC(2020, Month.January, 3), + ]), + ).toEqual(newUTC(2020, Month.January, 1)); + }); + + test('returns undefined for invalid array', () => { + expect(minDate([])).toBeUndefined(); + }); +}); diff --git a/packages/date-picker/src/shared/utils/minDate/index.ts b/packages/date-utils/src/minDate/minDate.ts similarity index 72% rename from packages/date-picker/src/shared/utils/minDate/index.ts rename to packages/date-utils/src/minDate/minDate.ts index dc33c66e35..d7b83b35c4 100644 --- a/packages/date-picker/src/shared/utils/minDate/index.ts +++ b/packages/date-utils/src/minDate/minDate.ts @@ -1,8 +1,10 @@ import { min } from 'date-fns'; -import { isDefined } from '../isDefined'; +import { isDefined } from '@leafygreen-ui/lib'; -// TODO: tests +/** + * Returns the fist date of an array of dates + */ export const minDate = ( datesArray: Array, ): Date | undefined => { diff --git a/packages/date-utils/src/newUTC/index.ts b/packages/date-utils/src/newUTC/index.ts new file mode 100644 index 0000000000..f4d580eea4 --- /dev/null +++ b/packages/date-utils/src/newUTC/index.ts @@ -0,0 +1 @@ +export { newUTC } from './newUTC'; diff --git a/packages/date-picker/src/shared/utils/newUTC/newUTC.spec.ts b/packages/date-utils/src/newUTC/newUTC.spec.ts similarity index 88% rename from packages/date-picker/src/shared/utils/newUTC/newUTC.spec.ts rename to packages/date-utils/src/newUTC/newUTC.spec.ts index 9b31c31457..3f697f1f45 100644 --- a/packages/date-picker/src/shared/utils/newUTC/newUTC.spec.ts +++ b/packages/date-utils/src/newUTC/newUTC.spec.ts @@ -1,6 +1,6 @@ import { newUTC } from '.'; -describe('packages/date-picker/utils/newUTC', () => { +describe('packages/date-utils/newUTC', () => { test('creates a new UTC date', () => { const date = newUTC(2023, 11, 5); expect(date.getUTCDate()).toEqual(5); diff --git a/packages/date-picker/src/shared/utils/newUTC/index.ts b/packages/date-utils/src/newUTC/newUTC.ts similarity index 100% rename from packages/date-picker/src/shared/utils/newUTC/index.ts rename to packages/date-utils/src/newUTC/newUTC.ts diff --git a/packages/date-utils/src/normalizeLocale/index.ts b/packages/date-utils/src/normalizeLocale/index.ts new file mode 100644 index 0000000000..c9dfaf2d19 --- /dev/null +++ b/packages/date-utils/src/normalizeLocale/index.ts @@ -0,0 +1 @@ +export { normalizeLocale } from './normalizeLocale'; diff --git a/packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts b/packages/date-utils/src/normalizeLocale/normalizeLocale.spec.ts similarity index 84% rename from packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts rename to packages/date-utils/src/normalizeLocale/normalizeLocale.spec.ts index 171c0d7ad6..4b5a4c0ba5 100644 --- a/packages/date-picker/src/shared/utils/normalizeLocale/normalizeLocale.spec.ts +++ b/packages/date-utils/src/normalizeLocale/normalizeLocale.spec.ts @@ -1,6 +1,6 @@ import { normalizeLocale } from '.'; -describe('packages/date-picker/utils/normalizeLocale', () => { +describe('packages/date-utils/normalizeLocale', () => { test('pass through valid locale', () => { expect(normalizeLocale('de-DE')).toBe('de-DE'); }); diff --git a/packages/date-picker/src/shared/utils/normalizeLocale/index.ts b/packages/date-utils/src/normalizeLocale/normalizeLocale.ts similarity index 100% rename from packages/date-picker/src/shared/utils/normalizeLocale/index.ts rename to packages/date-utils/src/normalizeLocale/normalizeLocale.ts diff --git a/packages/date-utils/src/setToUTCMidnight/index.ts b/packages/date-utils/src/setToUTCMidnight/index.ts new file mode 100644 index 0000000000..85b05b93ae --- /dev/null +++ b/packages/date-utils/src/setToUTCMidnight/index.ts @@ -0,0 +1 @@ +export { setToUTCMidnight } from './setToUTCMidnight'; diff --git a/packages/date-picker/src/shared/utils/setToUTCMidnight/setToUTCMidnight.spec.ts b/packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.spec.ts similarity index 84% rename from packages/date-picker/src/shared/utils/setToUTCMidnight/setToUTCMidnight.spec.ts rename to packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.spec.ts index 854b340a34..79ba29569a 100644 --- a/packages/date-picker/src/shared/utils/setToUTCMidnight/setToUTCMidnight.spec.ts +++ b/packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.spec.ts @@ -1,6 +1,6 @@ import { setToUTCMidnight } from '.'; -describe('packages/date-picker/utils/setToUTCMidnight', () => { +describe('packages/date-utils/setToUTCMidnight', () => { test('sets a date to UTC midnight', () => { const date = new Date(); const midnight = setToUTCMidnight(date); diff --git a/packages/date-picker/src/shared/utils/setToUTCMidnight/index.ts b/packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.ts similarity index 53% rename from packages/date-picker/src/shared/utils/setToUTCMidnight/index.ts rename to packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.ts index 63667fdcd6..fbf97f19ba 100644 --- a/packages/date-picker/src/shared/utils/setToUTCMidnight/index.ts +++ b/packages/date-utils/src/setToUTCMidnight/setToUTCMidnight.ts @@ -9,13 +9,3 @@ export const setToUTCMidnight = (date: Date): Date => { midnight.setUTCMilliseconds(0); return midnight; }; - -/** @deprecated */ -export const setToMidnight = (date: Date): Date => { - const midnight = new Date(date); - midnight.setHours(0); - midnight.setMinutes(0); - midnight.setSeconds(0); - midnight.setMilliseconds(0); - return midnight; -}; diff --git a/packages/date-utils/src/setUTCDate/index.ts b/packages/date-utils/src/setUTCDate/index.ts new file mode 100644 index 0000000000..c2f7eaa632 --- /dev/null +++ b/packages/date-utils/src/setUTCDate/index.ts @@ -0,0 +1 @@ +export { setUTCDate } from './setUTCDate'; diff --git a/packages/date-utils/src/setUTCDate/setUTCDate.spec.ts b/packages/date-utils/src/setUTCDate/setUTCDate.spec.ts new file mode 100644 index 0000000000..3c99462d8c --- /dev/null +++ b/packages/date-utils/src/setUTCDate/setUTCDate.spec.ts @@ -0,0 +1,22 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { setUTCDate } from '.'; + +describe('packages/date-utils/setUTCDate', () => { + test('at noon', () => { + expect(setUTCDate(newUTC(2020, Month.July, 4, 12, 0), 5)).toEqual( + newUTC(2020, Month.July, 5, 12, 0, 0), + ); + }); + test('at midnight', () => { + expect(setUTCDate(newUTC(2020, Month.July, 4, 0, 0), 5)).toEqual( + newUTC(2020, Month.July, 5, 0, 0, 0), + ); + }); + test('at 11:59pm', () => { + expect(setUTCDate(newUTC(2020, Month.July, 4, 23, 59), 5)).toEqual( + newUTC(2020, Month.July, 5, 23, 59), + ); + }); +}); diff --git a/packages/date-picker/src/shared/utils/setUTCDate/index.ts b/packages/date-utils/src/setUTCDate/setUTCDate.ts similarity index 100% rename from packages/date-picker/src/shared/utils/setUTCDate/index.ts rename to packages/date-utils/src/setUTCDate/setUTCDate.ts diff --git a/packages/date-utils/src/setUTCMonth/index.ts b/packages/date-utils/src/setUTCMonth/index.ts new file mode 100644 index 0000000000..f488906e8d --- /dev/null +++ b/packages/date-utils/src/setUTCMonth/index.ts @@ -0,0 +1 @@ +export { setUTCMonth } from './setUTCMonth'; diff --git a/packages/date-picker/src/shared/utils/setUTCMonth/setUTCMonth.spec.ts b/packages/date-utils/src/setUTCMonth/setUTCMonth.spec.ts similarity index 92% rename from packages/date-picker/src/shared/utils/setUTCMonth/setUTCMonth.spec.ts rename to packages/date-utils/src/setUTCMonth/setUTCMonth.spec.ts index 1d9b5349a8..9ec5e95b1e 100644 --- a/packages/date-picker/src/shared/utils/setUTCMonth/setUTCMonth.spec.ts +++ b/packages/date-utils/src/setUTCMonth/setUTCMonth.spec.ts @@ -1,10 +1,10 @@ import tzMock from 'timezone-mock'; -import { Month } from '../../constants'; +import { Month } from '../constants'; import { setUTCMonth } from '.'; -describe('packages/date-picker/utils/setUTCMonth', () => { +describe('packages/date-utils/setUTCMonth', () => { test('sets the month', () => { const d = new Date(Date.UTC(2023, Month.September, 10)); const d2 = setUTCMonth(d, Month.March); diff --git a/packages/date-picker/src/shared/utils/setUTCMonth/index.ts b/packages/date-utils/src/setUTCMonth/setUTCMonth.ts similarity index 100% rename from packages/date-picker/src/shared/utils/setUTCMonth/index.ts rename to packages/date-utils/src/setUTCMonth/setUTCMonth.ts diff --git a/packages/date-utils/src/setUTCYear/index.ts b/packages/date-utils/src/setUTCYear/index.ts new file mode 100644 index 0000000000..f2e1e60978 --- /dev/null +++ b/packages/date-utils/src/setUTCYear/index.ts @@ -0,0 +1 @@ +export { setUTCYear } from './setUTCYear'; diff --git a/packages/date-utils/src/setUTCYear/setUTCYear.spec.ts b/packages/date-utils/src/setUTCYear/setUTCYear.spec.ts new file mode 100644 index 0000000000..1354ff189b --- /dev/null +++ b/packages/date-utils/src/setUTCYear/setUTCYear.spec.ts @@ -0,0 +1,22 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { setUTCYear } from '.'; + +describe('packages/date-utils/setUTCYear', () => { + test('sets the year of a date', () => { + expect(setUTCYear(newUTC(2020, Month.July, 5), 2023)).toEqual( + newUTC(2023, Month.July, 5), + ); + }); + test('on December 31', () => { + expect(setUTCYear(newUTC(2020, Month.December, 31, 23, 59), 2023)).toEqual( + newUTC(2023, Month.December, 31, 23, 59), + ); + }); + test('on January 1', () => { + expect(setUTCYear(newUTC(2020, Month.January, 1, 0, 0), 2023)).toEqual( + newUTC(2023, Month.January, 1, 0, 0), + ); + }); +}); diff --git a/packages/date-picker/src/shared/utils/setUTCYear/index.ts b/packages/date-utils/src/setUTCYear/setUTCYear.ts similarity index 100% rename from packages/date-picker/src/shared/utils/setUTCYear/index.ts rename to packages/date-utils/src/setUTCYear/setUTCYear.ts diff --git a/packages/date-utils/src/sortDates/index.ts b/packages/date-utils/src/sortDates/index.ts new file mode 100644 index 0000000000..7a51618383 --- /dev/null +++ b/packages/date-utils/src/sortDates/index.ts @@ -0,0 +1 @@ +export { sortDates } from './sortDates'; diff --git a/packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts b/packages/date-utils/src/sortDates/sortDates.spec.ts similarity index 91% rename from packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts rename to packages/date-utils/src/sortDates/sortDates.spec.ts index b86b1ae3fb..4ef3d88a5a 100644 --- a/packages/date-picker/src/shared/utils/sortDates/sortDates.spec.ts +++ b/packages/date-utils/src/sortDates/sortDates.spec.ts @@ -1,9 +1,9 @@ -import { Month } from '../../constants'; +import { Month } from '../constants'; import { newUTC } from '../newUTC'; import { sortDates } from '.'; -describe('packages/date-picker/utils/sortDates', () => { +describe('packages/date-utils/sortDates', () => { const testDates = [ newUTC(2023, Month.September, 10), newUTC(2020, Month.March, 13), diff --git a/packages/date-picker/src/shared/utils/sortDates/index.ts b/packages/date-utils/src/sortDates/sortDates.ts similarity index 100% rename from packages/date-picker/src/shared/utils/sortDates/index.ts rename to packages/date-utils/src/sortDates/sortDates.ts diff --git a/packages/date-utils/src/toDate/index.ts b/packages/date-utils/src/toDate/index.ts new file mode 100644 index 0000000000..7ebff98224 --- /dev/null +++ b/packages/date-utils/src/toDate/index.ts @@ -0,0 +1 @@ +export { toDate } from './toDate'; diff --git a/packages/date-picker/src/shared/utils/toDate/toDate.spec.ts b/packages/date-utils/src/toDate/toDate.spec.ts similarity index 93% rename from packages/date-picker/src/shared/utils/toDate/toDate.spec.ts rename to packages/date-utils/src/toDate/toDate.spec.ts index 6e7dff4f81..f375f22d0c 100644 --- a/packages/date-picker/src/shared/utils/toDate/toDate.spec.ts +++ b/packages/date-utils/src/toDate/toDate.spec.ts @@ -1,6 +1,6 @@ import { toDate } from '.'; -describe('packages/date-picker/utils/toDate', () => { +describe('packages/date-utils/toDate', () => { test('Date', () => { const d1 = new Date(); const d2 = toDate(d1); diff --git a/packages/date-picker/src/shared/utils/toDate/index.ts b/packages/date-utils/src/toDate/toDate.ts similarity index 100% rename from packages/date-picker/src/shared/utils/toDate/index.ts rename to packages/date-utils/src/toDate/toDate.ts diff --git a/packages/date-utils/src/types.ts b/packages/date-utils/src/types.ts new file mode 100644 index 0000000000..85576dd80d --- /dev/null +++ b/packages/date-utils/src/types.ts @@ -0,0 +1,24 @@ +export type DateType = Date | null; +export type DateRangeType = [DateType, DateType]; + +export type LocaleString = 'iso8601' | string; + +export interface MonthObject { + long: string; + short: string; +} + +/** + * Object representing the abbreviations of a given weekday. + * Abbreviation formats defined in Unicode: https://www.unicode.org/reports/tr35/tr35-67/tr35-dates.html#dfst-weekday + */ +export interface WeekdayObject { + /** The long-form weekday name (e.g. Tuesday)*/ + long: string; + /** An abbreviated weekday name (e.g. Tue) */ + abbr: string; + /** A shorter weekday name (e.g. Tu)*/ + short?: string; + /** The shortest weekday name (e.g. T) */ + narrow: string; +} diff --git a/packages/date-utils/tsconfig.json b/packages/date-utils/tsconfig.json new file mode 100644 index 0000000000..72a0203d44 --- /dev/null +++ b/packages/date-utils/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@lg-tools/build/config/package.tsconfig.json", + "compilerOptions": { + "declarationDir": "dist", + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], + "@leafygreen-ui/*": ["../*/src"] + } + }, + "include": [ + "src/**/*" + ], + "exclude": ["**/*.spec.*", "**/*.story.*"], + "references": [ + { + "path": "../emotion" + }, + { + "path": "../lib" + } + ] +} diff --git a/packages/date-picker/src/shared/utils/cloneReverse/cloneReverse.spec.ts b/packages/lib/src/helpers/cloneReverse/cloneReverse.spec.ts similarity index 100% rename from packages/date-picker/src/shared/utils/cloneReverse/cloneReverse.spec.ts rename to packages/lib/src/helpers/cloneReverse/cloneReverse.spec.ts diff --git a/packages/date-picker/src/shared/utils/cloneReverse/index.ts b/packages/lib/src/helpers/cloneReverse/index.ts similarity index 100% rename from packages/date-picker/src/shared/utils/cloneReverse/index.ts rename to packages/lib/src/helpers/cloneReverse/index.ts diff --git a/packages/lib/src/helpers/index.ts b/packages/lib/src/helpers/index.ts index 48eb071ecb..c76bd3d9cb 100644 --- a/packages/lib/src/helpers/index.ts +++ b/packages/lib/src/helpers/index.ts @@ -1,5 +1,8 @@ -export { pickAndOmit } from './pickAndOmit'; -export { consoleOnce } from './consoleOnce'; export { allEqual } from './allEqual'; +export { cloneReverse } from './cloneReverse'; +export { consoleOnce } from './consoleOnce'; +export { isDefined } from './isDefined'; +export { isNotZeroLike, isZeroLike } from './isZeroLike'; +export { pickAndOmit } from './pickAndOmit'; export { rollover } from './rollover'; export { truncateStart } from './truncateStart'; diff --git a/packages/date-picker/src/shared/utils/isDefined/index.ts b/packages/lib/src/helpers/isDefined/index.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isDefined/index.ts rename to packages/lib/src/helpers/isDefined/index.ts diff --git a/packages/date-picker/src/shared/utils/isZeroLike/index.ts b/packages/lib/src/helpers/isZeroLike/index.ts similarity index 100% rename from packages/date-picker/src/shared/utils/isZeroLike/index.ts rename to packages/lib/src/helpers/isZeroLike/index.ts diff --git a/tools/test/config/jest.config.js b/tools/test/config/jest.config.js index 669097cb49..3213e2bb57 100644 --- a/tools/test/config/jest.config.js +++ b/tools/test/config/jest.config.js @@ -1,13 +1,19 @@ // For a detailed explanation regarding each configuration property, visit: // https://jestjs.io/docs/en/configuration.html -// When referencing files in this package, -// note we still need to declare the path relative to `` +// Note: When referencing files in this package, +// we still need to declare the path relative to `` (repository root) module.exports = { // The directory where Jest should output its coverage files coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['/node_modules/', '/dist/', '/index.ts', '.svg'], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/dist/', + '.svg', + '/index.tsx?', + '.(d|json|md|spec|stories|styles|types).tsx?', + ], displayName: 'Client', diff --git a/tools/test/src/index.ts b/tools/test/src/index.ts index 5159f77a6e..e12c7f4c40 100755 --- a/tools/test/src/index.ts +++ b/tools/test/src/index.ts @@ -32,6 +32,15 @@ export const test = ( : passThrough : []; + // Add coverage options + if (passThroughOptions.includes('--coverage')) { + const testDir = passThroughOptions.find(opt => !opt.startsWith('--')); + const dir = testDir ? `packages/${testDir}/src/` : ''; + const coverageFlag = + '--collectCoverageFrom=' + dir + '**/*.{js,jsx,ts,tsx}'; + passThroughOptions.push(coverageFlag); + } + const configFile = getConfigFile(options); const jestBinary = getJestBinary(options); From 128345f2b8b4472326b96d1defaeb765fa52d272 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 11 Dec 2023 16:49:20 -0500 Subject: [PATCH 324/351] Date Picker [LG-3887] fix page scroll (#2129) * fix scroll * fix propType --- packages/date-picker/src/DatePicker.stories.tsx | 7 +++++-- packages/date-picker/src/DatePicker/DatePicker.tsx | 4 ++-- .../src/DatePicker/DatePickerMenu/DatePickerMenu.tsx | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 3cb5098433..6c67a7b781 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -13,6 +13,7 @@ import { DatePickerContextProps, DatePickerProvider, } from './shared/components/DatePickerContext'; +import { MAX_DATE, MIN_DATE } from './shared/constants'; import { getProviderPropsFromStoryContext } from './shared/testutils/getProviderPropsFromStoryContext'; import { Locales, TimeZones } from './shared/testutils/testValues'; import { AutoComplete } from './shared/types'; @@ -36,7 +37,7 @@ const meta: StoryMetaType = { component: DatePicker, decorators: [ProviderWrapper], parameters: { - default: null, + default: 'LiveExample', controls: { exclude: [ 'handleValidation', @@ -63,6 +64,8 @@ const meta: StoryMetaType = { label: 'Pick a date', size: Size.Default, autoComplete: AutoComplete.Off, + min: MIN_DATE, + max: MAX_DATE, }, argTypes: { baseFontSize: { control: 'select' }, @@ -80,7 +83,7 @@ const meta: StoryMetaType = { export default meta; -export const Basic: StoryFn = props => { +export const LiveExample: StoryFn = props => { const [value, setValue] = useState(); return ( diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index c3d0cbf0a8..812f86676b 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -86,8 +86,8 @@ DatePicker.propTypes = { description: PropTypes.node, locale: PropTypes.string, timeZone: PropTypes.string, - min: PropTypes.string || PropTypes.instanceOf(Date), - max: PropTypes.string || PropTypes.instanceOf(Date), + min: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + max: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), baseFontSize: PropTypes.oneOf(Object.values(BaseFontSize)), disabled: PropTypes.bool, size: PropTypes.oneOf(Object.values(Size)), diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 1f9ecc000c..56dd3a23f5 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -185,21 +185,25 @@ export const DatePickerMenu = forwardRef( switch (key) { case keyMap.ArrowLeft: { + e.preventDefault(); nextHighlight = addDaysUTC(currentHighlight, -1); break; } case keyMap.ArrowRight: { + e.preventDefault(); nextHighlight = addDaysUTC(currentHighlight, 1); break; } case keyMap.ArrowUp: { + e.preventDefault(); nextHighlight = addDaysUTC(currentHighlight, -7); break; } case keyMap.ArrowDown: { + e.preventDefault(); nextHighlight = addDaysUTC(currentHighlight, 7); break; } From cf1ff453c10f4a09552aeda71e10b65386fb3c62 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:33:51 -0500 Subject: [PATCH 325/351] Date picker timezone tests (#2134) * mv eventContainingTargetValue, tabNTimes * updates mockTimeZone to reset properly * mv testing utils * imports * Update tsconfig.json * rename testTimeZoneLabels, testLocales * properly restore mocks * adds todo tests * update mock tz * adds queryCellISODate test util * Update DatePicker.spec.tsx * adds TZ calendar cell click tests --- packages/date-picker/package.json | 1 + .../date-picker/src/DatePicker.stories.tsx | 15 +- .../src/DatePicker/DatePicker.spec.tsx | 366 +++++++++++++----- .../src/DatePicker/DatePicker.testutils.tsx | 11 + .../DatePickerMenu/DatePickerMenu.spec.tsx | 7 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 20 +- .../CalendarGrid/CalendarGrid.stories.tsx | 9 +- .../DateInputBox/DateInputBox.stories.tsx | 6 +- .../date-picker/src/shared/testutils/index.ts | 14 +- .../shared/testutils/mockTimeZone/index.ts | 62 --- .../mockTimeZone/mockTimeZone.spec.ts | 51 --- packages/date-picker/tsconfig.json | 5 +- packages/date-utils/package.json | 2 +- packages/date-utils/src/index.ts | 3 + packages/date-utils/src/testing/index.ts | 2 + .../src/testing/mockTimeZone/index.ts | 67 ++++ .../testing/mockTimeZone/mockTimeZone.spec.ts | 64 +++ .../src/testing}/testValues.ts | 16 +- packages/date-utils/tsconfig.json | 6 +- packages/testing-lib/package.json | 10 +- .../src}/eventContainingTargetValue/index.ts | 4 +- packages/testing-lib/src/index.ts | 4 +- .../src}/tabNTimes/index.ts | 0 23 files changed, 483 insertions(+), 262 deletions(-) delete mode 100644 packages/date-picker/src/shared/testutils/mockTimeZone/index.ts delete mode 100644 packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts create mode 100644 packages/date-utils/src/testing/index.ts create mode 100644 packages/date-utils/src/testing/mockTimeZone/index.ts create mode 100644 packages/date-utils/src/testing/mockTimeZone/mockTimeZone.spec.ts rename packages/{date-picker/src/shared/testutils => date-utils/src/testing}/testValues.ts (75%) rename packages/{date-picker/src/shared/testutils => testing-lib/src}/eventContainingTargetValue/index.ts (66%) rename packages/{date-picker/src/shared/testutils => testing-lib/src}/tabNTimes/index.ts (100%) diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 9acdc0a6f7..629bc4f302 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -38,6 +38,7 @@ "devDependencies": { "@leafygreen-ui/button": "^21.0.7", "@leafygreen-ui/modal":"^16.0.3", + "@leafygreen-ui/testing-lib": "^0.3.4", "mockdate": "^3.0.5", "storybook-mock-date-decorator": "^1.0.1", "timezone-mock": "^1.3.6", diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 6c67a7b781..860c60f7ce 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -3,7 +3,12 @@ import React, { useState } from 'react'; import { StoryFn } from '@storybook/react'; import Button from '@leafygreen-ui/button'; -import { Month, newUTC } from '@leafygreen-ui/date-utils'; +import { + testLocales, + Month, + newUTC, + testTimeZoneLabels, +} from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import Modal from '@leafygreen-ui/modal'; @@ -15,7 +20,6 @@ import { } from './shared/components/DatePickerContext'; import { MAX_DATE, MIN_DATE } from './shared/constants'; import { getProviderPropsFromStoryContext } from './shared/testutils/getProviderPropsFromStoryContext'; -import { Locales, TimeZones } from './shared/testutils/testValues'; import { AutoComplete } from './shared/types'; import { DatePicker } from './DatePicker'; @@ -69,14 +73,17 @@ const meta: StoryMetaType = { }, argTypes: { baseFontSize: { control: 'select' }, - locale: { control: 'select', options: Locales }, + locale: { control: 'select', options: testLocales }, description: { control: 'text' }, label: { control: 'text' }, min: { control: 'date' }, max: { control: 'date' }, size: { control: 'select' }, state: { control: 'select' }, - timeZone: { control: 'select', options: [undefined, ...TimeZones] }, + timeZone: { + control: 'select', + options: [undefined, ...testTimeZoneLabels], + }, autoComplete: { control: 'select', options: Object.values(AutoComplete) }, }, }; diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 06b6de4dde..e27168489a 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { fireEvent, - // prettyDOM, render, waitFor, waitForElementToBeRemoved, @@ -17,10 +16,17 @@ import { setUTCMonth, setUTCYear, } from '@leafygreen-ui/date-utils'; +import { + mockTimeZone, + testTimeZones, +} from '@leafygreen-ui/date-utils/src/testing'; +import { + eventContainingTargetValue, + tabNTimes, +} from '@leafygreen-ui/testing-lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; import { defaultMax, defaultMin } from '../shared/constants'; -import { eventContainingTargetValue, tabNTimes } from '../shared/testutils'; import { getFormattedDateString, getValueFormatter } from '../shared/utils'; import { @@ -40,6 +46,10 @@ describe('packages/date-picker', () => { jest.useFakeTimers().setSystemTime(testToday); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('Rendering', () => { /// Note: Many rendering tests should be handled by Chromatic @@ -263,35 +273,6 @@ describe('packages/date-picker', () => { expect(menuContainerEl).not.toBeInTheDocument(); }); - test('if no value is set, menu opens to current month', async () => { - const { openMenu } = renderDatePicker(); - const { calendarGrid, monthSelect, yearSelect } = await openMenu(); - expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); - expect(monthSelect).toHaveValue(Month.December.toString()); - expect(yearSelect).toHaveValue('2023'); - }); - - test('if a value is set, menu opens to the month of that value', async () => { - const { openMenu } = renderDatePicker({ - value: new Date(Date.UTC(2023, Month.March, 10)), - }); - const { calendarGrid, monthSelect, yearSelect } = await openMenu(); - expect(calendarGrid).toHaveAttribute('aria-label', 'March 2023'); - expect(monthSelect).toHaveValue(Month.March.toString()); - expect(yearSelect).toHaveValue('2023'); - }); - - test('if value is invalid, menu still opens to the month of that value', async () => { - const { openMenu } = renderDatePicker({ - value: new Date(Date.UTC(2100, Month.July, 10)), - }); - const { calendarGrid, calendarCells } = await openMenu(); - expect(calendarGrid).toHaveAttribute('aria-label', 'July 2100'); - calendarCells.forEach(cell => { - expect(cell).toHaveAttribute('aria-disabled', 'true'); - }); - }); - test('renders the appropriate number of cells', async () => { const { openMenu } = renderDatePicker({ value: new Date(Date.UTC(2024, Month.February, 14)), @@ -300,8 +281,8 @@ describe('packages/date-picker', () => { expect(calendarCells).toHaveLength(29); }); - describe('when disabled is toggled to true', () => { - test('menu closes', async () => { + describe('when disabled is toggled to `true`', () => { + test('menu closes if open', async () => { const { findMenuElements, rerenderDatePicker } = renderDatePicker({ initialOpen: true, }); @@ -452,6 +433,8 @@ describe('packages/date-picker', () => { }); }); }); + + describe('when menu opens', () => {}); }); }); @@ -521,70 +504,199 @@ describe('packages/date-picker', () => { await waitFor(() => expect(menuContainerEl).not.toBeInTheDocument()); }); - test('focuses on the `today` cell by default', async () => { - const { calendarButton, findMenuElements, findByRole } = - renderDatePicker(); - userEvent.click(calendarButton); - const menuContainerEl = await findByRole('listbox'); - const { todayCell } = await findMenuElements(); - // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM - fireEvent.transitionEnd(menuContainerEl!); - expect(todayCell).toHaveFocus(); - }); + describe('if no value is set', () => { + describe.each(testTimeZones)( + 'when system time is in $tz', + ({ tz, UTCOffset }) => { + const expectedISO = '2023-12-' + (UTCOffset < 0 ? '24' : '25'); - test('focuses on the selected cell', async () => { - const value = newUTC(1994, Month.September, 10); - const { calendarButton, findMenuElements, findByRole } = - renderDatePicker({ - value: value, - }); - userEvent.click(calendarButton); - const menuContainerEl = await findByRole('listbox'); - const { queryCellByDate } = await findMenuElements(); - const valueCell = queryCellByDate(value); - // Manually fire the `transitionEnd` event. This is not fired automatically by JSDOM - fireEvent.transitionEnd(menuContainerEl!); - expect(valueCell).toHaveFocus(); + beforeEach(() => { + mockTimeZone(tz, UTCOffset); + jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('menu opens to current month', async () => { + const { calendarButton, waitForMenuToOpen } = + renderDatePicker(); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'December 2023', + ); + expect(monthSelect).toHaveValue(Month.December.toString()); + expect(yearSelect).toHaveValue('2023'); + }); + + test('initial focus (highlight) is set to `today`', async () => { + const { calendarButton, waitForMenuToOpen } = + renderDatePicker(); + userEvent.click(calendarButton); + const { calendarCells } = await waitForMenuToOpen(); + const highlightCell = calendarCells.find( + cell => cell?.getAttribute('data-highlighted') == 'true', + ); + expect(highlightCell).toHaveAttribute('data-iso', expectedISO); + expect(highlightCell).toHaveFocus(); + }); + }, + ); + + describe.each(testTimeZones)( + 'when timeZone prop is $tz', + ({ tz, UTCOffset }) => { + const expectedISO = '2023-12-' + (UTCOffset < 0 ? '24' : '25'); + + test('menu opens to current month', async () => { + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + timeZone: tz, + }); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'December 2023', + ); + expect(monthSelect).toHaveValue(Month.December.toString()); + expect(yearSelect).toHaveValue('2023'); + }); + + test('initial highlight is set to `today`', async () => { + jest.setSystemTime(newUTC(2023, Month.December, 25, 16, 0)); + + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + timeZone: tz, + }); + userEvent.click(calendarButton); + + const { calendarCells } = await waitForMenuToOpen(); + const highlightCell = calendarCells.find( + cell => cell?.getAttribute('data-highlighted') == 'true', + ); + expect(highlightCell).toHaveAttribute('data-iso', expectedISO); + expect(highlightCell).toHaveFocus(); + }); + }, + ); }); - }); - describe('Clicking a Calendar cell', () => { - test('fires a change handler', async () => { - const onDateChange = jest.fn(); - const { openMenu } = renderDatePicker({ - onDateChange, - }); - const { calendarCells } = await openMenu(); - const firstCell = calendarCells?.[0]; - userEvent.click(firstCell!); - expect(onDateChange).toHaveBeenCalled(); + describe('if a value is set', () => { + describe.each(testTimeZones)( + 'when system time is in $tz', + ({ tz, UTCOffset }) => { + beforeEach(() => { + jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC + mockTimeZone(tz, UTCOffset); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('menu opens to the month of that `value`', async () => { + const testValue = new Date(Date.UTC(2023, Month.March, 1)); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + value: testValue, + }); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'March 2023', + ); + expect(monthSelect).toHaveValue(Month.March.toString()); + expect(yearSelect).toHaveValue('2023'); + }); + + test('initial focus (highlight) is on the date of `value`', async () => { + const testValue = new Date(Date.UTC(2023, Month.March, 1)); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + value: testValue, + }); + userEvent.click(calendarButton); + const { calendarCells } = await waitForMenuToOpen(); + const highlightCell = calendarCells.find( + cell => cell?.getAttribute('data-highlighted') == 'true', + ); + expect(highlightCell).toHaveAttribute('data-iso', '2023-03-01'); + expect(highlightCell).toHaveFocus(); + }); + }, + ); + describe.each(testTimeZones)( + 'when timeZone prop is $tz', + ({ tz, UTCOffset }) => { + beforeEach(() => { + jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC + mockTimeZone(tz, UTCOffset); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('menu opens to the month of that `value`', async () => { + const testValue = new Date(Date.UTC(2023, Month.March, 1)); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + value: testValue, + }); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'March 2023', + ); + expect(monthSelect).toHaveValue(Month.March.toString()); + expect(yearSelect).toHaveValue('2023'); + }); + + test('initial focus (highlight) is on the date of `value`', async () => { + const testValue = new Date(Date.UTC(2023, Month.March, 1)); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ + value: testValue, + }); + userEvent.click(calendarButton); + const { calendarCells } = await waitForMenuToOpen(); + const highlightCell = calendarCells.find( + cell => cell?.getAttribute('data-highlighted') == 'true', + ); + expect(highlightCell).toHaveAttribute('data-iso', '2023-03-01'); + expect(highlightCell).toHaveFocus(); + }); + }, + ); }); - test('does nothing if the cell is out-of-range', async () => { - const onDateChange = jest.fn(); - const { openMenu } = renderDatePicker({ - onDateChange, - value: new Date(Date.UTC(2023, Month.September, 15)), - min: new Date(Date.UTC(2023, Month.September, 10)), + describe('if value is invalid', () => { + test('menu still opens to the month of that value', async () => { + const testValue = new Date(Date.UTC(2100, Month.July, 10)); + const { openMenu } = renderDatePicker({ + value: testValue, + }); + const { calendarGrid, calendarCells } = await openMenu(); + expect(calendarGrid).toHaveAttribute('aria-label', 'July 2100'); + calendarCells.forEach(cell => { + expect(cell).toHaveAttribute('aria-disabled', 'true'); + }); }); - const { calendarCells } = await openMenu(); - const firstCell = calendarCells?.[0]; - userEvent.click(firstCell!, {}, { skipPointerEventsCheck: true }); - expect(firstCell).toHaveAttribute('aria-disabled', 'true'); - expect(onDateChange).not.toHaveBeenCalled(); - }); - test('fires a validation handler', async () => { - const handleValidation = jest.fn(); - const { openMenu } = renderDatePicker({ - handleValidation, + test('initial focus (highlight) is on a chevron', async () => { + const testValue = new Date(Date.UTC(2100, Month.July, 10)); + const { openMenu } = renderDatePicker({ + value: testValue, + }); + const { leftChevron } = await openMenu(); + expect(leftChevron).toHaveFocus(); }); - const { calendarCells } = await openMenu(); - const firstCell = calendarCells?.[0]; - userEvent.click(firstCell!); - expect(handleValidation).toHaveBeenCalled(); }); + }); + describe('Clicking a Calendar cell', () => { test('closes the menu', async () => { const { openMenu } = renderDatePicker({}); const { calendarCells, menuContainerEl } = await openMenu(); @@ -593,19 +705,73 @@ describe('packages/date-picker', () => { await waitForElementToBeRemoved(menuContainerEl); }); - test('updates the input', async () => { - const { openMenu, dayInput, monthInput, yearInput } = - renderDatePicker({}); - const { todayCell } = await openMenu(); - userEvent.click(todayCell!); - await waitFor(() => { - expect(dayInput.value).toBe(testToday.getUTCDate().toString()); - expect(monthInput.value).toBe( - (testToday.getUTCMonth() + 1).toString(), - ); - expect(yearInput.value).toBe(testToday.getUTCFullYear().toString()); - }); - }); + describe.each(testTimeZones)( + 'when system time is in $tz', + ({ tz, UTCOffset }) => { + beforeEach(() => { + mockTimeZone(tz, UTCOffset); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('fires a change handler', async () => { + const onDateChange = jest.fn(); + const { openMenu } = renderDatePicker({ + onDateChange, + }); + const { calendarCells } = await openMenu(); + const firstCell = calendarCells?.[0]; + userEvent.click(firstCell!); + expect(onDateChange).toHaveBeenCalledWith( + newUTC(2023, Month.December, 1), + ); + }); + + test('fires a validation handler', async () => { + const handleValidation = jest.fn(); + const { openMenu } = renderDatePicker({ + handleValidation, + }); + const { calendarCells } = await openMenu(); + const firstCell = calendarCells?.[0]; + userEvent.click(firstCell!); + expect(handleValidation).toHaveBeenCalledWith( + newUTC(2023, Month.December, 1), + ); + }); + + test('updates the input', async () => { + const { openMenu, dayInput, monthInput, yearInput } = + renderDatePicker({}); + const { todayCell } = await openMenu(); + userEvent.click(todayCell!); + await waitFor(() => { + expect(dayInput.value).toBe(testToday.getUTCDate().toString()); + expect(monthInput.value).toBe( + (testToday.getUTCMonth() + 1).toString(), + ); + expect(yearInput.value).toBe( + testToday.getUTCFullYear().toString(), + ); + }); + }); + + test('does nothing if the cell is out-of-range', async () => { + const onDateChange = jest.fn(); + const { openMenu } = renderDatePicker({ + onDateChange, + value: new Date(Date.UTC(2023, Month.September, 15)), + min: new Date(Date.UTC(2023, Month.September, 10)), + }); + const { calendarCells } = await openMenu(); + const firstCell = calendarCells?.[0]; + expect(firstCell).toHaveAttribute('aria-disabled', 'true'); + userEvent.click(firstCell!, {}, { skipPointerEventsCheck: true }); + expect(onDateChange).not.toHaveBeenCalled(); + }); + }, + ); }); describe('Clicking a Chevron', () => { diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 7325c57271..54d326f470 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -62,6 +62,8 @@ export interface RenderMenuResult { todayCell: HTMLTableCellElement | null; /** Query for a cell with a given date value */ queryCellByDate: (date: Date) => HTMLTableCellElement | null; + /** Query for a cell with a given ISO date string */ + queryCellISODate: (isoString: string) => HTMLTableCellElement | null; } /** @@ -131,6 +133,14 @@ export const renderDatePicker = ( return cell as HTMLTableCellElement | null; }; + const queryCellISODate = ( + isoString: string, + ): HTMLTableCellElement | null => { + const cell = calendarGrid?.querySelector(`[data-iso="${isoString}"]`); + + return cell as HTMLTableCellElement | null; + }; + const todayCell = queryCellByDate(new Date(Date.now())); return { @@ -143,6 +153,7 @@ export const renderDatePicker = ( yearSelect: yearSelect as HTMLButtonElement | null, todayCell, queryCellByDate, + queryCellISODate, }; } diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index f4f881784c..8695d9cc0a 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -9,12 +9,15 @@ import { newUTC, setUTCDate, } from '@leafygreen-ui/date-utils'; +import { + mockTimeZone, + testTimeZones, +} from '@leafygreen-ui/date-utils/src/testing'; import { DatePickerProvider, DatePickerProviderProps, } from '../../shared/components/DatePickerContext'; -import { mockTimeZone, testTimeZones } from '../../shared/testutils'; import { SingleDateProvider, SingleDateProviderProps, @@ -231,7 +234,7 @@ describe('packages/date-picker/date-picker-menu', () => { jest.useFakeTimers(); }); afterEach(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); }); test('cell marked as `current` updates', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index a5875c0171..6a1cc4e2fd 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -5,7 +5,12 @@ import { userEvent, within } from '@storybook/testing-library'; import { last, omit } from 'lodash'; import MockDate from 'mockdate'; -import { Month, newUTC } from '@leafygreen-ui/date-utils'; +import { + testLocales, + Month, + newUTC, + testTimeZoneLabels, +} from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { type StoryMetaType } from '@leafygreen-ui/lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; @@ -16,11 +21,7 @@ import { type DatePickerContextProps, DatePickerProvider, } from '../../shared/components/DatePickerContext'; -import { - getProviderPropsFromStoryContext, - Locales, - TimeZones, -} from '../../shared/testutils'; +import { getProviderPropsFromStoryContext } from '../../shared/testutils'; import { type SingleDateContextProps, SingleDateProvider, @@ -64,8 +65,11 @@ const meta: StoryMetaType = { }, argTypes: { value: { control: 'date' }, - locale: { control: 'select', options: Locales }, - timeZone: { control: 'select', options: [undefined, ...TimeZones] }, + locale: { control: 'select', options: testLocales }, + timeZone: { + control: 'select', + options: [undefined, ...testTimeZoneLabels], + }, }, }; diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index 80225564db..5849aeba25 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -5,13 +5,14 @@ import { StoryFn } from '@storybook/react'; import { getISODate, isTodayTZ, + testLocales, Month, newUTC, + testTimeZoneLabels, } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; -import { Locales, TimeZones } from '../../../testutils'; import { DatePickerContextProps, DatePickerProvider, @@ -37,7 +38,7 @@ const meta: StoryMetaType = { generate: { combineArgs: { darkMode: [false, true], - locale: Locales, + locale: testLocales, }, decorator: ProviderWrapper, }, @@ -51,11 +52,11 @@ const meta: StoryMetaType = { darkMode: { control: 'boolean' }, locale: { control: 'select', - options: Locales, + options: testLocales, }, timeZone: { control: 'select', - options: TimeZones, + options: testTimeZoneLabels, }, }, }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 19ae1110bd..81c48d6f77 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -3,11 +3,11 @@ import React, { useEffect, useState } from 'react'; import { StoryFn } from '@storybook/react'; import { isValid } from 'date-fns'; -import { Month, newUTC } from '@leafygreen-ui/date-utils'; +import { testLocales, Month, newUTC } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { pickAndOmit, StoryMetaType, StoryType } from '@leafygreen-ui/lib'; -import { Locales, segmentRefsMock } from '../../../testutils'; +import { segmentRefsMock } from '../../../testutils'; import { contextPropNames, DatePickerContextProps, @@ -58,7 +58,7 @@ const meta: StoryMetaType = { }, argTypes: { value: { control: 'date' }, - locale: { control: 'select', options: Locales }, + locale: { control: 'select', options: testLocales }, }, }; diff --git a/packages/date-picker/src/shared/testutils/index.ts b/packages/date-picker/src/shared/testutils/index.ts index 53aacfe84e..aa8e085837 100644 --- a/packages/date-picker/src/shared/testutils/index.ts +++ b/packages/date-picker/src/shared/testutils/index.ts @@ -1,8 +1,14 @@ -export { eventContainingTargetValue } from './eventContainingTargetValue'; +import { createRef } from 'react'; + +import { SegmentRefs } from '../hooks'; + export { getProviderPropsFromStoryContext, type ProviderPropsObject, } from './getProviderPropsFromStoryContext'; -export { mockTimeZone } from './mockTimeZone'; -export { tabNTimes } from './tabNTimes'; -export * from './testValues'; + +export const segmentRefsMock: SegmentRefs = { + day: createRef(), + month: createRef(), + year: createRef(), +}; diff --git a/packages/date-picker/src/shared/testutils/mockTimeZone/index.ts b/packages/date-picker/src/shared/testutils/mockTimeZone/index.ts deleted file mode 100644 index 3fc1e1c6f8..0000000000 --- a/packages/date-picker/src/shared/testutils/mockTimeZone/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import timezoneMock, { TimeZone } from 'timezone-mock'; - -/** - * Mocks the `timeZone` returned from the `Intl.DateTimeFormat`, - * and the `getTimeZoneOffset` returned from `Date` - * @param timeZone IANA time zone string - * @param UTCOffset UTC offset (in hours) - */ -export const mockTimeZone = (timeZone: string, UTCOffset: number) => { - timezoneMock.register( - `Etc/GMT${UTCOffset >= 0 ? '+' : ''}${UTCOffset}` as TimeZone, - { - Date, - }, - ); - - const realDTFOptions = Intl.DateTimeFormat().resolvedOptions(); - - /** DTF.resolvedOptions */ - global.Intl.DateTimeFormat.prototype.resolvedOptions = jest - .fn() - .mockImplementation(() => ({ - ...realDTFOptions, - timeZone, - })); - - /** getTimezoneOffset */ - global.Date.prototype.getTimezoneOffset = jest - .fn() - .mockImplementation(() => UTCOffset * 60); - - /** getDate */ - global.Date.prototype.getDate = jest - .fn() - .mockImplementation(function getDate() { - /// @ts-expect-error - typeof `this` is unknown - const utcDate: number = (this as Date).getUTCDate(); - /// @ts-expect-error - typeof `this` is unknown - const utcHrs: number = (this as Date).getUTCHours(); - - const adjustedHr = utcHrs + UTCOffset; - const daysOffset = adjustedHr >= 24 ? 1 : adjustedHr < 0 ? -1 : 0; - return utcDate + daysOffset; - }); - - /** getHours */ - global.Date.prototype.getHours = jest - .fn() - .mockImplementation(function getHours() { - /// @ts-expect-error - typeof `this` is unknown - const utcHrs: number = (this as Date).getUTCHours(); - const adjustedHrs = utcHrs + UTCOffset; - const hours = - adjustedHrs >= 24 - ? adjustedHrs % 24 - : adjustedHrs < 0 - ? adjustedHrs + 24 - : adjustedHrs; - - return hours; - }); -}; diff --git a/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts b/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts deleted file mode 100644 index 27634baa53..0000000000 --- a/packages/date-picker/src/shared/testutils/mockTimeZone/mockTimeZone.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Month } from '@leafygreen-ui/date-utils'; - -import { testTimeZones } from '../testValues'; - -import { mockTimeZone } from '.'; - -describe('packages/date-picker/testutils/', () => { - describe('mockTimeZone', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - describe.each(testTimeZones)('$tz', ({ tz, UTCOffset }) => { - beforeEach(() => { - mockTimeZone(tz, UTCOffset); - }); - - test('midnight local', () => { - const mockToday = new Date( - Date.UTC(2020, Month.October, 14, -UTCOffset), - ); - jest.setSystemTime(mockToday); - - expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(tz); - expect(mockToday.getTimezoneOffset()).toBe(UTCOffset * 60); - - const now = new Date(Date.now()); - expect(now.getDate()).toBe(14); - expect(now.getHours()).toBe(0); - expect(now.getMinutes()).toBe(0); - }); - - test('midnight UTC', () => { - const mockToday = new Date(Date.UTC(2020, Month.December, 25, 0)); - jest.setSystemTime(mockToday); - - expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(tz); - expect(mockToday.getTimezoneOffset()).toBe(UTCOffset * 60); - - const now = new Date(Date.now()); - expect(now.getDate()).toBe(UTCOffset < 0 ? 24 : 25); - expect(now.getHours()).toBe(UTCOffset < 0 ? 24 + UTCOffset : UTCOffset); - expect(now.getMinutes()).toBe(0); - }); - }); - }); -}); diff --git a/packages/date-picker/tsconfig.json b/packages/date-picker/tsconfig.json index 8423fe8c40..9c4efec9d9 100644 --- a/packages/date-picker/tsconfig.json +++ b/packages/date-picker/tsconfig.json @@ -11,7 +11,7 @@ ], "@leafygreen-ui/*": [ "../*/src" - ] + ], } }, "include": [ @@ -61,6 +61,9 @@ { "path": "../select" }, + { + "path": "../testing-lib" + }, { "path": "../tokens" }, diff --git a/packages/date-utils/package.json b/packages/date-utils/package.json index 5bc1e19f71..ef531592c4 100644 --- a/packages/date-utils/package.json +++ b/packages/date-utils/package.json @@ -20,10 +20,10 @@ "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "lodash": "^4.17.21", + "timezone-mock": "^1.3.6", "weekstart": "^2.0.0" }, "devDependencies": { - "timezone-mock": "^1.3.6" }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/date-utils", "repository": { diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index c58f5210e8..933f0267d5 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -30,5 +30,8 @@ export { setToUTCMidnight } from './setToUTCMidnight'; export { setUTCDate } from './setUTCDate'; export { setUTCMonth } from './setUTCMonth'; export { setUTCYear } from './setUTCYear'; +// TODO: export /testing as a separate sub-directory +// i.e. `import {} from '@leafygreen-ui/date-utils/testing'` +export * from './testing'; export { toDate } from './toDate'; export * from './types'; diff --git a/packages/date-utils/src/testing/index.ts b/packages/date-utils/src/testing/index.ts new file mode 100644 index 0000000000..04aa3b6792 --- /dev/null +++ b/packages/date-utils/src/testing/index.ts @@ -0,0 +1,2 @@ +export { mockTimeZone } from './mockTimeZone'; +export * from './testValues'; diff --git a/packages/date-utils/src/testing/mockTimeZone/index.ts b/packages/date-utils/src/testing/mockTimeZone/index.ts new file mode 100644 index 0000000000..fb611d3bfc --- /dev/null +++ b/packages/date-utils/src/testing/mockTimeZone/index.ts @@ -0,0 +1,67 @@ +import timezoneMock, { type TimeZone } from 'timezone-mock'; + +/** + * Mocks the `timeZone` returned from the `Intl.DateTimeFormat`, + * and various `get*` methods on the `Date` prototype + * @param timeZone IANA time zone string + * @param UTCOffset UTC offset (in hours) + */ +export const mockTimeZone = (timeZone: string, UTCOffset: number) => { + timezoneMock.register( + `Etc/GMT${UTCOffset >= 0 ? '+' : ''}${UTCOffset}` as TimeZone, + { + Date, + }, + ); + + const DTFResolved = Intl.DateTimeFormat().resolvedOptions(); + + /** DTF.resolvedOptions */ + jest + .spyOn(global.Intl.DateTimeFormat.prototype, 'resolvedOptions') + .mockImplementation(() => ({ + ...DTFResolved, + timeZone, + })); + + /** getTimezoneOffset */ + jest + .spyOn(global.Date.prototype, 'getTimezoneOffset') + .mockImplementation(() => UTCOffset * 60); + + /** getDate */ + jest.spyOn(global.Date.prototype, 'getDate').mockImplementation(mockGetDate); + + /** getHours */ + jest + .spyOn(global.Date.prototype, 'getHours') + .mockImplementation(mockGetHours); + + /** + * MOCK FUNCTIONS + * */ + function mockGetDate() { + /// @ts-expect-error - typeof `this` is unknown + const utcDate: number = (this as Date).getUTCDate(); + /// @ts-expect-error - typeof `this` is unknown + const utcHrs: number = (this as Date).getUTCHours(); + + const adjustedHr = utcHrs + UTCOffset; + const daysOffset = adjustedHr >= 24 ? 1 : adjustedHr < 0 ? -1 : 0; + return utcDate + daysOffset; + } + + function mockGetHours() { + /// @ts-expect-error - typeof `this` is unknown + const utcHrs: number = (this as Date).getUTCHours(); + const adjustedHrs = utcHrs + UTCOffset; + const hours = + adjustedHrs >= 24 + ? adjustedHrs % 24 + : adjustedHrs < 0 + ? adjustedHrs + 24 + : adjustedHrs; + + return hours; + } +}; diff --git a/packages/date-utils/src/testing/mockTimeZone/mockTimeZone.spec.ts b/packages/date-utils/src/testing/mockTimeZone/mockTimeZone.spec.ts new file mode 100644 index 0000000000..5e9bfa3aa2 --- /dev/null +++ b/packages/date-utils/src/testing/mockTimeZone/mockTimeZone.spec.ts @@ -0,0 +1,64 @@ +import { Month } from '../../constants'; +import { testTimeZones } from '../testValues'; + +import { mockTimeZone } from '.'; + +describe('packages/date-picker/testutils/mockTimeZone', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe.each(testTimeZones)('$tz', ({ tz, UTCOffset }) => { + beforeEach(() => { + mockTimeZone(tz, UTCOffset); + }); + + test('midnight local', () => { + const mockToday = new Date(Date.UTC(2020, Month.October, 14, -UTCOffset)); + jest.setSystemTime(mockToday); + + expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(tz); + expect(mockToday.getTimezoneOffset()).toBe(UTCOffset * 60); + + const now = new Date(Date.now()); + expect(now.getDate()).toBe(14); + expect(now.getHours()).toBe(0); + expect(now.getMinutes()).toBe(0); + }); + + test('midnight UTC', () => { + const mockToday = new Date(Date.UTC(2020, Month.December, 25, 0)); + jest.setSystemTime(mockToday); + + expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBe(tz); + expect(mockToday.getTimezoneOffset()).toBe(UTCOffset * 60); + + const now = new Date(Date.now()); + expect(now.getDate()).toBe(UTCOffset < 0 ? 24 : 25); + expect(now.getHours()).toBe(UTCOffset < 0 ? 24 + UTCOffset : UTCOffset); + expect(now.getMinutes()).toBe(0); + }); + + test('maintains default `toLocaleString(...args)` behavior', () => { + const date = new Date(Date.UTC(2020, Month.December, 25, 0)); + expect(date.toLocaleString('en-US', { month: 'long' })).toEqual( + 'December', + ); + }); + }); + + test('restores mocks appropriately', () => { + jest.useRealTimers(); + + const now = new Date(Date.now()); + expect(Intl.DateTimeFormat().resolvedOptions().timeZone).toBeDefined(); + expect(now.getTimezoneOffset()).toBeDefined(); + + expect(now.getDate()).toBeDefined(); + expect(now.getHours()).toBeDefined(); + expect(now.getMinutes()).toBeDefined(); + }); +}); diff --git a/packages/date-picker/src/shared/testutils/testValues.ts b/packages/date-utils/src/testing/testValues.ts similarity index 75% rename from packages/date-picker/src/shared/testutils/testValues.ts rename to packages/date-utils/src/testing/testValues.ts index 1712df4ed2..9376f41404 100644 --- a/packages/date-picker/src/shared/testutils/testValues.ts +++ b/packages/date-utils/src/testing/testValues.ts @@ -1,13 +1,3 @@ -import { createRef } from 'react'; - -import { SegmentRefs } from '../hooks'; - -export const segmentRefsMock: SegmentRefs = { - day: createRef(), - month: createRef(), - year: createRef(), -}; - export const testTimeZones = [ { tz: 'Pacific/Honolulu', UTCOffset: -10 }, { tz: 'America/Los_Angeles', UTCOffset: -8 }, @@ -19,7 +9,7 @@ export const testTimeZones = [ ] as const; /** Time zones used to test with */ -export const TimeZones = testTimeZones.map(({ tz }) => tz); +export const testTimeZoneLabels = testTimeZones.map(({ tz }) => tz); /** Locales (date formats) to test with: * @@ -29,7 +19,7 @@ export const TimeZones = testTimeZones.map(({ tz }) => tz); * Farsi-Afghanistan (week starts on Sat) * English-Maldives (week starts on Fri.) */ -export const Locales = [ +export const testLocales = [ 'iso8601', 'de-DE', // German, Germany (uses `.` char separator) 'en-US', // English, US (week starts on Sun.) @@ -41,4 +31,4 @@ export const Locales = [ 'he-IL', // Hebrew, Israel 'ja-JP', // Japanese, Japan 'zh-CN', // Chinese, China -]; +] as const; diff --git a/packages/date-utils/tsconfig.json b/packages/date-utils/tsconfig.json index 72a0203d44..b5eed19ae2 100644 --- a/packages/date-utils/tsconfig.json +++ b/packages/date-utils/tsconfig.json @@ -6,18 +6,14 @@ "rootDir": "src", "baseUrl": ".", "paths": { - "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], "@leafygreen-ui/*": ["../*/src"] } }, "include": [ "src/**/*" ], - "exclude": ["**/*.spec.*", "**/*.story.*"], + "exclude": ["**/*.spec.*"], "references": [ - { - "path": "../emotion" - }, { "path": "../lib" } diff --git a/packages/testing-lib/package.json b/packages/testing-lib/package.json index 3f347d498a..e2568db3a4 100644 --- a/packages/testing-lib/package.json +++ b/packages/testing-lib/package.json @@ -12,6 +12,13 @@ ] } }, + "dependencies": { + "@testing-library/user-event": "13.5.0", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@lg-tools/test": "0.3.2" + }, "scripts": { "build": "lg build-package", "tsc": "lg build-ts", @@ -28,6 +35,5 @@ }, "bugs": { "url": "https://jira.mongodb.org/projects/PD/summary" - }, - "devDependencies": {} + } } diff --git a/packages/date-picker/src/shared/testutils/eventContainingTargetValue/index.ts b/packages/testing-lib/src/eventContainingTargetValue/index.ts similarity index 66% rename from packages/date-picker/src/shared/testutils/eventContainingTargetValue/index.ts rename to packages/testing-lib/src/eventContainingTargetValue/index.ts index cc66e1b9b8..06a86f7a7c 100644 --- a/packages/date-picker/src/shared/testutils/eventContainingTargetValue/index.ts +++ b/packages/testing-lib/src/eventContainingTargetValue/index.ts @@ -1,4 +1,6 @@ -/** Returns a jest object containing the expected target value */ +/** + * Returns a jest object containing the expected target value + */ export const eventContainingTargetValue = (value: any) => expect.objectContaining({ target: expect.objectContaining({ value }), diff --git a/packages/testing-lib/src/index.ts b/packages/testing-lib/src/index.ts index 1ff3db9b8a..dc9301d6b5 100644 --- a/packages/testing-lib/src/index.ts +++ b/packages/testing-lib/src/index.ts @@ -1,5 +1,7 @@ import * as Context from './context'; import * as jest from './jest'; import * as JestDOM from './jest-dom'; - export { Context, jest, JestDOM }; + +export { eventContainingTargetValue } from './eventContainingTargetValue'; +export { tabNTimes } from './tabNTimes'; diff --git a/packages/date-picker/src/shared/testutils/tabNTimes/index.ts b/packages/testing-lib/src/tabNTimes/index.ts similarity index 100% rename from packages/date-picker/src/shared/testutils/tabNTimes/index.ts rename to packages/testing-lib/src/tabNTimes/index.ts From 5dbcb490a94562047ede39beee4888f83b62151d Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:35:38 -0500 Subject: [PATCH 326/351] Date picker: DateUtils: Testing (#2133) * mv eventContainingTargetValue, tabNTimes * updates mockTimeZone to reset properly * mv testing utils * imports * Update tsconfig.json * rename testTimeZoneLabels, testLocales * properly restore mocks From c4d1b753798b97b08d3865952cc39e7b01ef92b7 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 13 Dec 2023 11:46:59 -0500 Subject: [PATCH 327/351] lint --- packages/date-picker/src/DatePicker.stories.tsx | 2 +- .../src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx | 2 +- .../components/Calendar/CalendarGrid/CalendarGrid.stories.tsx | 2 +- .../components/DateInput/DateInputBox/DateInputBox.stories.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 860c60f7ce..c44902ffd3 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -4,9 +4,9 @@ import { StoryFn } from '@storybook/react'; import Button from '@leafygreen-ui/button'; import { - testLocales, Month, newUTC, + testLocales, testTimeZoneLabels, } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 6a1cc4e2fd..290a5937e2 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -6,9 +6,9 @@ import { last, omit } from 'lodash'; import MockDate from 'mockdate'; import { - testLocales, Month, newUTC, + testLocales, testTimeZoneLabels, } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index 5849aeba25..681a40a31a 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -5,9 +5,9 @@ import { StoryFn } from '@storybook/react'; import { getISODate, isTodayTZ, - testLocales, Month, newUTC, + testLocales, testTimeZoneLabels, } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 81c48d6f77..374fa87039 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react'; import { StoryFn } from '@storybook/react'; import { isValid } from 'date-fns'; -import { testLocales, Month, newUTC } from '@leafygreen-ui/date-utils'; +import { Month, newUTC,testLocales } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { pickAndOmit, StoryMetaType, StoryType } from '@leafygreen-ui/lib'; From 6b53d1eda203dbc005cfbe1cb8d83eef8b355d88 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:14:04 -0500 Subject: [PATCH 328/351] Adam/date picker shared context (#2136) * mv shared context * rename SharedDatePickerContext * rename DatePickerContext * fixes --- .../date-picker/src/DatePicker.stories.tsx | 14 +-- .../date-picker/src/DatePicker/DatePicker.tsx | 14 +-- .../DatePickerContent/DatePickerContent.tsx | 8 +- .../DatePickerContent.types.ts | 2 +- .../DatePickerContext.tsx} | 26 ++-- .../DatePickerContext.types.ts} | 4 +- .../DatePickerContext/index.ts | 5 - .../useDatePickerComponentRefs.ts | 2 +- .../DatePickerInput/DatePickerInput.spec.tsx | 24 ++-- .../DatePickerInput.stories.tsx | 22 ++-- .../DatePickerInput/DatePickerInput.tsx | 8 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 32 ++--- .../DatePickerMenu/DatePickerMenu.stories.tsx | 32 ++--- .../DatePickerMenu/DatePickerMenu.tsx | 9 +- .../DatePickerMenuHeader.spec.tsx | 113 +++++++++--------- .../DatePickerMenuHeader.tsx | 8 +- .../src/DatePicker/SingleDateContext/index.ts | 9 -- .../utils/getSegmentToFocus/index.ts | 4 +- .../CalendarCell/CalendarCell.stories.tsx | 12 +- .../CalendarGrid/CalendarGrid.stories.tsx | 16 +-- .../Calendar/CalendarGrid/CalendarGrid.tsx | 4 +- .../DateFormField/DateFormField.stories.tsx | 18 +-- .../DateInput/DateFormField/DateFormField.tsx | 4 +- .../DateInputBox/DateInputBox.spec.tsx | 18 +-- .../DateInputBox/DateInputBox.stories.tsx | 18 +-- .../DateInput/DateInputBox/DateInputBox.tsx | 4 +- .../DateInputSegment.spec.tsx | 16 +-- .../DateInputSegment.stories.tsx | 17 +-- .../DateInputSegment/DateInputSegment.tsx | 4 +- .../src/shared/components/index.ts | 9 -- .../SharedDatePickerContext.spec.tsx} | 46 +++---- .../SharedDatePickerContext.tsx} | 36 +++--- .../SharedDatePickerContext.types.ts} | 8 +- .../SharedDatePickerContext.utils.ts} | 46 ++++--- .../date-picker/src/shared/context/index.ts | 14 +++ .../useDatePickerErrorNotifications.ts | 4 +- packages/date-picker/src/shared/index.ts | 1 + .../getProviderPropsFromStoryContext/index.ts | 8 +- .../utils/getFirstEmptySegment/index.ts | 4 +- .../shared/utils/getRelativeSegment/index.ts | 6 +- 40 files changed, 322 insertions(+), 327 deletions(-) rename packages/date-picker/src/DatePicker/{SingleDateContext/SingleDateContext.tsx => DatePickerContext/DatePickerContext.tsx} (90%) rename packages/date-picker/src/DatePicker/{SingleDateContext/SingleDateContext.types.ts => DatePickerContext/DatePickerContext.types.ts} (96%) rename packages/date-picker/src/{shared/components => DatePicker}/DatePickerContext/index.ts (64%) rename packages/date-picker/src/DatePicker/{SingleDateContext => DatePickerContext}/useDatePickerComponentRefs.ts (91%) delete mode 100644 packages/date-picker/src/DatePicker/SingleDateContext/index.ts rename packages/date-picker/src/shared/{components/DatePickerContext/DatePickerContext.spec.tsx => context/SharedDatePickerContext.spec.tsx} (75%) rename packages/date-picker/src/shared/{components/DatePickerContext/DatePickerContext.tsx => context/SharedDatePickerContext.tsx} (61%) rename packages/date-picker/src/shared/{components/DatePickerContext/DatePickerContext.types.ts => context/SharedDatePickerContext.types.ts} (85%) rename packages/date-picker/src/shared/{components/DatePickerContext/DatePickerContext.utils.ts => context/SharedDatePickerContext.utils.ts} (74%) create mode 100644 packages/date-picker/src/shared/context/index.ts rename packages/date-picker/src/shared/{components/DatePickerContext => context}/useDatePickerErrorNotifications.ts (95%) diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index c44902ffd3..44cae358b8 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -14,11 +14,11 @@ import { StoryMetaType } from '@leafygreen-ui/lib'; import Modal from '@leafygreen-ui/modal'; import { Size } from '@leafygreen-ui/tokens'; -import { - DatePickerContextProps, - DatePickerProvider, -} from './shared/components/DatePickerContext'; import { MAX_DATE, MIN_DATE } from './shared/constants'; +import { + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from './shared/context'; import { getProviderPropsFromStoryContext } from './shared/testutils/getProviderPropsFromStoryContext'; import { AutoComplete } from './shared/types'; import { DatePicker } from './DatePicker'; @@ -29,14 +29,14 @@ const ProviderWrapper = (Story: StoryFn, ctx: any) => { return ( - + - + ); }; -const meta: StoryMetaType = { +const meta: StoryMetaType = { title: 'Components/DatePicker/DatePicker', component: DatePicker, decorators: [ProviderWrapper], diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 812f86676b..335822b7c2 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -12,13 +12,13 @@ import { AutoComplete, DatePickerState } from '../shared'; import { ContextPropKeys, contextPropNames, - DatePickerProvider, -} from '../shared/components/DatePickerContext'; + SharedDatePickerProvider, +} from '../shared/context'; import { useControlledValue } from '../shared/hooks'; import { DatePickerProps } from './DatePicker.types'; import { DatePickerContent } from './DatePickerContent'; -import { SingleDateProvider } from './SingleDateContext'; +import { DatePickerProvider } from './DatePickerContext'; /** * LeafyGreen Date Picker component @@ -50,12 +50,12 @@ export const DatePicker = forwardRef( ); return ( - - ( > - - + + ); }, ); diff --git a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx index e0e8521c9c..2b2fd812ad 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx @@ -16,10 +16,10 @@ import { } from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; -import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { useSharedDatePickerContext } from '../../shared/context'; +import { useDatePickerContext } from '../DatePickerContext'; import { DatePickerInput } from '../DatePickerInput'; import { DatePickerMenu } from '../DatePickerMenu'; -import { useSingleDateContext } from '../SingleDateContext'; import { DatePickerContentProps } from './DatePickerContent.types'; @@ -28,7 +28,7 @@ export const DatePickerContent = forwardRef< DatePickerContentProps >(({ ...rest }: DatePickerContentProps, fwdRef) => { const { min, max, isOpen, menuId, disabled, isSelectOpen } = - useDatePickerContext(); + useSharedDatePickerContext(); const { refs, value, @@ -36,7 +36,7 @@ export const DatePickerContent = forwardRef< menuTriggerEvent, handleValidation, getHighlightedCell, - } = useSingleDateContext(); + } = useDatePickerContext(); const prevValue = usePrevious(value); const prevMin = usePrevious(min); diff --git a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.types.ts b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.types.ts index dac89f8d85..d90a68a86f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.types.ts @@ -1,4 +1,4 @@ -import { ContextPropKeys } from '../../shared/components/DatePickerContext'; +import { ContextPropKeys } from '../../shared/context'; import { DatePickerProps } from '../DatePicker.types'; /** diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx similarity index 90% rename from packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx rename to packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx index ffb0a0efe4..4b5e780237 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx @@ -18,29 +18,29 @@ import { } from '@leafygreen-ui/date-utils'; import { usePrevious } from '@leafygreen-ui/hooks'; -import { useDatePickerContext } from '../../shared/components'; +import { useSharedDatePickerContext } from '../../shared/context'; import { getFormattedDateString } from '../../shared/utils'; import { getInitialHighlight } from '../utils/getInitialHighlight'; import { - SingleDateContextProps, - SingleDateProviderProps, -} from './SingleDateContext.types'; + DatePickerContextProps, + DatePickerProviderProps, +} from './DatePickerContext.types'; import { useDateRangeComponentRefs } from './useDatePickerComponentRefs'; -export const SingleDateContext = createContext( - {} as SingleDateContextProps, +export const DatePickerContext = createContext( + {} as DatePickerContextProps, ); /** * A provider for context values in a single DatePicker */ -export const SingleDateProvider = ({ +export const DatePickerProvider = ({ children, value, setValue: _setValue, handleValidation: _handleValidation, -}: PropsWithChildren) => { +}: PropsWithChildren) => { const refs = useDateRangeComponentRefs(); const { isOpen, @@ -52,7 +52,7 @@ export const SingleDateProvider = ({ setInternalErrorMessage, clearInternalErrorMessage, isInRange, - } = useDatePickerContext(); + } = useSharedDatePickerContext(); const prevValue = usePrevious(value); const hour = new Date(Date.now()).getHours(); @@ -201,7 +201,7 @@ export const SingleDateProvider = ({ }, [prevValue, setMonth, today, value]); return ( - {children} - + ); }; /** * Access single date picker context values */ -export const useSingleDateContext = () => { - return useContext(SingleDateContext); +export const useDatePickerContext = () => { + return useContext(DatePickerContext); }; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts similarity index 96% rename from packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts rename to packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts index 5c6eb9c62a..ccc7b23171 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/SingleDateContext.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts @@ -12,7 +12,7 @@ export interface DatePickerComponentRefs { calendarButtonRef: React.RefObject; } -export interface SingleDateContextProps { +export interface DatePickerContextProps { /** * Ref objects for important date picker elements */ @@ -90,7 +90,7 @@ export interface SingleDateContextProps { } /** Props passed into the provider component */ -export interface SingleDateProviderProps { +export interface DatePickerProviderProps { value: DateType | undefined; setValue: (newVal: DateType) => void; handleValidation?: DatePickerProps['handleValidation']; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/index.ts b/packages/date-picker/src/DatePicker/DatePickerContext/index.ts similarity index 64% rename from packages/date-picker/src/shared/components/DatePickerContext/index.ts rename to packages/date-picker/src/DatePicker/DatePickerContext/index.ts index 08fe7b4bed..fe01ef1b34 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/index.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContext/index.ts @@ -7,8 +7,3 @@ export { type DatePickerContextProps, type DatePickerProviderProps, } from './DatePickerContext.types'; -export { - type ContextPropKeys, - contextPropNames, - defaultDatePickerContext, -} from './DatePickerContext.utils'; diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts b/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts similarity index 91% rename from packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts rename to packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts index 0e61b23268..ebbdf5ec6b 100644 --- a/packages/date-picker/src/DatePicker/SingleDateContext/useDatePickerComponentRefs.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts @@ -4,7 +4,7 @@ import { useDynamicRefs } from '@leafygreen-ui/hooks'; import { useSegmentRefs } from '../../shared/hooks'; -import { DatePickerComponentRefs } from './SingleDateContext.types'; +import { DatePickerComponentRefs } from './DatePickerContext.types'; /** Creates `ref` objects for any & all relevant component elements */ export const useDateRangeComponentRefs = (): DatePickerComponentRefs => { diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index 06f054266d..405e49f903 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -4,33 +4,33 @@ import userEvent from '@testing-library/user-event'; import { Month, newUTC } from '@leafygreen-ui/date-utils'; +import { + defaultSharedDatePickerContext, + SharedDatePickerProvider, + SharedDatePickerProviderProps, +} from '../../shared/context'; import { DatePickerProvider, DatePickerProviderProps, - defaultDatePickerContext, -} from '../../shared/components/DatePickerContext'; -import { - SingleDateProvider, - SingleDateProviderProps, -} from '../SingleDateContext'; +} from '../DatePickerContext'; import { DatePickerInput, DatePickerInputProps } from '.'; const renderDatePickerInput = ( props?: Omit | null, - singleDateContext?: Partial, - context?: Partial, + singleDateContext?: Partial, + context?: Partial, ) => { const result = render( - - + {}} {...singleDateContext} > - - , + + , ); const inputContainer = result.getByRole('combobox'); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index 0aabb2322e..40486dc334 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -7,15 +7,15 @@ import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; import { - DatePickerContextProps, - DatePickerProvider, -} from '../../shared/components/DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../shared/context'; import { getProviderPropsFromStoryContext } from '../../shared/testutils'; import { DatePickerProps } from '../DatePicker.types'; import { - SingleDateContextProps, - SingleDateProvider, -} from '../SingleDateContext'; + DatePickerContextProps, + DatePickerProvider, +} from '../DatePickerContext'; import { DatePickerInput } from './DatePickerInput'; @@ -25,18 +25,18 @@ const ProviderWrapper = (Story: StoryFn, ctx: any) => { return ( - - {}}> + + {}}> - - + + ); }; const meta: StoryMetaType< typeof DatePickerInput, - SingleDateContextProps & DatePickerContextProps + DatePickerContextProps & SharedDatePickerContextProps > = { title: 'Components/DatePicker/DatePicker/DatePickerInput', component: DatePickerInput, diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 4eb165e297..c676f3f3e9 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -12,12 +12,12 @@ import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; import { DateInputSegmentChangeEventHandler } from '../../shared/components/DateInput/DateInputSegment'; -import { useDatePickerContext } from '../../shared/components/DatePickerContext'; +import { useSharedDatePickerContext } from '../../shared/context'; import { getRelativeSegmentRef, isElementInputSegment, } from '../../shared/utils'; -import { useSingleDateContext } from '../SingleDateContext'; +import { useDatePickerContext } from '../DatePickerContext'; import { getSegmentToFocus } from '../utils/getSegmentToFocus'; import { DatePickerInputProps } from './DatePickerInput.types'; @@ -33,7 +33,7 @@ export const DatePickerInput = forwardRef( fwdRef, ) => { const { formatParts, disabled, isDirty, setIsDirty } = - useDatePickerContext(); + useSharedDatePickerContext(); const { refs: { segmentRefs, calendarButtonRef }, value, @@ -41,7 +41,7 @@ export const DatePickerInput = forwardRef( openMenu, toggleMenu, handleValidation, - } = useSingleDateContext(); + } = useDatePickerContext(); /** Called when the input's Date value has changed */ const handleInputValueChange = (inputVal?: Date | null) => { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 8695d9cc0a..9369faf3dc 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -14,14 +14,14 @@ import { testTimeZones, } from '@leafygreen-ui/date-utils/src/testing'; +import { + SharedDatePickerProvider, + SharedDatePickerProviderProps, +} from '../../shared/context'; import { DatePickerProvider, DatePickerProviderProps, -} from '../../shared/components/DatePickerContext'; -import { - SingleDateProvider, - SingleDateProviderProps, -} from '../SingleDateContext'; +} from '../DatePickerContext'; import { DatePickerMenu, DatePickerMenuProps } from '.'; @@ -35,29 +35,29 @@ const standardTimeStartDate = newUTC(2023, Month.November, 6); const renderDatePickerMenu = ( props?: Partial | null, - singleContext?: Partial | null, - context?: Partial | null, + singleContext?: Partial | null, + context?: Partial | null, ) => { const result = render( - - + {}} handleValidation={undefined} {...singleContext} > , - - , + + , ); const rerenderDatePickerMenu = ( newProps?: Partial | null, - newSingleContext?: Partial | null, + newSingleContext?: Partial | null, ) => result.rerender( - - + {}} handleValidation={undefined} @@ -67,8 +67,8 @@ const renderDatePickerMenu = ( )} /> - - , + + , ); const calendarGrid = result.getByRole('grid'); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 290a5937e2..2c73cc13fd 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -18,22 +18,22 @@ import { InlineCode } from '@leafygreen-ui/typography'; import { contextPropNames, - type DatePickerContextProps, - DatePickerProvider, -} from '../../shared/components/DatePickerContext'; + type SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../shared/context'; import { getProviderPropsFromStoryContext } from '../../shared/testutils'; import { - type SingleDateContextProps, - SingleDateProvider, -} from '../SingleDateContext'; + type DatePickerContextProps, + DatePickerProvider, +} from '../DatePickerContext'; import { DatePickerMenu } from './DatePickerMenu'; import { DatePickerMenuProps } from './DatePickerMenu.types'; const mockToday = newUTC(2023, Month.September, 14); type DecoratorArgs = DatePickerMenuProps & - SingleDateContextProps & - DatePickerContextProps; + DatePickerContextProps & + SharedDatePickerContextProps; const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = @@ -41,9 +41,9 @@ const MenuDecorator: Decorator = (Story: StoryFn, ctx: any) => { return ( - + - + ); }; @@ -85,12 +85,12 @@ export const Basic: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( - + Today: {new Date(Date.now()).toUTCString()} - + ); }, }; @@ -102,7 +102,7 @@ export const WithValue: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( - {}} > @@ -112,7 +112,7 @@ export const WithValue: DatePickerMenuStoryType = { - + ); }, }; @@ -126,12 +126,12 @@ export const MockedToday: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( - + Today: {new Date(Date.now()).toUTCString()} - + ); }, }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 56dd3a23f5..a661bb2813 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -24,9 +24,9 @@ import { CalendarCellState, CalendarGrid, } from '../../shared/components/Calendar'; -import { useDatePickerContext } from '../../shared/components/DatePickerContext'; import { MenuWrapper } from '../../shared/components/MenuWrapper'; -import { useSingleDateContext } from '../SingleDateContext'; +import { useSharedDatePickerContext } from '../../shared/context'; +import { useDatePickerContext } from '../DatePickerContext'; import { getNewHighlight } from './utils/getNewHighlight'; import { @@ -39,7 +39,8 @@ import { DatePickerMenuHeader } from './DatePickerMenuHeader'; export const DatePickerMenu = forwardRef( ({ onKeyDown, ...rest }: DatePickerMenuProps, fwdRef) => { - const { isInRange, isOpen, setIsDirty, timeZone } = useDatePickerContext(); + const { isInRange, isOpen, setIsDirty, timeZone } = + useSharedDatePickerContext(); const { refs, today, @@ -53,7 +54,7 @@ export const DatePickerMenu = forwardRef( setHighlight, getCellWithValue, getHighlightedCell, - } = useSingleDateContext(); + } = useDatePickerContext(); const ref = useForwardedRef(fwdRef, null); const cellRefs = refs.calendarCellRefs; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx index 2da2cd7e29..aecae30f3f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx @@ -5,41 +5,42 @@ import userEvent from '@testing-library/user-event'; import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { transitionDuration } from '@leafygreen-ui/tokens'; +import {} from '../../../shared/components'; import { - DatePickerContext, - defaultDatePickerContext, -} from '../../../shared/components'; + defaultSharedDatePickerContext, + SharedDatePickerContext, +} from '../../../shared/context'; import { - SingleDateContext, - SingleDateContextProps, -} from '../../SingleDateContext'; + DatePickerContext, + DatePickerContextProps, +} from '../../DatePickerContext'; import { DatePickerMenuHeader } from '.'; +const MockSharedDatePickerProvider = SharedDatePickerContext.Provider; const MockDatePickerProvider = DatePickerContext.Provider; -const MockSingleDateProvider = SingleDateContext.Provider; describe('packages/date-picker/menu/header', () => { describe('Rendering', () => { describe('Some month options are disabled', () => { test('When `month` and `min` are the same year, earlier month options are disabled', async () => { const { getByLabelText, findAllByRole } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -60,22 +61,22 @@ describe('packages/date-picker/menu/header', () => { test('When `month` and `max` are the same year, later month options are disabled', async () => { const { getByLabelText, findAllByRole } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -96,23 +97,23 @@ describe('packages/date-picker/menu/header', () => { test('When `month` and `max`/`min` are different years, no month options are disabled', async () => { const { getByLabelText, findAllByRole } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -131,23 +132,23 @@ describe('packages/date-picker/menu/header', () => { describe('When `year` is after `max`', () => { test('all options are disabled', async () => { const { getByLabelText, findAllByRole } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -165,23 +166,23 @@ describe('packages/date-picker/menu/header', () => { test('placeholder text renders the invalid month/year', async () => { const { getByLabelText } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -195,23 +196,23 @@ describe('packages/date-picker/menu/header', () => { describe('When `year` is before `min`', () => { test('all options are disabled', async () => { const { getByLabelText, findAllByRole } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -229,23 +230,23 @@ describe('packages/date-picker/menu/header', () => { test('placeholder text renders the invalid month/year', async () => { const { getByLabelText } = render( - - {}} /> - - , + + , ); const monthSelect = getByLabelText('Select month'); @@ -275,27 +276,27 @@ describe('packages/date-picker/menu/header', () => { }; return ( - - {children} - - + + ); }; - test('opening & closing a select menu calls `setIsSelectOpen` in DatePickerContext', async () => { + test('opening & closing a select menu calls `setIsSelectOpen` in SharedDatePickerContext', async () => { const { getByLabelText } = render( {}} /> diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx index 350126a56f..1c18abd80c 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.tsx @@ -12,9 +12,9 @@ import Icon from '@leafygreen-ui/icon'; import IconButton from '@leafygreen-ui/icon-button'; import { Option, Select } from '@leafygreen-ui/select'; -import { useDatePickerContext } from '../../../shared/components/DatePickerContext'; import { selectElementProps } from '../../../shared/constants'; -import { useSingleDateContext } from '../../SingleDateContext'; +import { useSharedDatePickerContext } from '../../../shared/context'; +import { useDatePickerContext } from '../../DatePickerContext'; import { menuHeaderSelectContainerStyles, menuHeaderStyles, @@ -38,8 +38,8 @@ export const DatePickerMenuHeader = forwardRef< DatePickerMenuHeaderProps >(({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { const { min, max, setIsSelectOpen, locale, isInRange } = - useDatePickerContext(); - const { month } = useSingleDateContext(); + useSharedDatePickerContext(); + const { month } = useDatePickerContext(); const monthOptions = getLocaleMonths(locale); const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); diff --git a/packages/date-picker/src/DatePicker/SingleDateContext/index.ts b/packages/date-picker/src/DatePicker/SingleDateContext/index.ts deleted file mode 100644 index 5428d9e13c..0000000000 --- a/packages/date-picker/src/DatePicker/SingleDateContext/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { - SingleDateContext, - SingleDateProvider, - useSingleDateContext, -} from './SingleDateContext'; -export { - type SingleDateContextProps, - type SingleDateProviderProps, -} from './SingleDateContext.types'; diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts index 064a1d6e2e..cb5efb645d 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts @@ -1,14 +1,14 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { DatePickerContextProps } from '../../../shared/components/DatePickerContext'; +import { SharedDatePickerContextProps } from '../../../shared/context'; import { SegmentRefs } from '../../../shared/hooks'; import { DateSegment } from '../../../shared/types'; import { getFirstEmptySegment } from '../../../shared/utils'; interface GetSegmentToFocusProps { target: EventTarget; - formatParts: DatePickerContextProps['formatParts']; + formatParts: SharedDatePickerContextProps['formatParts']; segmentRefs: SegmentRefs; } diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx index d2a094fe4d..c50a9c6d8b 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx @@ -5,9 +5,9 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../../context'; import { CalendarCell } from './CalendarCell'; import { @@ -15,7 +15,7 @@ import { CalendarCellState, } from './CalendarCell.types'; -const meta: StoryMetaType = { +const meta: StoryMetaType = { title: 'Components/DatePicker/Shared/CalendarCell', component: CalendarCell, parameters: { @@ -37,9 +37,9 @@ const meta: StoryMetaType = { return ( {/* @ts-expect-error - incomplete context value */} - + - + ); }, diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index 681a40a31a..14b3ef43ae 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -14,23 +14,23 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { - DatePickerContextProps, - DatePickerProvider, - useDatePickerContext, -} from '../../DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, + useSharedDatePickerContext, +} from '../../../context'; import { CalendarCell } from '../CalendarCell/CalendarCell'; import { CalendarGrid } from './CalendarGrid'; const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( - + - + ); -const meta: StoryMetaType = { +const meta: StoryMetaType = { title: 'Components/DatePicker/Shared/CalendarGrid', component: CalendarGrid, parameters: { @@ -64,7 +64,7 @@ const meta: StoryMetaType = { export default meta; export const Demo: StoryFn = ({ ...props }) => { - const { timeZone } = useDatePickerContext(); + const { timeZone } = useSharedDatePickerContext(); const [month] = useState(newUTC(2023, Month.August, 1)); const [hovered, setHovered] = useState(); diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx index ab4546a85a..33ef117b6c 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.tsx @@ -10,7 +10,7 @@ import { import { cx } from '@leafygreen-ui/emotion'; import { Disclaimer } from '@leafygreen-ui/typography'; -import { useDatePickerContext } from '../../DatePickerContext'; +import { useSharedDatePickerContext } from '../../../context'; import { calendarGridStyles, @@ -39,7 +39,7 @@ import { CalendarGridProps } from './CalendarGrid.types'; */ export const CalendarGrid = forwardRef( ({ month, children, className, ...rest }: CalendarGridProps, fwdRef) => { - const { locale } = useDatePickerContext(); + const { locale } = useSharedDatePickerContext(); const weekStartsOn = getWeekStartByLocale(locale); const weeks = useMemo( () => getWeeksArray(month, { locale }), diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx index 5f58e42c3c..d30c5ee969 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx @@ -6,17 +6,17 @@ import { css } from '@leafygreen-ui/emotion'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; -import { DatePickerState } from '../../../types'; import { - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../../context'; +import { DatePickerState } from '../../../types'; import { DateFormField } from './DateFormField'; const meta: StoryMetaType< typeof DateFormField, - Partial + Partial > = { title: 'Components/DatePicker/Shared/DateFormField', component: DateFormField, @@ -44,12 +44,12 @@ const meta: StoryMetaType< darkMode={ctx?.args.darkMode} baseFontSize={ctx?.args.baseFontSize} > - - + ), args: { @@ -83,7 +83,7 @@ export default meta; const Template: StoryFn = () => { return ( - = () => { placeholder="" /> - + ); }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx index 0d0f46dbd4..5c068baf2f 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { FormField, FormFieldInputContainer } from '@leafygreen-ui/form-field'; +import { useSharedDatePickerContext } from '../../../context'; import { DatePickerState } from '../../../types'; -import { useDatePickerContext } from '../../DatePickerContext'; import { CalendarButton } from '../CalendarButton'; import { iconButtonStyles } from './DateFormField.styles'; @@ -35,7 +35,7 @@ export const DateFormField = React.forwardRef< isOpen, menuId, size, - } = useDatePickerContext(); + } = useSharedDatePickerContext(); return ( , - context?: Partial, + context?: Partial, ) => { const result = render( - + - , + , ); const dayInput = result.container.querySelector( @@ -49,7 +49,7 @@ const renderDateInputBox = ( describe('packages/date-picker/shared/date-input-box', () => { const onSegmentChange = jest.fn(); - const testContext: Partial = { + const testContext: Partial = { locale: 'iso8601', timeZone: 'UTC', }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 374fa87039..b685a9fa1e 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -3,16 +3,16 @@ import React, { useEffect, useState } from 'react'; import { StoryFn } from '@storybook/react'; import { isValid } from 'date-fns'; -import { Month, newUTC,testLocales } from '@leafygreen-ui/date-utils'; +import { Month, newUTC, testLocales } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { pickAndOmit, StoryMetaType, StoryType } from '@leafygreen-ui/lib'; -import { segmentRefsMock } from '../../../testutils'; import { contextPropNames, - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../../context'; +import { segmentRefsMock } from '../../../testutils'; import { DateInputBox } from './DateInputBox'; @@ -26,14 +26,14 @@ const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => { return ( - + - + ); }; -const meta: StoryMetaType = { +const meta: StoryMetaType = { title: 'Components/DatePicker/Shared/DateInputBox', component: DateInputBox, decorators: [ProviderWrapper], @@ -92,7 +92,7 @@ export const Static: StoryFn = () => { export const Formats: StoryType< typeof DateInputBox, - DatePickerContextProps + SharedDatePickerContextProps > = () => <>; Formats.parameters = { generate: { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index 9107c261c3..41e2b24352 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -6,6 +6,7 @@ import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { keyMap } from '@leafygreen-ui/lib'; +import { useSharedDatePickerContext } from '../../../context'; import { useDateSegments } from '../../../hooks'; import { DateSegment, @@ -22,7 +23,6 @@ import { isExplicitSegmentValue, newDateFromSegments, } from '../../../utils'; -import { useDatePickerContext } from '../../DatePickerContext'; import { DateInputSegment } from '../DateInputSegment'; import { DateInputSegmentChangeEventHandler } from '../DateInputSegment/DateInputSegment.types'; @@ -59,7 +59,7 @@ export const DateInputBox = React.forwardRef( }: DateInputBoxProps, fwdRef, ) => { - const { formatParts, disabled, min, max } = useDatePickerContext(); + const { formatParts, disabled, min, max } = useSharedDatePickerContext(); const { theme } = useDarkMode(); const containerRef = useForwardedRef(fwdRef, null); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx index 3361020277..90c6cde9d2 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -4,20 +4,20 @@ import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { defaultMax, defaultMin } from '../../../constants'; +import { + defaultSharedDatePickerContext, + SharedDatePickerProvider, + SharedDatePickerProviderProps, +} from '../../../context'; import { DateSegment } from '../../../types'; import { getValueFormatter } from '../../../utils'; -import { - DatePickerProvider, - DatePickerProviderProps, - defaultDatePickerContext, -} from '../../DatePickerContext'; import { DateInputSegmentChangeEventHandler } from './DateInputSegment.types'; import { DateInputSegment, type DateInputSegmentProps } from '.'; const renderSegment = ( props?: Partial, - ctx?: Partial, + ctx?: Partial, ) => { const defaultProps = { value: '', @@ -26,9 +26,9 @@ const renderSegment = ( }; const result = render( - + - , + , ); const rerenderSegment = (newProps: Partial) => diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx index fdafa48bed..833b025d3b 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx @@ -5,15 +5,18 @@ import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; -import { DateSegmentValue } from '../../../types'; import { - DatePickerContextProps, - DatePickerProvider, -} from '../../DatePickerContext'; + SharedDatePickerContextProps, + SharedDatePickerProvider, +} from '../../../context'; +import { DateSegmentValue } from '../../../types'; import { DateInputSegment } from './DateInputSegment'; -const meta: StoryMetaType = { +const meta: StoryMetaType< + typeof DateInputSegment, + SharedDatePickerContextProps +> = { title: 'Components/DatePicker/Shared/DateInputSegment', component: DateInputSegment, parameters: { @@ -27,9 +30,9 @@ const meta: StoryMetaType = { }, decorator: (Instance, ctx) => ( // @ts-expect-error - incomplete context value - + - + ), excludeCombinations: [ { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index 010b49fd2a..a3ae38e55d 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -13,8 +13,8 @@ import { defaultMin, defaultPlaceholder, } from '../../../constants'; +import { useSharedDatePickerContext } from '../../../context'; import { getAutoComplete, getValueFormatter } from '../../../utils'; -import { useDatePickerContext } from '../../DatePickerContext'; import { calculateNewSegmentValue } from './calculateNewSegmentValue'; import { @@ -60,7 +60,7 @@ export const DateInputSegment = React.forwardRef< size, disabled, autoComplete: autoCompleteProp, - } = useDatePickerContext(); + } = useSharedDatePickerContext(); const formatter = getValueFormatter(segment); const autoComplete = getAutoComplete(autoCompleteProp, segment); const pattern = `[0-9]{${charsPerSegment[segment]}}`; diff --git a/packages/date-picker/src/shared/components/index.ts b/packages/date-picker/src/shared/components/index.ts index 9adeddaeb3..1b3d59912e 100644 --- a/packages/date-picker/src/shared/components/index.ts +++ b/packages/date-picker/src/shared/components/index.ts @@ -6,13 +6,4 @@ export { type CalendarGridProps, } from './Calendar'; export { DateInputBox, type DateInputBoxProps } from './DateInput'; -export { - contextPropNames, - DatePickerContext, - type DatePickerContextProps, - DatePickerProvider, - type DatePickerProviderProps, - defaultDatePickerContext, - useDatePickerContext, -} from './DatePickerContext'; export { MenuWrapper, type MenuWrapperProps } from './MenuWrapper'; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx b/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx similarity index 75% rename from packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx rename to packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx index af42ab597c..732d553314 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.spec.tsx +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx @@ -5,24 +5,26 @@ import { renderHook } from '@testing-library/react-hooks'; import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; -import { MAX_DATE, MIN_DATE } from '../../constants'; +import { MAX_DATE, MIN_DATE } from '../constants'; import { - DatePickerContextProps, - DatePickerProvider, - DatePickerProviderProps, - useDatePickerContext, + SharedDatePickerContextProps, + SharedDatePickerProvider, + SharedDatePickerProviderProps, + useSharedDatePickerContext, } from '.'; -const renderDatePickerProvider = (props?: Partial) => { +const renderSharedDatePickerProvider = ( + props?: Partial, +) => { const { result, rerender } = renderHook< PropsWithChildren<{}>, - DatePickerContextProps - >(useDatePickerContext, { + SharedDatePickerContextProps + >(useSharedDatePickerContext, { wrapper: ({ children }) => ( - + {children} - + ), }); @@ -30,13 +32,13 @@ const renderDatePickerProvider = (props?: Partial) => { }; describe('packages/date-picker-context', () => { - describe('useDatePickerContext', () => { + describe('useSharedDatePickerContext', () => { describe('min/max', () => { afterEach(() => { jest.resetAllMocks(); }); test('uses default min/max values when not provided', () => { - const { result } = renderDatePickerProvider(); + const { result } = renderSharedDatePickerProvider(); expect(result.current.min).toEqual(MIN_DATE); expect(result.current.max).toEqual(MAX_DATE); }); @@ -45,7 +47,7 @@ describe('packages/date-picker-context', () => { const testMin = newUTC(1999, Month.September, 2); const testMax = newUTC(2011, Month.June, 22); - const { result } = renderDatePickerProvider({ + const { result } = renderSharedDatePickerProvider({ min: testMin, max: testMax, }); @@ -59,7 +61,7 @@ describe('packages/date-picker-context', () => { const testMax = newUTC(1999, Month.September, 2); const testMin = newUTC(2011, Month.June, 22); - const { result } = renderDatePickerProvider({ + const { result } = renderSharedDatePickerProvider({ min: testMin, max: testMax, }); @@ -72,7 +74,7 @@ describe('packages/date-picker-context', () => { const errorSpy = jest.spyOn(consoleOnce, 'error'); const testMax = newUTC(1967, Month.March, 10); - const { result } = renderDatePickerProvider({ + const { result } = renderSharedDatePickerProvider({ max: testMax, }); expect(result.current.min).toEqual(MIN_DATE); @@ -84,7 +86,7 @@ describe('packages/date-picker-context', () => { const errorSpy = jest.spyOn(consoleOnce, 'error'); const testMin = newUTC(2067, Month.March, 10); - const { result } = renderDatePickerProvider({ + const { result } = renderSharedDatePickerProvider({ min: testMin, }); expect(result.current.min).toEqual(MIN_DATE); @@ -95,12 +97,12 @@ describe('packages/date-picker-context', () => { describe('isOpen', () => { test('is `false` by default', () => { - const { result } = renderDatePickerProvider(); + const { result } = renderSharedDatePickerProvider(); expect(result.current.isOpen).toBeFalsy(); }); test('setter updates the value to `true`', async () => { - const { result, rerender } = renderDatePickerProvider(); + const { result, rerender } = renderSharedDatePickerProvider(); act(() => result.current.setOpen(true)); rerender(); @@ -112,12 +114,12 @@ describe('packages/date-picker-context', () => { describe('isDirty', () => { test('is `false` by default', () => { - const { result } = renderDatePickerProvider(); + const { result } = renderSharedDatePickerProvider(); expect(result.current.isDirty).toBeFalsy(); }); test('setter updates the value to `true`', async () => { - const { result, rerender } = renderDatePickerProvider(); + const { result, rerender } = renderSharedDatePickerProvider(); act(() => result.current.setIsDirty(true)); rerender(); @@ -129,12 +131,12 @@ describe('packages/date-picker-context', () => { describe('isSelectOpen', () => { test('is `false` by default', () => { - const { result } = renderDatePickerProvider(); + const { result } = renderSharedDatePickerProvider(); expect(result.current.isSelectOpen).toBeFalsy(); }); test('setter updates the value to `true`', async () => { - const { result, rerender } = renderDatePickerProvider(); + const { result, rerender } = renderSharedDatePickerProvider(); act(() => result.current.setIsSelectOpen(true)); rerender(); diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx similarity index 61% rename from packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx rename to packages/date-picker/src/shared/context/SharedDatePickerContext.tsx index 18832e4711..ff75cde738 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx @@ -3,28 +3,27 @@ import { createContext, PropsWithChildren, useContext } from 'react'; import { useIdAllocator } from '@leafygreen-ui/hooks'; -import { AutoComplete } from '../../types'; +import { AutoComplete } from '../types'; import { - DatePickerContextProps, - DatePickerProviderProps, -} from './DatePickerContext.types'; + SharedDatePickerContextProps, + SharedDatePickerProviderProps, +} from './SharedDatePickerContext.types'; import { - defaultDatePickerContext, + defaultSharedDatePickerContext, getContextProps, -} from './DatePickerContext.utils'; +} from './SharedDatePickerContext.utils'; import { useDatePickerErrorNotifications } from './useDatePickerErrorNotifications'; -/** Create the DatePickerContext */ -export const DatePickerContext = createContext( - defaultDatePickerContext, -); +/** Create the SharedDatePickerContext */ +export const SharedDatePickerContext = + createContext(defaultSharedDatePickerContext); // TODO: Consider renaming this to `SharedDatePickerContext`, -// and use `DatePickerContext` for what's currently `SingleDateContext` +// and use `SharedDatePickerContext` for what's currently `DatePickerContext` -/** The Provider component for DatePickerContext */ -export const DatePickerProvider = ({ +/** The Provider component for SharedDatePickerContext */ +export const SharedDatePickerProvider = ({ children, initialOpen = false, disabled = false, @@ -32,7 +31,7 @@ export const DatePickerProvider = ({ state, autoComplete = AutoComplete.Off, ...rest -}: PropsWithChildren) => { +}: PropsWithChildren) => { const isInitiallyOpen = disabled ? false : initialOpen; const [isOpen, setOpen] = useState(isInitiallyOpen); @@ -49,7 +48,7 @@ export const DatePickerProvider = ({ } = useDatePickerErrorNotifications(state, errorMessage); return ( - {children} - + ); }; -/** A hook to access {@link DatePickerContextProps} */ -export const useDatePickerContext = () => useContext(DatePickerContext); +/** A hook to access {@link SharedDatePickerContextProps} */ +export const useSharedDatePickerContext = () => + useContext(SharedDatePickerContext); diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts similarity index 85% rename from packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts rename to packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts index 4d14cf30c8..f3bb94dac8 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.types.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts @@ -1,4 +1,4 @@ -import { BaseDatePickerProps, DatePickerState } from '../../types'; +import { BaseDatePickerProps, DatePickerState } from '../types'; import { UseDatePickerErrorNotificationsReturnObject } from './useDatePickerErrorNotifications'; @@ -8,13 +8,13 @@ export interface StateNotification { } /** The props expected to pass int the provider */ -export interface DatePickerProviderProps extends BaseDatePickerProps {} +export interface SharedDatePickerProviderProps extends BaseDatePickerProps {} /** * The values in context */ -export interface DatePickerContextProps - extends Omit, 'state'>, +export interface SharedDatePickerContextProps + extends Omit, 'state'>, UseDatePickerErrorNotificationsReturnObject { /** The earliest date accepted */ min: Date; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts similarity index 74% rename from packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts rename to packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts index cca2045ceb..871ab5cd65 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/DatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts @@ -6,24 +6,20 @@ import { getISODate, toDate } from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; -import { MAX_DATE, MIN_DATE } from '../../constants'; -import { - AutoComplete, - BaseDatePickerProps, - DatePickerState, -} from '../../types'; -import { getFormatParts } from '../../utils'; +import { MAX_DATE, MIN_DATE } from '../constants'; +import { AutoComplete, BaseDatePickerProps, DatePickerState } from '../types'; +import { getFormatParts } from '../utils'; import { - DatePickerContextProps, - DatePickerProviderProps, -} from './DatePickerContext.types'; + SharedDatePickerContextProps, + SharedDatePickerProviderProps, +} from './SharedDatePickerContext.types'; -export type ContextPropKeys = keyof DatePickerProviderProps & +export type ContextPropKeys = keyof SharedDatePickerProviderProps & keyof BaseDatePickerProps; /** - * Prop names that are in both DatePickerProps and DatePickerProviderProps + * Prop names that are in both DatePickerProps and SharedDatePickerProviderProps * */ export const contextPropNames: Array = [ 'label', @@ -42,7 +38,7 @@ export const contextPropNames: Array = [ ]; /** The default context value */ -export const defaultDatePickerContext: DatePickerContextProps = { +export const defaultSharedDatePickerContext: SharedDatePickerContextProps = { label: '', description: '', locale: 'iso8601', @@ -91,8 +87,8 @@ export const getIsInRange = * Returns a valid `Context` value given optional provider props */ export const getContextProps = ( - providerProps: DatePickerProviderProps, -): DatePickerContextProps => { + providerProps: SharedDatePickerProviderProps, +): SharedDatePickerContextProps => { const { min: minProp, max: maxProp, @@ -107,8 +103,8 @@ export const getContextProps = ( const [min, max] = getMinMax(toDate(minProp), toDate(maxProp)); - const providerValue: DatePickerContextProps = { - ...defaults(rest, defaultDatePickerContext), + const providerValue: SharedDatePickerContextProps = { + ...defaults(rest, defaultSharedDatePickerContext), timeZone, min, max, @@ -124,8 +120,8 @@ export const getContextProps = ( const getMinMax = (min: Date | null, max: Date | null): [Date, Date] => { const defaultRange: [Date, Date] = [ - defaultDatePickerContext.min, - defaultDatePickerContext.max, + defaultSharedDatePickerContext.min, + defaultSharedDatePickerContext.max, ]; // if both are defined @@ -143,31 +139,31 @@ const getMinMax = (min: Date | null, max: Date | null): [Date, Date] => { return [min, max]; } else if (min) { - if (isBefore(defaultDatePickerContext.max, min)) { + if (isBefore(defaultSharedDatePickerContext.max, min)) { consoleOnce.error( `LeafyGreen DatePicker: Provided min date (${getISODate( min, )}) is after the default max date (${getISODate( - defaultDatePickerContext.max, + defaultSharedDatePickerContext.max, )}). Using default values.`, ); return defaultRange; } - return [min, defaultDatePickerContext.max]; + return [min, defaultSharedDatePickerContext.max]; } else if (max) { - if (isBefore(max, defaultDatePickerContext.min)) { + if (isBefore(max, defaultSharedDatePickerContext.min)) { consoleOnce.error( `LeafyGreen DatePicker: Provided max date (${getISODate( max, )}) is before the default min date (${getISODate( - defaultDatePickerContext.min, + defaultSharedDatePickerContext.min, )}). Using default values.`, ); return defaultRange; } - return [defaultDatePickerContext.min, max]; + return [defaultSharedDatePickerContext.min, max]; } return defaultRange; diff --git a/packages/date-picker/src/shared/context/index.ts b/packages/date-picker/src/shared/context/index.ts new file mode 100644 index 0000000000..e05fe34680 --- /dev/null +++ b/packages/date-picker/src/shared/context/index.ts @@ -0,0 +1,14 @@ +export { + SharedDatePickerContext, + SharedDatePickerProvider, + useSharedDatePickerContext, +} from './SharedDatePickerContext'; +export { + type SharedDatePickerContextProps, + type SharedDatePickerProviderProps, +} from './SharedDatePickerContext.types'; +export { + type ContextPropKeys, + contextPropNames, + defaultSharedDatePickerContext, +} from './SharedDatePickerContext.utils'; diff --git a/packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts b/packages/date-picker/src/shared/context/useDatePickerErrorNotifications.ts similarity index 95% rename from packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts rename to packages/date-picker/src/shared/context/useDatePickerErrorNotifications.ts index 1b4dce2bdd..c38ef8f0ee 100644 --- a/packages/date-picker/src/shared/components/DatePickerContext/useDatePickerErrorNotifications.ts +++ b/packages/date-picker/src/shared/context/useDatePickerErrorNotifications.ts @@ -1,8 +1,8 @@ import { useMemo, useState } from 'react'; -import { DatePickerState } from '../../types'; +import { DatePickerState } from '../types'; -import { StateNotification } from './DatePickerContext.types'; +import { StateNotification } from './SharedDatePickerContext.types'; export interface UseDatePickerErrorNotificationsReturnObject { stateNotification: StateNotification; diff --git a/packages/date-picker/src/shared/index.ts b/packages/date-picker/src/shared/index.ts index e7f8eabcbf..5820ebd168 100644 --- a/packages/date-picker/src/shared/index.ts +++ b/packages/date-picker/src/shared/index.ts @@ -1,5 +1,6 @@ export * from './components'; export * from './constants'; +export * from './context'; export * from './hooks'; export * from './types'; export * from './utils'; diff --git a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts index 52fdcdfefc..4af8651ef3 100644 --- a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts +++ b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts @@ -6,18 +6,18 @@ import { pickAndOmit } from '@leafygreen-ui/lib'; import { ContextPropKeys, contextPropNames, - DatePickerProviderProps, -} from '../../components/DatePickerContext'; + SharedDatePickerProviderProps, +} from '../../context'; import { BaseDatePickerProps } from '../../types'; export interface ProviderPropsObject { leafyGreenProviderProps: LeafyGreenProviderProps; - datePickerProviderProps: DatePickerProviderProps; + datePickerProviderProps: SharedDatePickerProviderProps; storyProps: T; } export const getProviderPropsFromStoryContext =

( - ctx: StoryContext>, + ctx: StoryContext>, ): ProviderPropsObject>> => { const [ { darkMode, baseFontSize, ...datePickerProviderProps }, diff --git a/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts b/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts index 74196ed07d..93830e4748 100644 --- a/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts +++ b/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts @@ -1,4 +1,4 @@ -import { DatePickerContextProps } from '../../components/DatePickerContext'; +import { SharedDatePickerContextProps } from '../../context'; import { SegmentRefs } from '../../hooks'; import { DateSegment } from '../../types'; @@ -10,7 +10,7 @@ export const getFirstEmptySegment = ({ formatParts, segmentRefs, }: { - formatParts: Required['formatParts']; + formatParts: Required['formatParts']; segmentRefs: SegmentRefs; }) => { // if 1+ are empty, focus the first empty one diff --git a/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts b/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts index b52d90eb64..c298bddd5a 100644 --- a/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts +++ b/packages/date-picker/src/shared/utils/getRelativeSegment/index.ts @@ -1,14 +1,14 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { DatePickerContextProps } from '../../components/DatePickerContext'; +import { SharedDatePickerContextProps } from '../../context'; import { SegmentRefs } from '../../hooks'; import { DateSegment } from '../../types'; type RelativeDirection = 'next' | 'prev' | 'first' | 'last'; interface GetRelativeSegmentContext { segment: HTMLInputElement | React.RefObject; - formatParts: DatePickerContextProps['formatParts']; + formatParts: SharedDatePickerContextProps['formatParts']; segmentRefs: SegmentRefs; } @@ -23,7 +23,7 @@ export const getRelativeSegment = ( formatParts, }: { segment: DateSegment; - formatParts: DatePickerContextProps['formatParts']; + formatParts: SharedDatePickerContextProps['formatParts']; }, ): DateSegment | undefined => { if ( From 76ffbf321f54a3c348cbba9e887aa7d35fcafcf6 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Wed, 13 Dec 2023 14:28:44 -0500 Subject: [PATCH 329/351] Date Picker [LG-3886] aria labels (#2135) * add aria types * add type test * update DatePickerContextProps * add changeset * add more tests * remove aria-label * add a11y to dep * add aria types * add type test * update DatePickerContextProps * add changeset * add more tests * remove aria-label * add a11y to dep * a11y, not ally --- .changeset/stale-jeans-travel.md | 5 ++ packages/a11y/src/AriaLabelProps.ts | 6 +- packages/date-picker/package.json | 1 + .../src/DatePicker/DatePicker.spec.tsx | 82 +++++++++++++++++++ .../date-picker/src/DatePicker/DatePicker.tsx | 4 +- .../src/DatePicker/DatePicker.types.ts | 4 +- .../DateInput/DateFormField/DateFormField.tsx | 8 ++ .../context/SharedDatePickerContext.tsx | 12 +++ .../context/SharedDatePickerContext.types.ts | 27 +++++- .../context/SharedDatePickerContext.utils.ts | 4 + .../getProviderPropsFromStoryContext/index.ts | 2 + .../shared/types/BaseDatePickerProps.types.ts | 15 ++-- packages/date-picker/tsconfig.json | 3 + 13 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 .changeset/stale-jeans-travel.md diff --git a/.changeset/stale-jeans-travel.md b/.changeset/stale-jeans-travel.md new file mode 100644 index 0000000000..bbfeb0cf30 --- /dev/null +++ b/.changeset/stale-jeans-travel.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/a11y': patch +--- + +Update `AriaLabelProps` `label` type from `string` to `ReactNode` diff --git a/packages/a11y/src/AriaLabelProps.ts b/packages/a11y/src/AriaLabelProps.ts index d722630357..43ddb79243 100644 --- a/packages/a11y/src/AriaLabelProps.ts +++ b/packages/a11y/src/AriaLabelProps.ts @@ -1,3 +1,5 @@ +import { ReactNode } from 'react'; + /** * A union interface requiring _either_ `aria-label` or `aria-labelledby` */ @@ -43,7 +45,7 @@ export type AriaLabelPropsWithLabel = * * Optional if `aria-labelledby` or `aria-label` is provided */ - label?: string; + label?: ReactNode; } & AriaLabelProps) | ({ /** @@ -51,5 +53,5 @@ export type AriaLabelPropsWithLabel = * * Optional if `aria-labelledby` or `aria-label` is provided */ - label: string; + label: ReactNode; } & Partial); diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 629bc4f302..6675699eae 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -15,6 +15,7 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/a11y": "^1.4.11", "@leafygreen-ui/date-utils": "^0.1.0", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/form-field": "^0.2.0", diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index e27168489a..f7831ab455 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -88,6 +88,42 @@ describe('packages/date-picker', () => { expect(inputContainer).toBeInTheDocument(); }); + test('formField contains aria-label when a label is not provided', () => { + const { getByRole } = render( + , + ); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-label', 'Label'); + }); + + test('formField contains aria-labelledby when a label is not provided', () => { + const { getByRole } = render( + , + ); + const inputContainer = getByRole('combobox'); + expect(inputContainer).toHaveAttribute('aria-labelledby', 'Label'); + }); + + test('formField only contains a label if label, aria-label, and aria-labelledby are passes', () => { + const { getByRole, getByTestId } = render( + , + ); + const formField = getByTestId('lg-date-picker'); + const inputContainer = getByRole('combobox'); + expect(formField.querySelector('label')).toBeInTheDocument(); + expect(formField.querySelector('label')).toHaveTextContent('Label'); + expect(inputContainer).not.toHaveAttribute( + 'aria-labelledby', + 'AriaLabelledby', + ); + expect(inputContainer).not.toHaveAttribute('aria-label', 'AriaLabel'); + }); + test('renders 3 inputs', () => { const { dayInput, monthInput, yearInput } = renderDatePicker(); expect(dayInput).toBeInTheDocument(); @@ -113,6 +149,20 @@ describe('packages/date-picker', () => { expect(yearInput.value).toEqual('2023'); }); + describe('console', () => { + test('console warning when no labels are passed in', () => { + const consoleSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + /* @ts-expect-error - needs label/aria-label/aria-labelledby */ + render(); + expect(consoleSpy).toHaveBeenCalledWith( + 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', + ); + }); + }); + describe('Error states', () => { test('renders error state when `state` is "error"', () => { const { getByRole } = render( @@ -2683,4 +2733,36 @@ describe('packages/date-picker', () => { }); }); }); + + // eslint-disable-next-line jest/no-disabled-tests + test.skip('Types behave as expected', () => { + <> + {/* @ts-expect-error - needs label/aria-label/aria-labelledby */} + + + + + + {}} + initialValue={new Date()} + handleValidation={() => {}} + onChange={() => {}} + locale="iso8601" + timeZone="utc" + baseFontSize={13} + disabled={false} + size="default" + state="none" + errorMessage="?" + initialOpen={false} + autoComplete="off" + darkMode={false} + /> + ; + }); }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.tsx b/packages/date-picker/src/DatePicker/DatePicker.tsx index 335822b7c2..43da9e94b0 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.tsx @@ -86,8 +86,8 @@ DatePicker.propTypes = { description: PropTypes.node, locale: PropTypes.string, timeZone: PropTypes.string, - min: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), - max: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + min: PropTypes.instanceOf(Date), + max: PropTypes.instanceOf(Date), baseFontSize: PropTypes.oneOf(Object.values(BaseFontSize)), disabled: PropTypes.bool, size: PropTypes.oneOf(Object.values(Size)), diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index dbb839297a..3e5f8bf87d 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -4,7 +4,7 @@ import { DateType } from '@leafygreen-ui/date-utils'; import { BaseDatePickerProps } from '../shared/types'; -export interface DatePickerProps extends BaseDatePickerProps { +export type DatePickerProps = { /** * The selected date, given in UTC time */ @@ -35,4 +35,4 @@ export interface DatePickerProps extends BaseDatePickerProps { * Callback fired when any segment changes, (but not necessarily a full value) */ onChange?: (event: ChangeEvent) => void; -} +} & BaseDatePickerProps; diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx index 5c068baf2f..3f5aa93f07 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx @@ -35,6 +35,8 @@ export const DateFormField = React.forwardRef< isOpen, menuId, size, + ariaLabelProp, + ariaLabelledbyProp, } = useSharedDatePickerContext(); return ( @@ -51,6 +53,12 @@ export const DateFormField = React.forwardRef< ) => { const isInitiallyOpen = disabled ? false : initialOpen; @@ -47,6 +50,12 @@ export const SharedDatePickerProvider = ({ clearInternalErrorMessage, } = useDatePickerErrorNotifications(state, errorMessage); + if (!label && !ariaLabelledbyProp && !ariaLabelProp) { + console.warn( + 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', + ); + } + return ( {children} diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts index f3bb94dac8..7a1b8772de 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts @@ -1,3 +1,7 @@ +import { ReactNode } from 'react'; + +import { AriaLabelPropsWithLabel } from '@leafygreen-ui/a11y'; + import { BaseDatePickerProps, DatePickerState } from '../types'; import { UseDatePickerErrorNotificationsReturnObject } from './useDatePickerErrorNotifications'; @@ -6,15 +10,28 @@ export interface StateNotification { state: DatePickerState; message: string; } +type AriaLabelkeys = keyof AriaLabelPropsWithLabel; /** The props expected to pass int the provider */ -export interface SharedDatePickerProviderProps extends BaseDatePickerProps {} +export type SharedDatePickerProviderProps = Omit< + BaseDatePickerProps, + AriaLabelkeys +> & { + label?: ReactNode; + 'aria-label'?: string; + 'aria-labelledby'?: string; +}; + +type AriaLabelkeysWithoutLabel = Exclude; /** * The values in context */ export interface SharedDatePickerContextProps - extends Omit, 'state'>, + extends Omit< + Required, + 'state' | AriaLabelkeysWithoutLabel + >, UseDatePickerErrorNotificationsReturnObject { /** The earliest date accepted */ min: Date; @@ -57,4 +74,10 @@ export interface SharedDatePickerContextProps /** Setter for whether the select menus are open inside the menu */ setIsSelectOpen: React.Dispatch>; + + /** aria-label */ + ariaLabelProp: string; + + /** aria-labelledby */ + ariaLabelledbyProp: string; } diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts index 871ab5cd65..ac64623be3 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts @@ -22,6 +22,8 @@ export type ContextPropKeys = keyof SharedDatePickerProviderProps & * Prop names that are in both DatePickerProps and SharedDatePickerProviderProps * */ export const contextPropNames: Array = [ + 'aria-label', + 'aria-labelledby', 'label', 'description', 'locale', @@ -39,6 +41,8 @@ export const contextPropNames: Array = [ /** The default context value */ export const defaultSharedDatePickerContext: SharedDatePickerContextProps = { + ariaLabelProp: '', + ariaLabelledbyProp: '', label: '', description: '', locale: 'iso8601', diff --git a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts index 4af8651ef3..90cc2b7bff 100644 --- a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts +++ b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts @@ -31,6 +31,8 @@ export const getProviderPropsFromStoryContext =

( }, datePickerProviderProps: { label: '', + 'aria-label': '', + 'aria-labelledby': '', ...datePickerProviderProps, }, storyProps, diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts index f4e0b00a24..526d490abf 100644 --- a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -1,14 +1,10 @@ +import { AriaLabelPropsWithLabel } from '@leafygreen-ui/a11y'; import { LocaleString } from '@leafygreen-ui/date-utils'; import { DarkModeProps } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; import { AutoComplete, DatePickerState } from './types'; -export interface BaseDatePickerProps extends DarkModeProps { - /** - * A label for the input - */ - label: React.ReactNode; - +export type BaseDatePickerProps = { /** * A description for the date picker. * @@ -38,10 +34,10 @@ export interface BaseDatePickerProps extends DarkModeProps { timeZone?: string; /** The earliest date accepted */ - min?: string | Date; + min?: Date; /** The latest date accepted */ - max?: string | Date; + max?: Date; /** * The base font size of the input. Inherits from the nearest LeafyGreenProvider @@ -74,4 +70,5 @@ export interface BaseDatePickerProps extends DarkModeProps { * @default 'off' */ autoComplete?: AutoComplete; -} +} & DarkModeProps & + AriaLabelPropsWithLabel; diff --git a/packages/date-picker/tsconfig.json b/packages/date-picker/tsconfig.json index 9c4efec9d9..4c1564dce1 100644 --- a/packages/date-picker/tsconfig.json +++ b/packages/date-picker/tsconfig.json @@ -22,6 +22,9 @@ "**/*.story.*" ], "references": [ + { + "path": "../a11y" + }, { "path": "../button" }, From 63fcf6168c8d3a4b4b8da76329e0aba5717bbcaf Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:30:34 -0500 Subject: [PATCH 330/351] Date picker [LG-3888, LG-3890] timezone prop today (#2132) * adds timezone highlight tests * creates getSimulatedTZDate * Update isSameTZDay.ts * getISODateTZ isSameTZMonth * isSameTZMonth tests * comments * adds todos * Update SingleDateContext.tsx * typo * cleanup Content & Menu components * getFirstOfUTCMonth * fix getInitialHighlight * updates menu tests * more reorganizing tests * update month queries * Update DatePickerMenu.spec.tsx * LG 3890 chevron focus * fixes testValues * re-enable tests * docs --- .../src/DatePicker/DatePicker.spec.tsx | 293 +++++++----------- .../src/DatePicker/DatePicker.testutils.tsx | 6 +- .../src/DatePicker/DatePicker.types.ts | 27 +- .../DatePickerContent/DatePickerContent.tsx | 54 +--- .../DatePickerContext/DatePickerContext.tsx | 31 +- .../DatePickerContext.types.ts | 10 +- .../useDatePickerComponentRefs.ts | 8 + .../DatePickerMenu/DatePickerMenu.spec.tsx | 271 ++++++++-------- .../DatePickerMenu/DatePickerMenu.tsx | 134 ++++++-- .../DatePickerMenuHeader.spec.tsx | 16 +- .../DatePickerMenuHeader.tsx | 4 +- .../utils/getNewHighlight/index.ts | 4 +- .../getInitialHighlight.spec.ts | 25 +- .../utils/getInitialHighlight/index.ts | 15 +- .../shared/types/BaseDatePickerProps.types.ts | 34 +- .../date-utils/src/getFirstOfMonth/index.ts | 1 - .../getFirstOfUTCMonth.spec.ts} | 10 +- .../getFirstOfUTCMonth.ts} | 2 +- .../src/getFirstOfUTCMonth/index.ts | 1 + .../src/getISODateTZ/getISODateTZ.ts | 19 ++ packages/date-utils/src/getISODateTZ/index.ts | 1 + .../getSimulatedTZDate.spec.ts | 25 ++ .../getSimulatedTZDate/getSimulatedTZDate.ts | 24 ++ .../src/getSimulatedTZDate/index.ts | 1 + packages/date-utils/src/index.ts | 5 +- .../date-utils/src/isSameTZDay/isSameTZDay.ts | 23 +- .../date-utils/src/isSameTZMonth/index.ts | 1 + .../src/isSameTZMonth/isSameTZMonth.spec.ts | 19 ++ .../src/isSameTZMonth/isSameTZMonth.ts | 31 ++ packages/date-utils/src/testing/testValues.ts | 7 +- 30 files changed, 639 insertions(+), 463 deletions(-) delete mode 100644 packages/date-utils/src/getFirstOfMonth/index.ts rename packages/date-utils/src/{getFirstOfMonth/getFirstOfMonth.spec.ts => getFirstOfUTCMonth/getFirstOfUTCMonth.spec.ts} (59%) rename packages/date-utils/src/{getFirstOfMonth/getFirstOfMonth.ts => getFirstOfUTCMonth/getFirstOfUTCMonth.ts} (80%) create mode 100644 packages/date-utils/src/getFirstOfUTCMonth/index.ts create mode 100644 packages/date-utils/src/getISODateTZ/getISODateTZ.ts create mode 100644 packages/date-utils/src/getISODateTZ/index.ts create mode 100644 packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts create mode 100644 packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts create mode 100644 packages/date-utils/src/getSimulatedTZDate/index.ts create mode 100644 packages/date-utils/src/isSameTZMonth/index.ts create mode 100644 packages/date-utils/src/isSameTZMonth/isSameTZMonth.spec.ts create mode 100644 packages/date-utils/src/isSameTZMonth/isSameTZMonth.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index f7831ab455..001a822f90 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -19,6 +19,7 @@ import { import { mockTimeZone, testTimeZones, + undefinedTZ, } from '@leafygreen-ui/date-utils/src/testing'; import { eventContainingTargetValue, @@ -38,14 +39,16 @@ import { } from './DatePicker.testutils'; import { DatePicker } from '.'; -const testToday = newUTC(2023, Month.December, 26); +// Set the current time to noon UTC on 2023-12-25 +const testToday = newUTC(2023, Month.December, 25, 12); describe('packages/date-picker', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); beforeEach(() => { - // Set the current time to midnight UTC on 2023-12-26 - jest.useFakeTimers().setSystemTime(testToday); + jest.setSystemTime(testToday); }); - afterEach(() => { jest.restoreAllMocks(); }); @@ -133,18 +136,18 @@ describe('packages/date-picker', () => { test('renders `value` prop', () => { const { dayInput, monthInput, yearInput } = renderDatePicker({ - value: new Date(Date.now()), + value: newUTC(2023, Month.December, 25), }); - expect(dayInput.value).toEqual('26'); + expect(dayInput.value).toEqual('25'); expect(monthInput.value).toEqual('12'); expect(yearInput.value).toEqual('2023'); }); test('renders `initialValue` prop', () => { const { dayInput, monthInput, yearInput } = renderDatePicker({ - initialValue: new Date(Date.now()), + initialValue: newUTC(2023, Month.December, 25), }); - expect(dayInput.value).toEqual('26'); + expect(dayInput.value).toEqual('25'); expect(monthInput.value).toEqual('12'); expect(yearInput.value).toEqual('2023'); }); @@ -554,194 +557,118 @@ describe('packages/date-picker', () => { await waitFor(() => expect(menuContainerEl).not.toBeInTheDocument()); }); - describe('if no value is set', () => { - describe.each(testTimeZones)( - 'when system time is in $tz', - ({ tz, UTCOffset }) => { - const expectedISO = '2023-12-' + (UTCOffset < 0 ? '24' : '25'); - - beforeEach(() => { - mockTimeZone(tz, UTCOffset); - jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('menu opens to current month', async () => { - const { calendarButton, waitForMenuToOpen } = - renderDatePicker(); - userEvent.click(calendarButton); - const { calendarGrid, monthSelect, yearSelect } = - await waitForMenuToOpen(); - expect(calendarGrid).toHaveAttribute( - 'aria-label', - 'December 2023', - ); - expect(monthSelect).toHaveValue(Month.December.toString()); - expect(yearSelect).toHaveValue('2023'); - }); - - test('initial focus (highlight) is set to `today`', async () => { - const { calendarButton, waitForMenuToOpen } = - renderDatePicker(); - userEvent.click(calendarButton); - const { calendarCells } = await waitForMenuToOpen(); - const highlightCell = calendarCells.find( - cell => cell?.getAttribute('data-highlighted') == 'true', + describe.each([testTimeZones])( + 'when system time is in $tz', + ({ tz, UTCOffset }) => { + describe.each([undefinedTZ, ...testTimeZones])( + 'and timeZone prop is $tz', + props => { + const offset = props.UTCOffset ?? UTCOffset; + const dec24Local = newUTC( + 2023, + Month.December, + 24, + 23 - offset, + 59, ); - expect(highlightCell).toHaveAttribute('data-iso', expectedISO); - expect(highlightCell).toHaveFocus(); - }); - }, - ); - - describe.each(testTimeZones)( - 'when timeZone prop is $tz', - ({ tz, UTCOffset }) => { - const expectedISO = '2023-12-' + (UTCOffset < 0 ? '24' : '25'); + const dec24ISO = '2023-12-24'; - test('menu opens to current month', async () => { - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - timeZone: tz, + beforeEach(() => { + jest.setSystemTime(dec24Local); + mockTimeZone(tz, UTCOffset); }); - userEvent.click(calendarButton); - const { calendarGrid, monthSelect, yearSelect } = - await waitForMenuToOpen(); - expect(calendarGrid).toHaveAttribute( - 'aria-label', - 'December 2023', - ); - expect(monthSelect).toHaveValue(Month.December.toString()); - expect(yearSelect).toHaveValue('2023'); - }); - - test('initial highlight is set to `today`', async () => { - jest.setSystemTime(newUTC(2023, Month.December, 25, 16, 0)); - - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - timeZone: tz, + afterEach(() => { + jest.restoreAllMocks(); }); - userEvent.click(calendarButton); - - const { calendarCells } = await waitForMenuToOpen(); - const highlightCell = calendarCells.find( - cell => cell?.getAttribute('data-highlighted') == 'true', - ); - expect(highlightCell).toHaveAttribute('data-iso', expectedISO); - expect(highlightCell).toHaveFocus(); - }); - }, - ); - }); - describe('if a value is set', () => { - describe.each(testTimeZones)( - 'when system time is in $tz', - ({ tz, UTCOffset }) => { - beforeEach(() => { - jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC - mockTimeZone(tz, UTCOffset); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('menu opens to the month of that `value`', async () => { - const testValue = new Date(Date.UTC(2023, Month.March, 1)); - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - value: testValue, - }); - userEvent.click(calendarButton); - const { calendarGrid, monthSelect, yearSelect } = - await waitForMenuToOpen(); - expect(calendarGrid).toHaveAttribute( - 'aria-label', - 'March 2023', - ); - expect(monthSelect).toHaveValue(Month.March.toString()); - expect(yearSelect).toHaveValue('2023'); - }); + describe('if no value is set', () => { + test('default focus (highlight) is on `today`', async () => { + const { calendarButton, waitForMenuToOpen } = + renderDatePicker({ + timeZone: props.tz, + }); + userEvent.click(calendarButton); + const { queryCellByISODate } = await waitForMenuToOpen(); + expect(queryCellByISODate(dec24ISO)).toHaveFocus(); + }); - test('initial focus (highlight) is on the date of `value`', async () => { - const testValue = new Date(Date.UTC(2023, Month.March, 1)); - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - value: testValue, + test('menu opens to current month', async () => { + const { calendarButton, waitForMenuToOpen } = + renderDatePicker({ + timeZone: props.tz, + }); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'December 2023', + ); + expect(monthSelect).toHaveTextContent('Dec'); + expect(yearSelect).toHaveTextContent('2023'); + }); }); - userEvent.click(calendarButton); - const { calendarCells } = await waitForMenuToOpen(); - const highlightCell = calendarCells.find( - cell => cell?.getAttribute('data-highlighted') == 'true', - ); - expect(highlightCell).toHaveAttribute('data-iso', '2023-03-01'); - expect(highlightCell).toHaveFocus(); - }); - }, - ); - describe.each(testTimeZones)( - 'when timeZone prop is $tz', - ({ tz, UTCOffset }) => { - beforeEach(() => { - jest.setSystemTime(newUTC(2023, Month.December, 25, 0, 0)); // Midnight UTC - mockTimeZone(tz, UTCOffset); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - test('menu opens to the month of that `value`', async () => { - const testValue = new Date(Date.UTC(2023, Month.March, 1)); - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - value: testValue, - }); - userEvent.click(calendarButton); - const { calendarGrid, monthSelect, yearSelect } = - await waitForMenuToOpen(); - expect(calendarGrid).toHaveAttribute( - 'aria-label', - 'March 2023', - ); - expect(monthSelect).toHaveValue(Month.March.toString()); - expect(yearSelect).toHaveValue('2023'); - }); + describe('when `value` is set', () => { + test('focus (highlight) starts on current value', async () => { + const testValue = newUTC(2024, Month.September, 10); + const { calendarButton, waitForMenuToOpen } = + renderDatePicker({ + value: testValue, + timeZone: props.tz, + }); + userEvent.click(calendarButton); + const { queryCellByISODate } = await waitForMenuToOpen(); + expect(queryCellByISODate('2024-09-10')).toHaveFocus(); + }); - test('initial focus (highlight) is on the date of `value`', async () => { - const testValue = new Date(Date.UTC(2023, Month.March, 1)); - const { calendarButton, waitForMenuToOpen } = renderDatePicker({ - value: testValue, + test('menu opens to the month of that `value`', async () => { + const testValue = newUTC(2024, Month.September, 10); + const { calendarButton, waitForMenuToOpen } = + renderDatePicker({ + value: testValue, + timeZone: props.tz, + }); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'September 2024', + ); + expect(monthSelect).toHaveTextContent('Sep'); + expect(yearSelect).toHaveTextContent('2024'); + }); }); - userEvent.click(calendarButton); - const { calendarCells } = await waitForMenuToOpen(); - const highlightCell = calendarCells.find( - cell => cell?.getAttribute('data-highlighted') == 'true', - ); - expect(highlightCell).toHaveAttribute('data-iso', '2023-03-01'); - expect(highlightCell).toHaveFocus(); - }); - }, - ); - }); + }, + ); + }, + ); - describe('if value is invalid', () => { - test('menu still opens to the month of that value', async () => { - const testValue = new Date(Date.UTC(2100, Month.July, 10)); - const { openMenu } = renderDatePicker({ + describe('if `value` is not valid', () => { + test('focus (highlight) starts on chevron button', async () => { + const testValue = newUTC(2100, Month.July, 4); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ value: testValue, }); - const { calendarGrid, calendarCells } = await openMenu(); - expect(calendarGrid).toHaveAttribute('aria-label', 'July 2100'); - calendarCells.forEach(cell => { - expect(cell).toHaveAttribute('aria-disabled', 'true'); - }); + userEvent.click(calendarButton); + const { leftChevron } = await waitForMenuToOpen(); + expect(leftChevron).toHaveFocus(); }); - test('initial focus (highlight) is on a chevron', async () => { - const testValue = new Date(Date.UTC(2100, Month.July, 10)); - const { openMenu } = renderDatePicker({ + test('menu opens to the month of that `value`', async () => { + const testValue = newUTC(2100, Month.July, 4); + const { calendarButton, waitForMenuToOpen } = renderDatePicker({ value: testValue, }); - const { leftChevron } = await openMenu(); - expect(leftChevron).toHaveFocus(); + userEvent.click(calendarButton); + const { calendarGrid, monthSelect, yearSelect } = + await waitForMenuToOpen(); + + expect(calendarGrid).toHaveAttribute('aria-label', 'July 2100'); + expect(monthSelect).toHaveTextContent('Jul'); + expect(yearSelect).toHaveTextContent('2100'); }); }); }); @@ -1239,6 +1166,7 @@ describe('packages/date-picker', () => { userEvent.keyboard(`[Enter]`); expect(handleValidation).toHaveBeenCalledWith(undefined); }); + test.todo('within a form, does not submit form'); }); describe('Escape key', () => { @@ -1580,14 +1508,15 @@ describe('packages/date-picker', () => { describe('when a value is set', () => { test('fires value change handler', () => { const onDateChange = jest.fn(); + const testVal = newUTC(2023, Month.September, 10); const { monthInput } = renderDatePicker({ onDateChange, - value: testToday, + value: testVal, }); userEvent.click(monthInput); userEvent.keyboard(`{arrowdown}`); expect(onDateChange).toHaveBeenCalledWith( - setUTCMonth(testToday, testToday.getUTCMonth() - 1), + setUTCMonth(testVal, testVal.getUTCMonth() - 1), ); }); @@ -2700,7 +2629,7 @@ describe('packages/date-picker', () => { const firstCell = calendarCells?.[0]; userEvent.click(firstCell!); await waitFor(() => { - expect(dayInput.value).toEqual('26'); + expect(dayInput.value).toEqual('25'); expect(monthInput.value).toEqual('12'); expect(yearInput.value).toEqual('2023'); }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 54d326f470..91010cc604 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -63,7 +63,7 @@ export interface RenderMenuResult { /** Query for a cell with a given date value */ queryCellByDate: (date: Date) => HTMLTableCellElement | null; /** Query for a cell with a given ISO date string */ - queryCellISODate: (isoString: string) => HTMLTableCellElement | null; + queryCellByISODate: (isoString: string) => HTMLTableCellElement | null; } /** @@ -133,7 +133,7 @@ export const renderDatePicker = ( return cell as HTMLTableCellElement | null; }; - const queryCellISODate = ( + const queryCellByISODate = ( isoString: string, ): HTMLTableCellElement | null => { const cell = calendarGrid?.querySelector(`[data-iso="${isoString}"]`); @@ -153,7 +153,7 @@ export const renderDatePicker = ( yearSelect: yearSelect as HTMLButtonElement | null, todayCell, queryCellByDate, - queryCellISODate, + queryCellByISODate, }; } diff --git a/packages/date-picker/src/DatePicker/DatePicker.types.ts b/packages/date-picker/src/DatePicker/DatePicker.types.ts index 3e5f8bf87d..0378f399ee 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.types.ts +++ b/packages/date-picker/src/DatePicker/DatePicker.types.ts @@ -6,7 +6,17 @@ import { BaseDatePickerProps } from '../shared/types'; export type DatePickerProps = { /** - * The selected date, given in UTC time + * The selected date. + * + * Note that this Date object will be read as UTC time. + * Providing `Date.now()` could result in the incorrect date being displayed, + * depending on the system time zone. + * + * To set `value` to today, regardless of timeZone, use `setToUTCMidnight(new Date(Date.now()))`. + * + * e.g. `2023-12-31` at 20:00 in Los Angeles, will be `2024-01-01` at 04:00 in UTC. + * To set the correct day (`2023-12-31`) as the DatePicker value + * we must first convert our local timestamp to `2023-12-31` at midnight */ value?: DateType; @@ -16,18 +26,23 @@ export type DatePickerProps = { * * _Not_ fired when a date segment changes, but does not create a full date * - * Callback date argument will be a Date object in ISO-8601 format, and in UTC time. + * Callback date argument will be a Date object in UTC time, or `null` */ onDateChange?: (value?: DateType) => void; - /** The initial selected date. Ignored if `value` is provided */ + /** + * The initial selected date. Ignored if `value` is provided + * + * Note that this Date object will be read as UTC time. + * See `value` prop documentation for more details + */ initialValue?: DateType; /** - * A callback fired when validation should run, based on our [form validation guidelines](https://www.mongodb.design/foundation/forms/#form-validation-error-handling). - * Use this callback to compute the correct `state` value. + * A callback fired when validation should run, based on [form validation guidelines](https://www.mongodb.design/foundation/forms/#form-validation-error-handling). + * Use this callback to compute the correct `state` and `errorMessage` value. * - * Callback date argument will be a Date object in ISO 8601 format, and in UTC time. + * Callback date argument will be a Date object in UTC time, or `null` */ handleValidation?: (value?: DateType) => void; diff --git a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx index 2b2fd812ad..ae36cd5ecb 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContent/DatePickerContent.tsx @@ -1,11 +1,9 @@ import React, { forwardRef, KeyboardEventHandler, - TransitionEventHandler, useEffect, useRef, } from 'react'; -import { ExitHandler } from 'react-transition-group/Transition'; import isEqual from 'lodash/isEqual'; import { isSameUTCDay } from '@leafygreen-ui/date-utils'; @@ -29,14 +27,7 @@ export const DatePickerContent = forwardRef< >(({ ...rest }: DatePickerContentProps, fwdRef) => { const { min, max, isOpen, menuId, disabled, isSelectOpen } = useSharedDatePickerContext(); - const { - refs, - value, - closeMenu, - menuTriggerEvent, - handleValidation, - getHighlightedCell, - } = useDatePickerContext(); + const { value, closeMenu, handleValidation } = useDatePickerContext(); const prevValue = usePrevious(value); const prevMin = usePrevious(min); @@ -48,7 +39,9 @@ export const DatePickerContent = forwardRef< useBackdropClick(closeMenu, [formFieldRef, menuRef], isOpen && !isSelectOpen); - /** This listens to when the disabled prop changes to true and closes the menu */ + /** + * This listens to when the disabled prop changes to true and closes the menu + */ useEffect(() => { // if disabled is true but was previously false. This prevents this effect from rerunning multiple times since other states are updated when the menu closes. if (disabled && !prevDisabledValue) { @@ -57,34 +50,9 @@ export const DatePickerContent = forwardRef< } }, [closeMenu, disabled, handleValidation, value, prevDisabledValue]); - /** Fired when the CSS transition to open the menu is fired */ - const handleMenuTransitionEntered: TransitionEventHandler = e => { - // Whether this event is firing in response to the menu transition - const isTransitionedElementMenu = e.target === menuRef.current; - - // Whether the latest openMenu event was triggered by the calendar button - const isTriggeredByButton = - menuTriggerEvent && - refs.calendarButtonRef.current?.contains( - menuTriggerEvent.target as HTMLElement, - ); - - // Only move focus to input when opened via button click - if (isOpen && isTransitionedElementMenu && isTriggeredByButton) { - // When the menu opens, set focus to the `highlight` cell - const highlightedCell = getHighlightedCell(); - highlightedCell?.focus(); - } - }; - - /** Fired when the Transform element for the menu has exited */ - const handleMenuTransitionExited: ExitHandler = () => { - if (!isOpen) { - closeMenu(); - } - }; - - /** Handle key down events that should be fired regardless of target */ + /** + * Handle key down events that should be fired regardless of target. + */ const handleDatePickerKeyDown: KeyboardEventHandler = e => { const { key } = e; @@ -114,7 +82,7 @@ export const DatePickerContent = forwardRef< }; /** - * Side Effects + * SIDE EFFECTS */ /** When value changes, validate it */ @@ -124,9 +92,7 @@ export const DatePickerContent = forwardRef< } }, [handleValidation, prevValue, value]); - /** - * If min/max changes, re-validate the value - */ + /** If min/max changes, re-validate the value */ useEffect(() => { if ( (prevMin && !isSameUTCDay(min, prevMin)) || @@ -148,8 +114,6 @@ export const DatePickerContent = forwardRef< id={menuId} refEl={formFieldRef} onKeyDown={handleDatePickerKeyDown} - onTransitionEnd={handleMenuTransitionEntered} - onExited={handleMenuTransitionExited} /> ); diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx index 4b5e780237..24ccae1239 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx @@ -11,7 +11,7 @@ import React, { import { DateType, - getFirstOfMonth, + getFirstOfUTCMonth, getISODate, isOnOrBefore, isSameUTCDay, @@ -49,6 +49,7 @@ export const DatePickerProvider = ({ min, max, locale, + timeZone, setInternalErrorMessage, clearInternalErrorMessage, isInRange, @@ -67,13 +68,13 @@ export const DatePickerProvider = ({ /** * Keep track of the displayed month */ - const [month, _setMonth] = useState(getFirstOfMonth(value ?? today)); + const [month, _setMonth] = useState(getFirstOfUTCMonth(value ?? today)); /** * Keep track of the element the user is highlighting with the keyboard */ const [highlight, _setHighlight] = useState( - getInitialHighlight(value, today), + getInitialHighlight(value, today, timeZone), ); /*********** @@ -85,7 +86,7 @@ export const DatePickerProvider = ({ */ const setValue = (newVal?: DateType) => { _setValue(newVal ?? null); - setMonth(getFirstOfMonth(newVal ?? today)); + setMonth(getFirstOfUTCMonth(newVal ?? today)); }; /** @@ -150,9 +151,9 @@ export const DatePickerProvider = ({ refs.calendarButtonRef.current?.focus(); } // update month to something valid - setMonth(getFirstOfMonth(value ?? today)); + setMonth(getFirstOfUTCMonth(value ?? today)); // update highlight to something valid - setHighlight(getInitialHighlight(value, today)); + setHighlight(getInitialHighlight(value, today, timeZone)); }); }; @@ -173,17 +174,21 @@ export const DatePickerProvider = ({ * Returns the cell element with the provided value */ const getCellWithValue = (date: DateType): HTMLTableCellElement | null => { - const highlightKey = getISODate(date); - const cell = highlightKey - ? refs.calendarCellRefs(highlightKey)?.current - : null; - return cell; + if (isInRange(date)) { + const highlightKey = getISODate(date); + const cell = highlightKey + ? refs.calendarCellRefs(highlightKey)?.current + : null; + return cell; + } + + return null; }; /** * Returns the cell element with the current highlight value */ - const getHighlightedCell = () => { + const getHighlightedCell = (): HTMLTableCellElement | null => { return getCellWithValue(highlight); }; @@ -196,7 +201,7 @@ export const DatePickerProvider = ({ */ useEffect(() => { if (!isSameUTCDay(value, prevValue)) { - setMonth(getFirstOfMonth(value ?? today)); + setMonth(getFirstOfUTCMonth(value ?? today)); } }, [prevValue, setMonth, today, value]); diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts index ccc7b23171..dd09c847d2 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.types.ts @@ -10,6 +10,10 @@ export interface DatePickerComponentRefs { segmentRefs: SegmentRefs; calendarCellRefs: DynamicRefGetter; calendarButtonRef: React.RefObject; + chevronButtonRefs: { + left: React.RefObject; + right: React.RefObject; + }; } export interface DatePickerContextProps { @@ -34,12 +38,12 @@ export interface DatePickerContextProps { handleValidation: Required['handleValidation']; /** - * The current date, at UTC midnight + * The current date, in the browser's time zone */ today: Date; /** - * The currently displayed month in the menu + * The currently displayed month in the menu. */ month: Date; @@ -50,7 +54,7 @@ export interface DatePickerContextProps { setMonth: (newMonth: Date) => void; /** - * The Date value for the calendar cell in the menu that has, or should have focus + * The Date value for the calendar cell in the menu that has, or should have focus. */ highlight: DateType; diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts b/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts index ebbdf5ec6b..b7bc6f0bde 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts +++ b/packages/date-picker/src/DatePicker/DatePickerContext/useDatePickerComponentRefs.ts @@ -17,9 +17,17 @@ export const useDateRangeComponentRefs = (): DatePickerComponentRefs => { const calendarButtonRef = useRef(null); + const leftChevronRef = useRef(null); + const rightChevronRef = useRef(null); + const chevronButtonRefs = { + left: leftChevronRef, + right: rightChevronRef, + }; + return { segmentRefs, calendarCellRefs, calendarButtonRef, + chevronButtonRefs, }; }; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 9369faf3dc..0a672a8eb5 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -5,6 +5,7 @@ import { addDays } from 'date-fns'; import { getISODate, + getISODateTZ, Month, newUTC, setUTCDate, @@ -28,11 +29,6 @@ import { DatePickerMenu, DatePickerMenuProps } from '.'; const testToday = newUTC(2023, Month.September, 10); const testValue = newUTC(2023, Month.September, 14); -const standardTimeEndDate = newUTC(2023, Month.March, 11); -const daylightTimeStartDate = newUTC(2023, Month.March, 12); -const daylightTimeEndDate = newUTC(2023, Month.November, 5); -const standardTimeStartDate = newUTC(2023, Month.November, 6); - const renderDatePickerMenu = ( props?: Partial | null, singleContext?: Partial | null, @@ -78,7 +74,10 @@ const renderDatePickerMenu = ( ) as Array; const todayCell = calendarGrid.querySelector( - `[data-iso="${getISODate(testToday)}"]`, + `[data-iso="${getISODateTZ( + new Date(Date.now()), + context?.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone, + )}"]`, ); const getCellWithValue = (date: Date) => @@ -87,8 +86,17 @@ const renderDatePickerMenu = ( const getCellWithISOString = (isoStr: string) => calendarGrid.querySelector(`[data-iso="${isoStr}"]`); - const leftChevron = result?.queryByLabelText('Previous month'); - const rightChevron = result?.queryByLabelText('Next month'); + const getCurrentCell = () => + calendarGrid.querySelector('[aria-current="true"]'); + + const leftChevron = + result.queryByLabelText('Previous month') || + result.queryByLabelText('Previous valid month'); + const rightChevron = + result.queryByLabelText('Next month') || + result.queryByLabelText('Next valid month'); + const monthSelect = result.queryByLabelText('Select month'); + const yearSelect = result.queryByLabelText('Select year'); return { ...result, @@ -98,8 +106,11 @@ const renderDatePickerMenu = ( todayCell, getCellWithValue, getCellWithISOString, + getCurrentCell, leftChevron, rightChevron, + monthSelect, + yearSelect, }; }; @@ -152,25 +163,13 @@ describe('packages/date-picker/date-picker-menu', () => { ); }); - test('default highlight is on today', () => { - const { todayCell } = renderDatePickerMenu(); - userEvent.tab(); - expect(todayCell).toHaveFocus(); - }); - - test('highlight starts on current value when provided', () => { - const { getCellWithValue } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - const valueCell = getCellWithValue(testValue); - expect(valueCell).toHaveFocus(); - }); - describe('rendered cells', () => { test('have correct `aria-label`', () => { - const { todayCell } = renderDatePickerMenu(); - expect(todayCell).toHaveAttribute('aria-label', 'Sun Sep 10 2023'); + const { getCellWithISOString } = renderDatePickerMenu(); + expect(getCellWithISOString('2023-09-10')).toHaveAttribute( + 'aria-label', + 'Sun Sep 10 2023', + ); }); }); @@ -214,7 +213,7 @@ describe('packages/date-picker/date-picker-menu', () => { expect(isEveryCellDisabled).toBe(true); }); - test("doesn't not highlight a cell", () => { + test('does not highlight a cell', () => { const { calendarCells } = renderDatePickerMenu(null, { value: newUTC(2048, Month.December, 25), }); @@ -225,88 +224,68 @@ describe('packages/date-picker/date-picker-menu', () => { }); }); - describe('when system date changes', () => { - describe.each(testTimeZones)( - 'when system time zone is $tz', - ({ tz, UTCOffset }) => { + describe.each(testTimeZones)( + 'when system time is in $tz', + ({ tz, UTCOffset }) => { + describe.each([ + { tz: undefined, UTCOffset: undefined }, + ...testTimeZones, + ])('and timeZone prop is $tz', prop => { + const dec24Local = newUTC( + 2023, + Month.December, + 24, + 23 - (prop.UTCOffset ?? UTCOffset), + 59, + ); + const dec25Local = newUTC( + 2023, + Month.December, + 25, + 0 - (prop.UTCOffset ?? UTCOffset), + 0, + ); + const dec24ISO = '2023-12-24'; + const dec25ISO = '2023-12-25'; + const ctx = { + timeZone: prop?.tz, + }; + beforeEach(() => { + jest.setSystemTime(dec24Local); mockTimeZone(tz, UTCOffset); - jest.useFakeTimers(); }); afterEach(() => { jest.restoreAllMocks(); }); - test('cell marked as `current` updates', () => { - const dec24Local = newUTC( - 2023, - Month.December, - 24, - 23 - UTCOffset, - 59, - ); - - const dec25Local = newUTC( - 2023, - Month.December, - 25, - 0 - UTCOffset, - 0, - ); - - jest.setSystemTime(dec24Local); - + test('when date changes, cell marked as `current` updates', () => { const { getCellWithISOString, rerenderDatePickerMenu } = - renderDatePickerMenu(); - const dec24Cell = getCellWithISOString('2023-12-24'); + renderDatePickerMenu(null, null, ctx); + const dec24Cell = getCellWithISOString(dec24ISO); expect(dec24Cell).toHaveAttribute('aria-current', 'true'); jest.setSystemTime(dec25Local); rerenderDatePickerMenu(); - const dec25LocalCell = getCellWithISOString('2023-12-25'); + const dec25LocalCell = getCellWithISOString(dec25ISO); expect(dec25LocalCell).toHaveAttribute('aria-current', 'true'); }); - - describe.each(testTimeZones)( - 'and timeZone prop is $tz', - ({ tz: tzProp, UTCOffset: propOffset }) => { - test('cell marked as `current` updates', () => { - const dec24Local = newUTC( - 2023, - Month.December, - 24, - 23 - propOffset, - 59, - ); - - const dec25Local = newUTC( - 2023, - Month.December, - 25, - 0 - propOffset, - 0, - ); - - jest.setSystemTime(dec24Local); - const { getCellWithISOString, rerenderDatePickerMenu } = - renderDatePickerMenu(null, null, { timeZone: tzProp }); - const dec24Cell = getCellWithISOString('2023-12-24'); - expect(dec24Cell).toHaveAttribute('aria-current', 'true'); - jest.setSystemTime(dec25Local); - rerenderDatePickerMenu(); - const dec25Cell = getCellWithISOString('2023-12-25'); - expect(dec25Cell).toHaveAttribute('aria-current', 'true'); - }); - }, - ); - }, - ); - }); + }); + }, + ); }); describe('Keyboard navigation', () => { describe('Arrow Keys', () => { + beforeEach(() => { + jest.setSystemTime(testToday); + mockTimeZone('America/New_York', -5); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + test('left arrow moves focus to the previous day', async () => { const { getCellWithValue } = renderDatePickerMenu(null, { value: testValue, @@ -352,58 +331,66 @@ describe('packages/date-picker/date-picker-menu', () => { }); describe('when switching between daylight savings and standard time', () => { - // DT: Sun, Mar 12, 2023 – Sun, Nov 5, 2023 + // DST: Sun, Mar 12, 2023 – Sun, Nov 5, 2023 - describe('daylight time start (Mar 12 2023)', () => { - const weekBeforeDTStart = new Date(Date.UTC(2023, Month.March, 5)); + const standardTimeEndDate = newUTC(2023, Month.March, 11, 22); + const weekBeforeDTStart = newUTC(2023, Month.March, 5, 22); + const daylightTimeStartDate = newUTC(2023, Month.March, 12, 22); + const daylightTimeEndDate = newUTC(2023, Month.November, 5, 22); + const weekAfterDTEnd = newUTC(2023, Month.November, 12, 22); + const standardTimeStartDate = newUTC(2023, Month.November, 6, 22); + describe('DST start (Mar 12 2023)', () => { test('left arrow moves focus to prev day', async () => { jest.setSystemTime(daylightTimeStartDate); // Mar 12 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - const prevDayCell = getCellWithValue(standardTimeEndDate); // Mar 11 + const currentDayCell = getCellWithISOString('2023-03-12'); // Mar 12 + await waitFor(() => expect(currentDayCell).toHaveFocus()); + userEvent.keyboard('{arrowleft}'); + const prevDayCell = getCellWithISOString('2023-03-11'); // Mar 11 await waitFor(() => expect(prevDayCell).toHaveFocus()); }); test('right arrow moves focus to next day', async () => { jest.setSystemTime(standardTimeEndDate); // Mar 11 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); + const currentDayCell = getCellWithISOString('2023-03-11'); // Mar 11 + await waitFor(() => expect(currentDayCell).toHaveFocus()); + userEvent.keyboard('{arrowright}'); - const nextDayCell = getCellWithValue(daylightTimeStartDate); // Mar 12 + const nextDayCell = getCellWithISOString('2023-03-12'); // Mar 12 await waitFor(() => expect(nextDayCell).toHaveFocus()); }); test('up arrow moves focus to the previous week', async () => { jest.setSystemTime(daylightTimeStartDate); // Mar 12 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowup}'); - const prevWeekCell = getCellWithValue(weekBeforeDTStart); // Mar 5 + const prevWeekCell = getCellWithISOString('2023-03-05'); // Mar 5 await waitFor(() => expect(prevWeekCell).toHaveFocus()); }); test('down arrow moves focus to the next week', async () => { jest.setSystemTime(weekBeforeDTStart); // Mar 5 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowdown}'); - const nextWeekCell = getCellWithValue(daylightTimeStartDate); // Mar 12 + const nextWeekCell = getCellWithISOString('2023-03-12'); // Mar 12 await waitFor(() => expect(nextWeekCell).toHaveFocus()); }); }); - describe('daylight time end (Nov 5 2023)', () => { - const weekAfterDTEnd = new Date(Date.UTC(2023, Month.November, 12)); - + describe('DST end (Nov 5 2023)', () => { test('left arrow moves focus to prev day', async () => { jest.setSystemTime(standardTimeStartDate); // Nov 6 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowleft}'); - const prevDayCell = getCellWithValue(daylightTimeEndDate); // Nov 5 + const prevDayCell = getCellWithISOString('2023-11-05'); // Nov 5 await waitFor(() => expect(prevDayCell).toHaveFocus()); }); @@ -411,73 +398,99 @@ describe('packages/date-picker/date-picker-menu', () => { test('right arrow moves focus to next day', async () => { jest.setSystemTime(daylightTimeEndDate); // Nov 5 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowright}'); - const nextDayCell = getCellWithValue(standardTimeStartDate); // Nov 6 + const nextDayCell = getCellWithISOString('2023-11-06'); // Nov 6 await waitFor(() => expect(nextDayCell).toHaveFocus()); }); test('up arrow moves focus to the previous week', async () => { jest.setSystemTime(weekAfterDTEnd); // Nov 12 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowup}'); - const prevWeekCell = getCellWithValue(daylightTimeEndDate); // Nov 5 + const prevWeekCell = getCellWithISOString('2023-11-05'); // Nov 5 await waitFor(() => expect(prevWeekCell).toHaveFocus()); }); test('down arrow moves focus to the next week', async () => { jest.setSystemTime(daylightTimeEndDate); // Nov 5 - const { getCellWithValue } = renderDatePickerMenu(); + const { getCellWithISOString } = renderDatePickerMenu(); userEvent.tab(); userEvent.keyboard('{arrowdown}'); - const nextWeekCell = getCellWithValue(weekAfterDTEnd); // Nov 12 + const nextWeekCell = getCellWithISOString('2023-11-12'); // Nov 12 await waitFor(() => expect(nextWeekCell).toHaveFocus()); }); }); }); describe('when next day would be out of range', () => { + const testValue = newUTC(2023, Month.September, 10); + const isoString = '2023-09-10'; const singleCtx = { - value: testToday, + value: testValue, }; test('left arrow does nothing', async () => { - const { todayCell } = renderDatePickerMenu(null, singleCtx, { - min: testToday, - }); + const { getCellWithISOString } = renderDatePickerMenu( + null, + singleCtx, + { + min: testValue, + }, + ); userEvent.tab(); userEvent.keyboard('{arrowleft}'); - await waitFor(() => expect(todayCell).toHaveFocus()); + await waitFor(() => + expect(getCellWithISOString(isoString)).toHaveFocus(), + ); }); test('right arrow does nothing', async () => { - const { todayCell } = renderDatePickerMenu(null, singleCtx, { - max: testToday, - }); + const { getCellWithISOString } = renderDatePickerMenu( + null, + singleCtx, + { + max: testValue, + }, + ); userEvent.tab(); userEvent.keyboard('{arrowright}'); - await waitFor(() => expect(todayCell).toHaveFocus()); + await waitFor(() => + expect(getCellWithISOString(isoString)).toHaveFocus(), + ); }); test('up arrow does nothing', async () => { - const { todayCell } = renderDatePickerMenu(null, singleCtx, { - min: addDays(testToday, -6), - }); + const { getCellWithISOString } = renderDatePickerMenu( + null, + singleCtx, + { + min: addDays(testValue, -6), + }, + ); userEvent.tab(); userEvent.keyboard('{arrowup}'); - await waitFor(() => expect(todayCell).toHaveFocus()); + await waitFor(() => + expect(getCellWithISOString(isoString)).toHaveFocus(), + ); }); test('down arrow does nothing', async () => { - const { todayCell } = renderDatePickerMenu(null, singleCtx, { - max: addDays(testToday, 6), - }); + const { getCellWithISOString } = renderDatePickerMenu( + null, + singleCtx, + { + max: addDays(testValue, 6), + }, + ); userEvent.tab(); userEvent.keyboard('{arrowdown}'); - await waitFor(() => expect(todayCell).toHaveFocus()); + await waitFor(() => + expect(getCellWithISOString(isoString)).toHaveFocus(), + ); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index a661bb2813..744ed4a530 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -1,13 +1,15 @@ import React, { forwardRef, KeyboardEventHandler, + TransitionEventHandler, useEffect, useRef, } from 'react'; +import { ExitHandler } from 'react-transition-group/Transition'; import { addDaysUTC, - getFirstOfMonth, + getFirstOfUTCMonth, getFullMonthLabel, getISODate, getUTCDateString, @@ -28,6 +30,7 @@ import { MenuWrapper } from '../../shared/components/MenuWrapper'; import { useSharedDatePickerContext } from '../../shared/context'; import { useDatePickerContext } from '../DatePickerContext'; +import { shouldChevronBeDisabled } from './DatePickerMenuHeader/utils'; import { getNewHighlight } from './utils/getNewHighlight'; import { menuCalendarGridStyles, @@ -39,7 +42,7 @@ import { DatePickerMenuHeader } from './DatePickerMenuHeader'; export const DatePickerMenu = forwardRef( ({ onKeyDown, ...rest }: DatePickerMenuProps, fwdRef) => { - const { isInRange, isOpen, setIsDirty, timeZone } = + const { min, max, isInRange, isOpen, setIsDirty, timeZone } = useSharedDatePickerContext(); const { refs, @@ -54,6 +57,7 @@ export const DatePickerMenu = forwardRef( setHighlight, getCellWithValue, getHighlightedCell, + menuTriggerEvent, } = useDatePickerContext(); const ref = useForwardedRef(fwdRef, null); @@ -65,12 +69,22 @@ export const DatePickerMenu = forwardRef( const monthLabel = getFullMonthLabel(month); - /** Set the highlighted cell when the value changes in the input */ - useEffect(() => { - if (value && !isSameUTCDay(value, prevValue) && isInRange(value)) { - setHighlight(value); + /** Returns the current state of the cell */ + const getCellState = (cellDay: Date | null): CalendarCellState => { + if (isInRange(cellDay)) { + if (isSameUTCDay(cellDay, value)) { + return CalendarCellState.Active; + } + + return CalendarCellState.Default; } - }, [value, isInRange, setHighlight, prevValue]); + + return CalendarCellState.Disabled; + }; + + /** + * SETTERS + */ /** setDisplayMonth with side effects */ const updateMonth = (newMonth: Date) => { @@ -99,6 +113,17 @@ export const DatePickerMenu = forwardRef( }); }; + /** + * SIDE EFFECTS + */ + + /** Set the highlighted cell when the value changes in the input */ + useEffect(() => { + if (value && !isSameUTCDay(value, prevValue) && isInRange(value)) { + setHighlight(value); + } + }, [value, isInRange, setHighlight, prevValue]); + /** * If the new value is not the current month, update the month */ @@ -108,21 +133,52 @@ export const DatePickerMenu = forwardRef( !isSameUTCDay(value, prevValue) && !isSameUTCMonth(value, month) ) { - setDisplayMonth(getFirstOfMonth(value)); + setDisplayMonth(getFirstOfUTCMonth(value)); } }, [month, prevValue, setDisplayMonth, value]); - /** Returns the current state of the cell */ - const getCellState = (cellDay: Date | null): CalendarCellState => { - if (isInRange(cellDay)) { - if (isSameUTCDay(cellDay, value)) { - return CalendarCellState.Active; - } + /** + * EVENT HANDLERS + */ - return CalendarCellState.Default; + /** + * Fired when the CSS transition to open the menu is finished. + * Sets the initial focus on the highlighted cell + */ + const handleMenuTransitionEntered: TransitionEventHandler = e => { + // Whether this event is firing in response to the menu transition + const isTransitionedElementMenu = e.target === ref.current; + + // Whether the latest openMenu event was triggered by the calendar button + const isTriggeredByButton = + menuTriggerEvent && + refs.calendarButtonRef.current?.contains( + menuTriggerEvent.target as HTMLElement, + ); + + // Only move focus to input when opened via button click + if (isOpen && isTransitionedElementMenu && isTriggeredByButton) { + // When the menu opens, set focus to the `highlight` cell + const highlightedCell = getHighlightedCell(); + + if (highlightedCell) { + highlightedCell.focus(); + } else if (!shouldChevronBeDisabled('left', month, min)) { + refs.chevronButtonRefs.left.current?.focus(); + } else if (!shouldChevronBeDisabled('right', month, max)) { + refs.chevronButtonRefs.right.current?.focus(); + } } + }; - return CalendarCellState.Disabled; + /** + * Fired when the Transform element for the menu has exited. + * Fires side effects when the menu closes + */ + const handleMenuTransitionExited: ExitHandler = () => { + if (!isOpen) { + closeMenu(); + } }; /** Called when any calendar cell is clicked */ @@ -146,7 +202,10 @@ export const DatePickerMenu = forwardRef( } }; - // Focus trap + /** + * Fired on any key press. + * Handles custom focus trap logic + */ const handleMenuKeyPress: KeyboardEventHandler = e => { const { key } = e; @@ -177,7 +236,10 @@ export const DatePickerMenu = forwardRef( onKeyDown?.(e); }; - /** Called on any keydown within the CalendarGrid element */ + /** + * Called on any keydown within the CalendarGrid element. + * Responsible for updating the highlight +/- 1 day or 1 week + */ const handleCalendarKeyDown: KeyboardEventHandler = e => { const { key } = e; @@ -228,10 +290,12 @@ export const DatePickerMenu = forwardRef( role="listbox" active={isOpen} spacing={spacing[1]} + data-today={today.toISOString()} className={menuWrapperStyles} usePortal onKeyDown={handleMenuKeyPress} - data-today={today.toISOString()} + onTransitionEnd={handleMenuTransitionEntered} + onExited={handleMenuTransitionExited} {...rest} >

@@ -246,22 +310,26 @@ export const DatePickerMenu = forwardRef( month={month} className={menuCalendarGridStyles} onKeyDown={handleCalendarKeyDown} + // TODO: Test month label in different time zones aria-label={monthLabel} > - {(day, i) => ( - - {day.getUTCDate()} - - )} + {(day, i) => { + return ( + // TODO: Test highlight rendering in different time zones + + {day.getUTCDate()} + + ); + }}
diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx index aecae30f3f..7224921344 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenuHeader/DatePickerMenuHeader.spec.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useState } from 'react'; +import React, { createRef, PropsWithChildren, useState } from 'react'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -19,6 +19,12 @@ import { DatePickerMenuHeader } from '.'; const MockSharedDatePickerProvider = SharedDatePickerContext.Provider; const MockDatePickerProvider = DatePickerContext.Provider; +const mockRefs = { + chevronButtonRefs: { + left: createRef(), + right: createRef(), + }, +}; describe('packages/date-picker/menu/header', () => { describe('Rendering', () => { @@ -34,6 +40,7 @@ describe('packages/date-picker/menu/header', () => { { { { { { { { (({ setMonth, ...rest }: DatePickerMenuHeaderProps, fwdRef) => { const { min, max, setIsSelectOpen, locale, isInRange } = useSharedDatePickerContext(); - const { month } = useDatePickerContext(); + const { refs, month } = useDatePickerContext(); const monthOptions = getLocaleMonths(locale); const yearOptions = range(min.getUTCFullYear(), max.getUTCFullYear() + 1); @@ -111,6 +111,7 @@ export const DatePickerMenuHeader = forwardRef< return (
{ const value = newUTC(2023, Month.September, 10); - const today = newUTC(2023, Month.December, 26); + const today = newUTC(2023, Month.December, 25, 1); - test('returns `value` when provided', () => { - const highlight = getInitialHighlight(value, today); - expect(highlight).toBe(value); - }); + describe.each(testTimeZones)('for timeZone $tz', ({ tz, UTCOffset }) => { + test('returns `value` when provided', () => { + const highlight = getInitialHighlight(value, today, tz); + expect(highlight).toEqual(value); + }); - test('returns `today` if no value is provided', () => { - const highlight = getInitialHighlight(null, today); - expect(highlight).toBe(today); + test('returns `today` if no value is provided', () => { + const highlight = getInitialHighlight(null, today, tz); + const expectedDate = newUTC( + 2023, + Month.December, + UTCOffset < -1 ? 24 : 25, + 0, + ); + expect(highlight).toEqual(expectedDate); + }); }); }); diff --git a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts index 1f79e7d695..c98d697fb6 100644 --- a/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts +++ b/packages/date-picker/src/DatePicker/utils/getInitialHighlight/index.ts @@ -1,10 +1,21 @@ -import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; +import { + DateType, + getSimulatedTZDate, + isValidDate, + setToUTCMidnight, +} from '@leafygreen-ui/date-utils'; /** Returns the initial highlight value when the date picker is opened */ export const getInitialHighlight = ( value: DateType | undefined, today: Date, + timeZone: string, ) => { if (isValidDate(value)) return value; - return today; + + // return the UTC-midnight representation of the local `today` + // e.g. given `today` = "2023-12-24T12:00:00Z", and `timeZone` = 'Pacific/Auckland' (UTC+13) + // Locally, the date is `2023-12-25`, and so we should return that date + const simulatedToday = getSimulatedTZDate(today, timeZone); + return setToUTCMidnight(simulatedToday); }; diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts index 526d490abf..273cda29a9 100644 --- a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -8,13 +8,16 @@ export type BaseDatePickerProps = { /** * A description for the date picker. * - * It's recommended to set a meaningful time zone representation as the description (e.g. "Coordinated Universal Time") + * It's recommended to set a meaningful time zone representation as the description + * (e.g. "Coordinated Universal Time") */ description?: React.ReactNode; /** - * Sets the _presentation format_ for the displayed date. - * Fallback to the user’s browser preference (if supported), otherwise ISO-8601. + * Sets the _presentation format_ for the displayed date, + * and localizes month & weekday labels. + * Defaults to the user’s browser preference (if available), + * otherwise ISO-8601. * * Currently only the following values are officially supported: 'en-US' | 'en-GB' | 'iso8601' * Other valid [Locale](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale) @@ -25,18 +28,20 @@ export type BaseDatePickerProps = { locale?: LocaleString; /** - * A valid IANA timezone string, or UTC offset. - * Sets the _presentation time zone_ for the displayed date. - * Fallback to the user’s browser preference (if available), otherwise UTC. - * - * @default 'utc' + * A valid IANA timezone string, or UTC offset, + * used to calculate initial values. + * Defaults to the user’s browser settings. */ timeZone?: string; - /** The earliest date accepted */ + /** + * The earliest date accepted, in UTC + */ min?: Date; - /** The latest date accepted */ + /** + * The latest date accepted, in UTC + */ max?: Date; /** @@ -49,7 +54,9 @@ export type BaseDatePickerProps = { */ disabled?: boolean; - /** The size of the input */ + /** + * The size of the input + */ size?: Size; /** @@ -62,7 +69,10 @@ export type BaseDatePickerProps = { */ errorMessage?: string; - /** Whether the calendar menu is initially open. Note: The calendar menu will not open if disabled is set to true. */ + /** + * Whether the calendar menu is initially open. + * Note: The calendar menu will not open if disabled is set to `true`. + */ initialOpen?: boolean; /** diff --git a/packages/date-utils/src/getFirstOfMonth/index.ts b/packages/date-utils/src/getFirstOfMonth/index.ts deleted file mode 100644 index 9e8562233d..0000000000 --- a/packages/date-utils/src/getFirstOfMonth/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { getFirstOfMonth } from './getFirstOfMonth'; diff --git a/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts b/packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.spec.ts similarity index 59% rename from packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts rename to packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.spec.ts index d030d61bc4..e01ac5f1ef 100644 --- a/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.spec.ts +++ b/packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.spec.ts @@ -1,20 +1,20 @@ -import { getFirstOfMonth } from '.'; +import { getFirstOfUTCMonth } from '.'; -describe('packages/date-utils/getFirstOfMonth', () => { +describe('packages/date-utils/getFirstOfUTCMonth', () => { test('returns the first day of the provided month', () => { - expect(getFirstOfMonth(new Date(Date.UTC(2023, 0, 31)))).toEqual( + expect(getFirstOfUTCMonth(new Date(Date.UTC(2023, 0, 31)))).toEqual( new Date(Date.UTC(2023, 0, 1)), ); }); test('returns the same day if provided the 1st', () => { - expect(getFirstOfMonth(new Date(Date.UTC(2023, 11, 1)))).toEqual( + expect(getFirstOfUTCMonth(new Date(Date.UTC(2023, 11, 1)))).toEqual( new Date(Date.UTC(2023, 11, 1)), ); }); test('returned time is midnight', () => { - const first = getFirstOfMonth(new Date(Date.UTC(2023, 11, 1))); + const first = getFirstOfUTCMonth(new Date(Date.UTC(2023, 11, 1))); expect(first.getUTCHours()).toEqual(0); expect(first.getUTCMinutes()).toEqual(0); expect(first.getUTCSeconds()).toEqual(0); diff --git a/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.ts b/packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.ts similarity index 80% rename from packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.ts rename to packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.ts index 4d297b8a8e..de3a653efe 100644 --- a/packages/date-utils/src/getFirstOfMonth/getFirstOfMonth.ts +++ b/packages/date-utils/src/getFirstOfUTCMonth/getFirstOfUTCMonth.ts @@ -2,6 +2,6 @@ import { setToUTCMidnight } from '../setToUTCMidnight'; import { setUTCDate } from '../setUTCDate'; /** Returns midnight on the fist day of the provided month */ -export const getFirstOfMonth = (date: Date) => { +export const getFirstOfUTCMonth = (date: Date) => { return setToUTCMidnight(setUTCDate(date, 1)); }; diff --git a/packages/date-utils/src/getFirstOfUTCMonth/index.ts b/packages/date-utils/src/getFirstOfUTCMonth/index.ts new file mode 100644 index 0000000000..2a79d0cd48 --- /dev/null +++ b/packages/date-utils/src/getFirstOfUTCMonth/index.ts @@ -0,0 +1 @@ +export { getFirstOfUTCMonth } from './getFirstOfUTCMonth'; diff --git a/packages/date-utils/src/getISODateTZ/getISODateTZ.ts b/packages/date-utils/src/getISODateTZ/getISODateTZ.ts new file mode 100644 index 0000000000..754c63b5d7 --- /dev/null +++ b/packages/date-utils/src/getISODateTZ/getISODateTZ.ts @@ -0,0 +1,19 @@ +import { addMilliseconds } from 'date-fns'; +import { getTimezoneOffset } from 'date-fns-tz'; + +import { getISODate } from '../getISODate'; +import { DateType } from '../types'; + +export const getISODateTZ = (date: DateType, timeZone: string) => { + const offsetMs = getTimezoneOffset(timeZone); + + if (!date || isNaN(offsetMs)) return getISODate(date); + + // a date object that, when printed in ISO format, + // _looks like_ the local time for the given time zone. + // e.g. given date = "2023-12-26T01:00Z" and timeZone = "America/New_York", + // we get the date "2023-12-25T20:00Z" which _looks like_ the NYC local time (even though the date object technically incorrect) + const zonedInUtc = addMilliseconds(date, offsetMs); + + return getISODate(zonedInUtc); +}; diff --git a/packages/date-utils/src/getISODateTZ/index.ts b/packages/date-utils/src/getISODateTZ/index.ts new file mode 100644 index 0000000000..1cc45869a7 --- /dev/null +++ b/packages/date-utils/src/getISODateTZ/index.ts @@ -0,0 +1 @@ +export { getISODateTZ } from './getISODateTZ'; diff --git a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts new file mode 100644 index 0000000000..265d31de63 --- /dev/null +++ b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts @@ -0,0 +1,25 @@ +import { padStart } from 'lodash'; + +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; +import { testTimeZones } from '../testing/testValues'; + +import { getSimulatedTZDate } from './getSimulatedTZDate'; + +describe('packages/date-utils/getSimulatedTZDate', () => { + test.each(testTimeZones)('Simulates a date in $tz', ({ tz, UTCOffset }) => { + // 2023-07-04 + const testDate = newUTC(2023, Month.December, 25, 0, 0); + + const sim = getSimulatedTZDate(testDate, tz); + + const TZDay = UTCOffset < 0 ? '24' : '25'; + const TZHr = + UTCOffset < 0 + ? String(24 + UTCOffset) + : padStart(String(UTCOffset), 2, '0'); + + const expectedISOString = '2023-12-' + TZDay + 'T' + TZHr + ':00:00.000Z'; + expect(sim?.toISOString()).toEqual(expectedISOString); + }); +}); diff --git a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts new file mode 100644 index 0000000000..5673a10e7a --- /dev/null +++ b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts @@ -0,0 +1,24 @@ +import { addMilliseconds } from 'date-fns'; +import { getTimezoneOffset } from 'date-fns-tz'; + +import { isValidDate } from '../isValidDate'; + +/** + * Returns a date object that, _looks like_ the local time + * for the given time zone when printed in ISO format. + * + * e.g. given `date = "2023-12-25T01:00Z"` and `timeZone = "America/Los_Angeles"`, + * we get a date with the ISO date stamp `"2023-12-24T17:00Z"`, + * which _looks like_ the LA local time, + * (though the date object technically incorrect). + */ +export const getSimulatedTZDate = (date: Date, timeZone: string): Date => { + if (!isValidDate(date)) return date; + + // Milliseconds offset between the given time zone & UTC + const offsetMs = getTimezoneOffset(timeZone, date); + if (isNaN(offsetMs)) return date; + + const zonedInUtc = addMilliseconds(date, offsetMs); + return zonedInUtc; +}; diff --git a/packages/date-utils/src/getSimulatedTZDate/index.ts b/packages/date-utils/src/getSimulatedTZDate/index.ts new file mode 100644 index 0000000000..bb04d850ff --- /dev/null +++ b/packages/date-utils/src/getSimulatedTZDate/index.ts @@ -0,0 +1 @@ +export { getSimulatedTZDate } from './getSimulatedTZDate'; diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index 933f0267d5..e0ecea381a 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -2,14 +2,16 @@ export { addDaysUTC } from './addDaysUTC'; export { addMonthsUTC } from './addMonthsUTC'; export * from './constants'; export { getDaysInUTCMonth } from './getDaysInUTCMonth'; -export { getFirstOfMonth } from './getFirstOfMonth'; +export { getFirstOfUTCMonth } from './getFirstOfUTCMonth'; export { getFullMonthLabel } from './getFullMonthLabel'; export { getISODate } from './getISODate'; +export { getISODateTZ } from './getISODateTZ'; export { getLastOfMonth } from './getLastOfMonth'; export { getLocaleMonths } from './getLocaleMonths'; export { getLocaleWeekdays } from './getLocaleWeekdays'; export { getMonthIndex } from './getMonthIndex'; export { getMonthName } from './getMonthName'; +export { getSimulatedTZDate } from './getSimulatedTZDate'; export { getUTCDateString } from './getUTCDateString'; export { getWeekdayName } from './getWeekdayName'; export { getWeeksArray } from './getWeeksArray'; @@ -17,6 +19,7 @@ export { isCurrentUTCDay } from './isCurrentUTCDay'; export { isOnOrAfter } from './isOnOrAfter'; export { isOnOrBefore } from './isOnOrBefore'; export { isSameTZDay } from './isSameTZDay'; +export { isSameTZMonth } from './isSameTZMonth'; export { isSameUTCDay } from './isSameUTCDay'; export { isSameUTCMonth } from './isSameUTCMonth'; export { isSameUTCRange } from './isSameUTCRange'; diff --git a/packages/date-utils/src/isSameTZDay/isSameTZDay.ts b/packages/date-utils/src/isSameTZDay/isSameTZDay.ts index 995a2b1e9c..a26ff61c03 100644 --- a/packages/date-utils/src/isSameTZDay/isSameTZDay.ts +++ b/packages/date-utils/src/isSameTZDay/isSameTZDay.ts @@ -1,7 +1,7 @@ -import { addMilliseconds } from 'date-fns'; -import { getTimezoneOffset } from 'date-fns-tz'; - +import { getSimulatedTZDate } from '../getSimulatedTZDate'; import { isSameUTCDay } from '../isSameUTCDay'; +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; /** * @@ -23,23 +23,18 @@ import { isSameUTCDay } from '../isSameUTCDay'; * @param timeZone An IANA timeZone string */ export const isSameTZDay = ( - zoned: Date, - utc: Date, + zoned: DateType, + utc: DateType, timeZone: string, ): boolean => { - // Milliseconds offset between the given time zone & UTC - const offsetMs = getTimezoneOffset(timeZone, zoned); - if (isNaN(offsetMs)) return false; + if (!(isValidDate(zoned) && isValidDate(utc))) return false; - // a date object that, when printed in ISO format, - // _looks like_ the local time for the given time zone. - // e.g. given zoned = "2023-12-26T01:00Z" and timeZone = "America/New_York", - // we get the date "2023-12-25T20:00Z" which _looks like_ the NYC local time (even though the date object technically incorrect) - const zonedInUtc = addMilliseconds(zoned, offsetMs); + // Get a date object that _looks_ like the zoned date when rendered in ISO format + const simulatedTZDate = getSimulatedTZDate(zoned, timeZone); // We then use this date object to check if it's the same day as the provided utc date // e.g. is "2023-12-25T20:00Z" the same day as "2023-12-25T00:00Z"? - const isSameDay = isSameUTCDay(utc, zonedInUtc); + const isSameDay = isSameUTCDay(utc, simulatedTZDate); return isSameDay; }; diff --git a/packages/date-utils/src/isSameTZMonth/index.ts b/packages/date-utils/src/isSameTZMonth/index.ts new file mode 100644 index 0000000000..2b5c95c89f --- /dev/null +++ b/packages/date-utils/src/isSameTZMonth/index.ts @@ -0,0 +1 @@ +export { isSameTZMonth } from './isSameTZMonth'; diff --git a/packages/date-utils/src/isSameTZMonth/isSameTZMonth.spec.ts b/packages/date-utils/src/isSameTZMonth/isSameTZMonth.spec.ts new file mode 100644 index 0000000000..6659cd1b38 --- /dev/null +++ b/packages/date-utils/src/isSameTZMonth/isSameTZMonth.spec.ts @@ -0,0 +1,19 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; +import { testTimeZones } from '../testing/testValues'; + +import { isSameTZMonth } from './isSameTZMonth'; + +describe('packages/date-utils/isSameTZMonth', () => { + test.each(testTimeZones)('returns true in $tz', ({ tz, UTCOffset }) => { + if (UTCOffset <= 0) { + const testUTC = newUTC(2023, Month.November, 30); + const testZoned = newUTC(2023, Month.December, 1, -(UTCOffset + 1)); + expect(isSameTZMonth(testZoned, testUTC, tz)).toBe(true); + } else { + const testUTC = newUTC(2023, Month.December, 1); + const testZoned = newUTC(2023, Month.November, 30, 24 - UTCOffset); + expect(isSameTZMonth(testZoned, testUTC, tz)).toBe(true); + } + }); +}); diff --git a/packages/date-utils/src/isSameTZMonth/isSameTZMonth.ts b/packages/date-utils/src/isSameTZMonth/isSameTZMonth.ts new file mode 100644 index 0000000000..07224b9b24 --- /dev/null +++ b/packages/date-utils/src/isSameTZMonth/isSameTZMonth.ts @@ -0,0 +1,31 @@ +import { getSimulatedTZDate } from '../getSimulatedTZDate'; +import { isSameUTCMonth } from '../isSameUTCMonth'; +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; + +/** + * Returns whether the `zonedTime`, in a given `timeZone`, + * has the same date as the provided UTC date, + * + * * e.g. + * ```js + * isSameTZDay( + * Date("2023-11-01T01:00Z"), // Nov. 1 UCT, but 11:00 on Oct. 31 in NYC + * Date("2023-10-31T00:00Z"), // 00:00 on Oct 31 2023 in UTC + * "America/New_York" + * ) + * // Returns `true` + */ +export const isSameTZMonth = ( + zoned: DateType, + utc: DateType, + timeZone: string, +) => { + if (!(isValidDate(zoned) && isValidDate(utc))) return false; + + // Get a date object that _looks_ like the zoned date when rendered in ISO format + const simulatedTZDate = getSimulatedTZDate(zoned, timeZone); + + const isSameMonth = isSameUTCMonth(utc, simulatedTZDate); + return isSameMonth; +}; diff --git a/packages/date-utils/src/testing/testValues.ts b/packages/date-utils/src/testing/testValues.ts index 9376f41404..eb0b420c00 100644 --- a/packages/date-utils/src/testing/testValues.ts +++ b/packages/date-utils/src/testing/testValues.ts @@ -5,9 +5,14 @@ export const testTimeZones = [ { tz: 'Europe/London', UTCOffset: +0 }, { tz: 'Asia/Istanbul', UTCOffset: +3 }, { tz: 'Asia/Seoul', UTCOffset: +9 }, - { tz: 'Pacific/Auckland', UTCOffset: +13 }, + { tz: 'Pacific/Kiritimati', UTCOffset: +14 }, ] as const; +export const undefinedTZ = { + tz: undefined, + UTCOffset: undefined, +}; + /** Time zones used to test with */ export const testTimeZoneLabels = testTimeZones.map(({ tz }) => tz); From c9f7c74fdd78ef5d4153b7303e512d04f9abacd4 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 14 Dec 2023 15:46:32 -0500 Subject: [PATCH 331/351] Date picker [LG-3894] Local date strings (#2137) * docs * updates getUTCDateString implementation --- .../src/DatePicker/DatePicker.spec.tsx | 4 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 45 +++++++++---------- .../getSimulatedTZDate.spec.ts | 2 +- .../getSimulatedTZDate/getSimulatedTZDate.ts | 2 +- .../getSimulatedUTCDate.spec.ts | 16 +++++++ .../getSimulatedUTCDate.ts | 32 +++++++++++++ .../src/getSimulatedUTCDate/index.ts | 1 + .../getUTCDateString/getUTCDateString.spec.ts | 12 +++-- .../src/getUTCDateString/getUTCDateString.ts | 41 ++++++++++++++--- packages/date-utils/src/index.ts | 1 + packages/date-utils/src/types.ts | 2 + 11 files changed, 117 insertions(+), 41 deletions(-) create mode 100644 packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.spec.ts create mode 100644 packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.ts create mode 100644 packages/date-utils/src/getSimulatedUTCDate/index.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 001a822f90..58d508efd6 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1244,7 +1244,9 @@ describe('packages/date-picker', () => { /** * Arrow Keys: * Since arrow key behavior changes based on whether the input or menu is focused, - * many tests exist in the "DatePickerInput" and "DatePickerMenu" components + * more detailed tests suites are located in + * - DatePickerInput: (./DatePickerInput/DatePickerInput.spec.tsx) and + * - DatePickerMenu: (./DatePickerMenu/DatePickerMenu.spec.tsx) */ describe('Arrow key', () => { describe('Input', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 0a672a8eb5..08d046410a 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -163,16 +163,6 @@ describe('packages/date-picker/date-picker-menu', () => { ); }); - describe('rendered cells', () => { - test('have correct `aria-label`', () => { - const { getCellWithISOString } = renderDatePickerMenu(); - expect(getCellWithISOString('2023-09-10')).toHaveAttribute( - 'aria-label', - 'Sun Sep 10 2023', - ); - }); - }); - describe('when value is updated', () => { test('grid is labelled as the current month', () => { const { getByRole, rerenderDatePickerMenu } = renderDatePickerMenu(); @@ -224,6 +214,23 @@ describe('packages/date-picker/date-picker-menu', () => { }); }); + // TODO: Test in multiple time zones with a properly mocked Date object + describe('rendered cells', () => { + test('have correct text content and `aria-label`', () => { + const { calendarCells } = renderDatePickerMenu(); + + calendarCells.forEach((cell, i) => { + const date = String(i + 1); + expect(cell).toHaveTextContent(date); + + expect(cell).toHaveAttribute( + 'aria-label', + expect.stringContaining(`September ${date}, 2023`), + ); + }); + }); + }); + describe.each(testTimeZones)( 'when system time is in $tz', ({ tz, UTCOffset }) => { @@ -231,20 +238,10 @@ describe('packages/date-picker/date-picker-menu', () => { { tz: undefined, UTCOffset: undefined }, ...testTimeZones, ])('and timeZone prop is $tz', prop => { - const dec24Local = newUTC( - 2023, - Month.December, - 24, - 23 - (prop.UTCOffset ?? UTCOffset), - 59, - ); - const dec25Local = newUTC( - 2023, - Month.December, - 25, - 0 - (prop.UTCOffset ?? UTCOffset), - 0, - ); + const elevenLocal = 23 - (prop.UTCOffset ?? UTCOffset); + const midnightLocal = 0 - (prop.UTCOffset ?? UTCOffset); + const dec24Local = newUTC(2023, Month.December, 24, elevenLocal, 59); + const dec25Local = newUTC(2023, Month.December, 25, midnightLocal, 0); const dec24ISO = '2023-12-24'; const dec25ISO = '2023-12-25'; const ctx = { diff --git a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts index 265d31de63..0f68389191 100644 --- a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts +++ b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.spec.ts @@ -8,7 +8,7 @@ import { getSimulatedTZDate } from './getSimulatedTZDate'; describe('packages/date-utils/getSimulatedTZDate', () => { test.each(testTimeZones)('Simulates a date in $tz', ({ tz, UTCOffset }) => { - // 2023-07-04 + // 2023-12-25 const testDate = newUTC(2023, Month.December, 25, 0, 0); const sim = getSimulatedTZDate(testDate, tz); diff --git a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts index 5673a10e7a..9a30080e04 100644 --- a/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts +++ b/packages/date-utils/src/getSimulatedTZDate/getSimulatedTZDate.ts @@ -4,7 +4,7 @@ import { getTimezoneOffset } from 'date-fns-tz'; import { isValidDate } from '../isValidDate'; /** - * Returns a date object that, _looks like_ the local time + * Returns a date object that _looks like_ the local time * for the given time zone when printed in ISO format. * * e.g. given `date = "2023-12-25T01:00Z"` and `timeZone = "America/Los_Angeles"`, diff --git a/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.spec.ts b/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.spec.ts new file mode 100644 index 0000000000..0ee6a80f41 --- /dev/null +++ b/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.spec.ts @@ -0,0 +1,16 @@ +import { Month } from '../constants'; +import { newUTC } from '../newUTC'; + +import { getSimulatedUTCDate } from '.'; + +describe('packages/date-utils/getSimulatedUTCDate', () => { + // TODO: Test in multiple time zones with properly mocked Date object + test('Simulates a date in UTC', () => { + // 2023-12-25 + const testDate = newUTC(2023, Month.December, 25, 0, 0); + + const sim = getSimulatedUTCDate(testDate); + + expect(sim?.toDateString()).toEqual('Mon Dec 25 2023'); + }); +}); diff --git a/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.ts b/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.ts new file mode 100644 index 0000000000..3493a4879b --- /dev/null +++ b/packages/date-utils/src/getSimulatedUTCDate/getSimulatedUTCDate.ts @@ -0,0 +1,32 @@ +import { addMilliseconds } from 'date-fns'; +import { getTimezoneOffset } from 'date-fns-tz'; + +import { isValidDate } from '../isValidDate'; + +/** + * The inverse of `getSimulatedTZDate`, returns a date object that _looks like_ + * the UTC representation when printed in the local time zone. + * + * e.g. given `date = "2023-12-25T01:00Z"` and `timeZone = "America/Los_Angeles"`, + * by default using `date.toDateString` (or similar) + * this would print the locale string: + * "Sun Dec 24 2023" + * + * This function returns a modified, (technically incorrect) date object, + * such that the function `date.toDateString` (or similar) + * returns the locale string: + * `Mon Dec 25 2023` + */ +export const getSimulatedUTCDate = (date: Date, timeZone?: string): Date => { + timeZone = timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone; + + if (!isValidDate(date)) return date; + + // Milliseconds offset between the given time zone & UTC + const offsetMs = getTimezoneOffset(timeZone, date); + if (isNaN(offsetMs)) return date; + + const simulatedUTC = addMilliseconds(date, -offsetMs); + + return simulatedUTC; +}; diff --git a/packages/date-utils/src/getSimulatedUTCDate/index.ts b/packages/date-utils/src/getSimulatedUTCDate/index.ts new file mode 100644 index 0000000000..7a039c83c8 --- /dev/null +++ b/packages/date-utils/src/getSimulatedUTCDate/index.ts @@ -0,0 +1 @@ +export { getSimulatedUTCDate } from './getSimulatedUTCDate'; diff --git a/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts b/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts index 4839f22045..194748b338 100644 --- a/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts +++ b/packages/date-utils/src/getUTCDateString/getUTCDateString.spec.ts @@ -1,17 +1,15 @@ import { Month } from '../constants'; +import { newUTC } from '../newUTC'; import { getUTCDateString } from '.'; describe('packages/date-utils/getUTCDateString', () => { + // TODO: Test in multiple time zones with a properly mocked Date object test('returns date string relative to UTC', () => { - const date = new Date(Date.UTC(2023, Month.September, 10)); + const date = newUTC(2023, Month.September, 10); const str = getUTCDateString(date); - expect(str).toBe('Sun Sep 10 2023'); + expect(str).toBe('Sunday, September 10, 2023'); }); - test('returns date string relative with time provided', () => { - const date = new Date(Date.UTC(2023, Month.September, 10, 12, 0)); - const str = getUTCDateString(date); - expect(str).toBe('Sun Sep 10 2023'); - }); + test.todo('returns a localized date string'); }); diff --git a/packages/date-utils/src/getUTCDateString/getUTCDateString.ts b/packages/date-utils/src/getUTCDateString/getUTCDateString.ts index 6a5930d8e8..cf5d991033 100644 --- a/packages/date-utils/src/getUTCDateString/getUTCDateString.ts +++ b/packages/date-utils/src/getUTCDateString/getUTCDateString.ts @@ -1,12 +1,39 @@ -import { addMinutes } from 'date-fns'; +import { getSimulatedUTCDate } from '../getSimulatedUTCDate'; +import { isValidLocale } from '../isValidLocale'; -export const getUTCDateString = (date: Date): string => { - const utcOffsetMins = date.getTimezoneOffset(); +interface GetUTCDateStringOptions { + locale?: string; +} - // Create a timestamp that, when printed in local time, - // appears as the UTC equivalent of the provided date - const fakeUTCDate = addMinutes(date, utcOffsetMins); +/** + * Returns a localized date string for the UTC representation of a date, regardless of system time zone + * + * e.g. + * ``` + * getUTCDateString( + * Date("2023-12-25T01:00:00Z"), + * { locale: 'en-US' } + * ) // "Monday, December 25, 2023" + * ``` + */ +export const getUTCDateString = ( + date: Date, + options?: GetUTCDateStringOptions, +): string => { + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + + const dateInTZ = getSimulatedUTCDate(date, timeZone); + + const locale = isValidLocale(options?.locale) + ? options?.locale + : Intl.DateTimeFormat().resolvedOptions().locale; + + const utcDateString = dateInTZ.toLocaleDateString(locale, { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }); - const utcDateString = fakeUTCDate.toDateString(); return utcDateString; }; diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index e0ecea381a..e9f4c920fe 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -12,6 +12,7 @@ export { getLocaleWeekdays } from './getLocaleWeekdays'; export { getMonthIndex } from './getMonthIndex'; export { getMonthName } from './getMonthName'; export { getSimulatedTZDate } from './getSimulatedTZDate'; +export { getSimulatedUTCDate } from './getSimulatedUTCDate'; export { getUTCDateString } from './getUTCDateString'; export { getWeekdayName } from './getWeekdayName'; export { getWeeksArray } from './getWeeksArray'; diff --git a/packages/date-utils/src/types.ts b/packages/date-utils/src/types.ts index 85576dd80d..2bb0b1c94a 100644 --- a/packages/date-utils/src/types.ts +++ b/packages/date-utils/src/types.ts @@ -4,7 +4,9 @@ export type DateRangeType = [DateType, DateType]; export type LocaleString = 'iso8601' | string; export interface MonthObject { + /** The localized long-form month name (e.g. December, July) */ long: string; + /** A localized short-form month name (e.g. Dec, Jul) */ short: string; } From 848a631d25c6393a8d6a17dc61d84e496fce0565 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:24:58 -0500 Subject: [PATCH 332/351] Date picker [LG-3864] year input rollover (#2138) * prevents rollover in year segment * Update DatePicker.spec.tsx --- .../src/DatePicker/DatePicker.spec.tsx | 8 +-- .../DateInputSegment.spec.tsx | 56 ++++--------------- .../DateInputSegment/DateInputSegment.tsx | 14 +++-- 3 files changed, 24 insertions(+), 54 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 58d508efd6..f18de797d8 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1352,7 +1352,7 @@ describe('packages/date-picker', () => { expect(onDateChange).toHaveBeenCalled(); }); - test('rolls year value over from max prop to min prop', () => { + test('does not roll over year', () => { const onDateChange = jest.fn(); const { yearInput } = renderDatePicker({ onDateChange, @@ -1363,7 +1363,7 @@ describe('packages/date-picker', () => { userEvent.click(yearInput); userEvent.keyboard(`{arrowup}`); expect(onDateChange).toHaveBeenCalledWith( - newUTC(1969, Month.July, 5), + newUTC(2021, Month.July, 5), ); }); @@ -1522,7 +1522,7 @@ describe('packages/date-picker', () => { ); }); - test('rolls year value over from min prop to max prop', () => { + test('does not roll over year', () => { const onDateChange = jest.fn(); const { yearInput } = renderDatePicker({ onDateChange, @@ -1533,7 +1533,7 @@ describe('packages/date-picker', () => { userEvent.click(yearInput); userEvent.keyboard(`{arrowdown}`); expect(onDateChange).toHaveBeenCalledWith( - newUTC(2020, Month.July, 5), + newUTC(1968, Month.July, 5), ); }); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx index 90c6cde9d2..4b32dac90d 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -273,7 +273,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `min` when the new value is greater than the `max` value', () => { + test('rolls value over to default `min` value if value exceeds `max`', () => { const { input } = renderSegment({ segment: 'day', onChange: onChangeHandler, @@ -300,7 +300,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with provided `min` prop when the new value is greater than the `max` value', () => { + test('rolls value over to provided `min` value if value exceeds `max`', () => { const { input } = renderSegment({ segment: 'day', onChange: onChangeHandler, @@ -344,7 +344,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `max` value when the new value is less than the `min` value', () => { + test('rolls value over to default `max` value if value exceeds `min`', () => { const { input } = renderSegment({ segment: 'day', onChange: onChangeHandler, @@ -371,7 +371,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with provided `max` prop value when the new value is less than the `min` value', () => { + test('rolls value over to provided `max` value if value exceeds `min`', () => { const { input } = renderSegment({ segment: 'day', onChange: onChangeHandler, @@ -421,7 +421,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `min` when the new value is greater than the `max` value', () => { + test('rolls value over to default `min` value if value exceeds `max`', () => { const { input } = renderSegment({ segment: 'month', onChange: onChangeHandler, @@ -452,7 +452,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with provided `min` prop when the new value is greater than the `max` value', () => { + test('rolls value over to provided `min` value if value exceeds `max`', () => { const { input } = renderSegment({ segment: 'month', onChange: onChangeHandler, @@ -500,7 +500,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `max` value when the new value is less than the `min` value', () => { + test('rolls value over to default `max` value if value exceeds `min`', () => { const { input } = renderSegment({ segment: 'month', onChange: onChangeHandler, @@ -531,7 +531,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with provided `max` prop value when the new value is less than the `min` value', () => { + test('rolls value over to provided `max` value if value exceeds `min`', () => { const { input } = renderSegment({ segment: 'month', onChange: onChangeHandler, @@ -582,7 +582,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `min` when the new value is greater than the `max` value', () => { + test('does _not_ rollover if value exceeds max', () => { const { input } = renderSegment({ segment: 'year', onChange: onChangeHandler, @@ -592,7 +592,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { userEvent.type(input, '{arrowup}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(defaultMin['year']), + value: formatter(defaultMax['year'] + 1), }), ); }); @@ -612,22 +612,6 @@ describe('packages/date-picker/shared/date-input-segment', () => { }), ); }); - - test('calls handler with provided `min` prop when the new value is greater than the `max` value', () => { - const { input } = renderSegment({ - segment: 'year', - onChange: onChangeHandler, - value: formatter(defaultMax['year']), - min: 1969, - }); - - userEvent.type(input, '{arrowup}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter(1969), - }), - ); - }); }); describe('Down arrow', () => { @@ -660,7 +644,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { ); }); - test('calls handler with default `max` value when the new value is less than the `min` value', () => { + test('does _not_ rollover if value exceeds min', () => { const { input } = renderSegment({ segment: 'year', onChange: onChangeHandler, @@ -670,7 +654,7 @@ describe('packages/date-picker/shared/date-input-segment', () => { userEvent.type(input, '{arrowdown}'); expect(onChangeHandler).toHaveBeenCalledWith( expect.objectContaining({ - value: formatter(defaultMax['year']), + value: formatter(defaultMin['year'] - 1), }), ); }); @@ -690,22 +674,6 @@ describe('packages/date-picker/shared/date-input-segment', () => { }), ); }); - - test('calls handler with provided `max` prop value when the new value is less than the `min` value', () => { - const { input } = renderSegment({ - segment: 'year', - onChange: onChangeHandler, - value: formatter(defaultMin['year']), - max: 2000, - }); - - userEvent.type(input, '{arrowdown}'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ - value: formatter(2000), - }), - ); - }); }); }); }); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index a3ae38e55d..b754db4d50 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -104,14 +104,16 @@ export const DateInputSegment = React.forwardRef< e.preventDefault(); const valueDiff = key === keyMap.ArrowUp ? 1 : -1; + const defaultVal = key === keyMap.ArrowUp ? min : max; - const currentValue: number = value - ? Number(value) - : key === keyMap.ArrowUp - ? max - : min; + const incrementedValue: number = value + ? Number(value) + valueDiff + : defaultVal; - const newValue = rollover(currentValue + valueDiff, min, max); + const newValue = + segment === 'year' + ? incrementedValue + : rollover(incrementedValue, min, max); const valueString = formatter(newValue); onChange({ From 2414e5e98fda34ba604068d8c7e02ec92a394b72 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Fri, 15 Dec 2023 16:04:57 -0500 Subject: [PATCH 333/351] Date picker [LG-3895] Segment input restriction (#2140) * reorganize segment tests * create getNewSegmentValueFromArrowKeyPress * updates getNewSegmentValueFromInputValue * tests for getNewSegmentValueFromInputValue * only fires change handler for explicit values * lint * Update DateInputSegment.tsx --- .../date-picker/src/DatePicker.stories.tsx | 6 +- .../src/DatePicker/DatePicker.spec.tsx | 166 ++++++++++------- .../src/DatePicker/DatePicker.testutils.tsx | 2 +- .../DateInput/DateInputBox/DateInputBox.tsx | 12 +- .../DateInputSegment.spec.tsx | 115 ++++++------ .../DateInputSegment/DateInputSegment.tsx | 52 +++--- .../calculateNewSegmentValue/index.ts | 29 --- .../getNewSegmentValueFromArrowKeyPress.ts | 31 ++++ .../getNewSegmentValueFromInputValue.spec.ts | 173 ++++++++++++++++++ .../getNewSegmentValueFromInputValue.ts | 56 ++++++ .../DateInput/DateInputSegment/utils/index.ts | 1 + .../date-picker/src/shared/utils/index.ts | 1 + .../isEverySegmentValueExplicit/index.ts | 1 + .../isEverySegmentValueExplicit.ts | 14 ++ 14 files changed, 478 insertions(+), 181 deletions(-) delete mode 100644 packages/date-picker/src/shared/components/DateInput/DateInputSegment/calculateNewSegmentValue/index.ts create mode 100644 packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts create mode 100644 packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.spec.ts create mode 100644 packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.ts create mode 100644 packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/index.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/index.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 44cae358b8..7b531334b7 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -97,7 +97,11 @@ export const LiveExample: StoryFn = props => { { + // eslint-disable-next-line no-console + console.log('Storybook: onDateChange', { v }); + setValue(v); + }} handleValidation={date => // eslint-disable-next-line no-console console.log('Storybook: handleValidation', { date }) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index f18de797d8..b60b7a66b0 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1656,43 +1656,71 @@ describe('packages/date-picker', () => { }); describe('into a single segment', () => { - test('does not fire a value change handler', () => { + test('does not fire a value change handler', async () => { const onDateChange = jest.fn(); const { yearInput } = renderDatePicker({ onDateChange, }); userEvent.type(yearInput, '2023'); - expect(onDateChange).not.toHaveBeenCalled(); + await waitFor(() => expect(onDateChange).not.toHaveBeenCalled()); }); - test('does not fire a validation handler', () => { + test('does not fire a validation handler', async () => { const handleValidation = jest.fn(); const { yearInput } = renderDatePicker({ handleValidation, }); userEvent.type(yearInput, '2023'); - expect(handleValidation).not.toHaveBeenCalled(); + await waitFor(() => expect(handleValidation).not.toHaveBeenCalled()); }); - test('fires a segment change handler', () => { + test('fires a segment change handler', async () => { const onChange = jest.fn(); const { yearInput } = renderDatePicker({ onChange, }); userEvent.type(yearInput, '2023'); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue('2023'), + await waitFor(() => + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2023'), + ), ); }); - test('does not immediately format the segment', () => { + test('does not immediately format the segment (year)', async () => { const onChange = jest.fn(); - const { monthInput } = renderDatePicker({ onChange }); - userEvent.type(monthInput, '1'); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue('1'), - ); - expect(monthInput.value).toBe('1'); + const { yearInput } = renderDatePicker({ onChange }); + userEvent.type(yearInput, '20'); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('20'), + ); + expect(yearInput.value).toBe('20'); + }); + }); + + test('does not immediately format the segment (day)', async () => { + const onChange = jest.fn(); + const { dayInput } = renderDatePicker({ onChange }); + userEvent.type(dayInput, '2'); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('2'), + ); + expect(dayInput.value).toBe('2'); + }); + }); + + test('allows typing multiple digits', async () => { + const onChange = jest.fn(); + const { dayInput } = renderDatePicker({ onChange }); + userEvent.type(dayInput, '26'); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue('26'), + ); + expect(dayInput.value).toBe('26'); + }); }); describe('typing space', () => { @@ -1926,6 +1954,15 @@ describe('packages/date-picker', () => { expect(dayInput).toHaveFocus(); await waitFor(() => expect(dayInput).toHaveValue('2')); }); + + test('when day value is explicit, segment is formatted', async () => { + const { dayInput } = renderDatePicker({ + locale, + }); + userEvent.type(dayInput, '26'); + expect(dayInput).toHaveFocus(); + await waitFor(() => expect(dayInput).toHaveValue('26')); + }); }); describe('for en-US format', () => { @@ -1976,8 +2013,8 @@ describe('packages/date-picker', () => { }); }); - describe('typing a full value', () => { - test('fires value change handler', () => { + describe('typing a full date value', () => { + test('fires value change handler for explicit values', async () => { const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ onDateChange, @@ -1985,12 +2022,27 @@ describe('packages/date-picker', () => { userEvent.type(yearInput, '2003'); userEvent.type(monthInput, '12'); userEvent.type(dayInput, '26'); - expect(onDateChange).toHaveBeenCalledWith( - expect.objectContaining(newUTC(2003, Month.December, 26)), + + await waitFor(() => + expect(onDateChange).toHaveBeenCalledWith( + expect.objectContaining(newUTC(2003, Month.December, 26)), + ), ); }); - test('properly renders the input', () => { + test('does not fire value change handler for ambiguous values', async () => { + const onDateChange = jest.fn(); + const { yearInput, monthInput, dayInput } = renderDatePicker({ + onDateChange, + }); + userEvent.type(yearInput, '2003'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '2'); + + await waitFor(() => expect(onDateChange).not.toHaveBeenCalled()); + }); + + test('properly renders the input', async () => { const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ onDateChange, @@ -1998,9 +2050,11 @@ describe('packages/date-picker', () => { userEvent.type(yearInput, '2003'); userEvent.type(monthInput, '12'); userEvent.type(dayInput, '26'); - expect(yearInput).toHaveValue('2003'); - expect(monthInput).toHaveValue('12'); - expect(dayInput).toHaveValue('26'); + await waitFor(() => { + expect(yearInput).toHaveValue('2003'); + expect(monthInput).toHaveValue('12'); + expect(dayInput).toHaveValue('26'); + }); }); describe('if value is out of range', () => { @@ -2019,7 +2073,7 @@ describe('packages/date-picker', () => { ); }); - test('properly renders input if value is after MAX', () => { + test('properly renders input if value is after MAX', async () => { const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ onDateChange, @@ -2027,12 +2081,15 @@ describe('packages/date-picker', () => { userEvent.type(yearInput, '2048'); userEvent.type(monthInput, '12'); userEvent.type(dayInput, '23'); - expect(yearInput).toHaveValue('2048'); - expect(monthInput).toHaveValue('12'); - expect(dayInput).toHaveValue('23'); + + await waitFor(() => { + expect(yearInput).toHaveValue('2048'); + expect(monthInput).toHaveValue('12'); + expect(dayInput).toHaveValue('23'); + }); }); - test('fire a value change handler if value is before MIN', () => { + test('fire a value change handler if value is before MIN', async () => { const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ onDateChange, @@ -2040,12 +2097,14 @@ describe('packages/date-picker', () => { userEvent.type(yearInput, '1969'); userEvent.type(monthInput, '7'); userEvent.type(dayInput, '20'); - expect(onDateChange).toHaveBeenCalledWith( - expect.objectContaining(newUTC(1969, Month.July, 20)), + await waitFor(() => + expect(onDateChange).toHaveBeenCalledWith( + expect.objectContaining(newUTC(1969, Month.July, 20)), + ), ); }); - test('properly renders input if value is before MIN', () => { + test('properly renders input if value is before MIN', async () => { const onDateChange = jest.fn(); const { yearInput, monthInput, dayInput } = renderDatePicker({ onDateChange, @@ -2053,9 +2112,11 @@ describe('packages/date-picker', () => { userEvent.type(yearInput, '1969'); userEvent.type(monthInput, '7'); userEvent.type(dayInput, '20'); - expect(yearInput).toHaveValue('1969'); - expect(monthInput).toHaveValue('07'); - expect(dayInput).toHaveValue('20'); + await waitFor(() => { + expect(yearInput).toHaveValue('1969'); + expect(monthInput).toHaveValue('07'); + expect(dayInput).toHaveValue('20'); + }); }); }); }); @@ -2095,42 +2156,21 @@ describe('packages/date-picker', () => { }); describe('typing new characters', () => { - test('does not immediately format the year', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker({}); - userEvent.type(yearInput, '2019'); - userEvent.type(monthInput, '6'); - userEvent.type(dayInput, '1'); - - userEvent.type(yearInput, '9'); - userEvent.type(yearInput, '9'); - expect(yearInput).toHaveValue('1999'); - }); - - test('appends to the segment value if the resulting value is valid', () => { + test('even if the resulting value is valid, keeps the input as-is', async () => { const { monthInput } = renderDatePicker({}); userEvent.type(monthInput, '1'); userEvent.tab(); - expect(monthInput).toHaveValue('01'); + await waitFor(() => expect(monthInput).toHaveValue('01')); userEvent.type(monthInput, '2'); - expect(monthInput).toHaveValue('12'); + await waitFor(() => expect(monthInput).toHaveValue('01')); }); - describe('if the resulting value is not valid', () => { - test('overwrites the segment with the incoming digit 1-9', () => { - const { monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '6'); - expect(monthInput).toHaveValue('06'); - userEvent.type(monthInput, '9'); - expect(monthInput).toHaveValue('09'); - }); - - test('overwrites the segment with the incoming digit 0', () => { - const { monthInput } = renderDatePicker({}); - userEvent.type(monthInput, '6'); - expect(monthInput).toHaveValue('06'); - userEvent.type(monthInput, '0'); - expect(monthInput).toHaveValue('0'); - }); + test('if the resulting value is not valid, keeps the input as-is', async () => { + const { monthInput } = renderDatePicker({}); + userEvent.type(monthInput, '6'); + await waitFor(() => expect(monthInput).toHaveValue('06')); + userEvent.type(monthInput, '9'); + await waitFor(() => expect(monthInput).toHaveValue('06')); }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx index 91010cc604..d8309869d5 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.testutils.tsx @@ -72,7 +72,7 @@ export interface RenderMenuResult { export const renderDatePicker = ( props?: Partial, ): RenderDatePickerResult => { - const defaultProps = { label: '' }; + const defaultProps = { label: 'label' }; const result = render( , ); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index 41e2b24352..c915db5a03 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -20,6 +20,7 @@ import { getMinSegmentValue, getRelativeSegment, getValueFormatter, + isEverySegmentValueExplicit, isExplicitSegmentValue, newDateFromSegments, } from '../../../utils'; @@ -86,15 +87,16 @@ export const DateInputBox = React.forwardRef( const hasAnySegmentChanged = !isEqual(newSegments, prevSegments); if (hasAnySegmentChanged) { - const utcDate = newDateFromSegments(newSegments); const areAllSegmentsEmpty = !doesSomeSegmentExist(newSegments); + const areAllExplicit = isEverySegmentValueExplicit(newSegments); + const utcDate = newDateFromSegments(newSegments); - if (utcDate) { - // Update the value iff all segments create a valid date. - setValue?.(utcDate); - } else if (areAllSegmentsEmpty) { + if (areAllSegmentsEmpty) { // otherwise, if no segment exists, set the external value to null setValue?.(null); + } else if (areAllExplicit && !!utcDate) { + // Update the value iff all segments create a valid date. + setValue?.(utcDate); } } }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx index 4b32dac90d..3997c91569 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.spec.tsx @@ -5,7 +5,6 @@ import userEvent from '@testing-library/user-event'; import { defaultMax, defaultMin } from '../../../constants'; import { - defaultSharedDatePickerContext, SharedDatePickerProvider, SharedDatePickerProviderProps, } from '../../../context'; @@ -26,14 +25,16 @@ const renderSegment = ( }; const result = render( - + , ); const rerenderSegment = (newProps: Partial) => result.rerender( - , + + , + , ); const getInput = () => @@ -141,75 +142,77 @@ describe('packages/date-picker/shared/date-input-segment', () => { }); describe('Typing', () => { - test('calls the change handler', () => { - const { input } = renderSegment({ - onChange: onChangeHandler, + describe('into an empty segment', () => { + test('calls the change handler', () => { + const { input } = renderSegment({ + onChange: onChangeHandler, + }); + + userEvent.type(input, '8'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: '8' }), + ); }); - userEvent.type(input, '8'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '8' }), - ); - }); + test('allows zero character', () => { + const { input } = renderSegment({ + onChange: onChangeHandler, + }); - test('allows typing additional characters to create a valid value', () => { - const { input } = renderSegment({ - value: '02', - onChange: onChangeHandler, + userEvent.type(input, '0'); + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: '0' }), + ); }); - userEvent.type(input, '6'); - expect(onChangeHandler).toHaveBeenCalled(); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '26' }), - ); - }); + test('allows typing leading zeroes', async () => { + const { input, rerenderSegment } = renderSegment({ + onChange: onChangeHandler, + }); + + userEvent.type(input, '0'); + rerenderSegment({ value: '0' }); - test('does not allow additional characters that create an invalid value', () => { - const { input } = renderSegment({ - value: '26', - onChange: onChangeHandler, + userEvent.type(input, '2'); + await waitFor(() => { + expect(onChangeHandler).toHaveBeenCalledWith( + expect.objectContaining({ value: '02' }), + ); + }); }); - userEvent.type(input, '6'); - expect(onChangeHandler).toHaveBeenCalled(); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '06' }), - ); - }); + test('does not allow non-number characters', () => { + const { input } = renderSegment({ + onChange: onChangeHandler, + }); - test('allows zero character', () => { - const { input } = renderSegment({ - onChange: onChangeHandler, + userEvent.type(input, 'aB$/'); + expect(onChangeHandler).not.toHaveBeenCalled(); }); - - userEvent.type(input, '0'); - expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '0' }), - ); }); - test('allows leading zeroes', async () => { - const { input } = renderSegment({ - value: '0', - onChange: onChangeHandler, - }); + describe('into a segment with a value', () => { + test('allows typing additional characters if the current value is incomplete', () => { + const { input } = renderSegment({ + value: '2', + onChange: onChangeHandler, + }); - userEvent.type(input, '2'); - await waitFor(() => + userEvent.type(input, '6'); expect(onChangeHandler).toHaveBeenCalledWith( - expect.objectContaining({ value: '02' }), - ), - ); - }); - - test('does not allow non-number characters', () => { - const { input } = renderSegment({ - onChange: onChangeHandler, + expect.objectContaining({ value: '26' }), + ); }); - userEvent.type(input, 'aB$/'); - expect(onChangeHandler).not.toHaveBeenCalled(); + test('does not allow additional characters that create an invalid value', () => { + const { input } = renderSegment({ + value: '26', + onChange: onChangeHandler, + }); + + userEvent.type(input, '6'); + expect(onChangeHandler).not.toHaveBeenCalled(); + }); }); }); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index b754db4d50..57c23f8aac 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -3,7 +3,7 @@ import React, { ChangeEventHandler, KeyboardEventHandler } from 'react'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; -import { keyMap, rollover } from '@leafygreen-ui/lib'; +import { keyMap } from '@leafygreen-ui/lib'; import { Size } from '@leafygreen-ui/tokens'; import { useUpdatedBaseFontSize } from '@leafygreen-ui/typography'; @@ -16,7 +16,7 @@ import { import { useSharedDatePickerContext } from '../../../context'; import { getAutoComplete, getValueFormatter } from '../../../utils'; -import { calculateNewSegmentValue } from './calculateNewSegmentValue'; +import { getNewSegmentValueFromArrowKeyPress } from './utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress'; import { baseStyles, fontSizeStyles, @@ -25,6 +25,7 @@ import { segmentWidthStyles, } from './DateInputSegment.styles'; import { DateInputSegmentProps } from './DateInputSegment.types'; +import { getNewSegmentValueFromInputValue } from './utils'; /** * Renders a single date segment with the @@ -65,29 +66,30 @@ export const DateInputSegment = React.forwardRef< const autoComplete = getAutoComplete(autoCompleteProp, segment); const pattern = `[0-9]{${charsPerSegment[segment]}}`; - /** Prevent non-numeric values from triggering a change event */ + /** + * Receives native input events, + * determines whether the input value is valid and should change, + * and fires a custom `DateInputSegmentChangeEvent`. + */ const handleChange: ChangeEventHandler = e => { const { target } = e; - const containsPeriod = /\./.test(target.value); - const prevValue = value ?? ''; - - // macOS adds a period when pressing SPACE twice inside a text input. - // If there is a period replace the value with the prevValue. - if (containsPeriod) { - target.value = prevValue; - return; - } - const hasValueChanged = target.value !== prevValue; - const numericValue = Number(target.value); + const newValue = getNewSegmentValueFromInputValue( + segment, + value, + target.value, + ); - if (!isNaN(numericValue) && hasValueChanged) { - const newValue = calculateNewSegmentValue(segment, target.value); + const hasValueChanged = newValue !== value; + if (hasValueChanged) { onChange({ segment, value: newValue, }); + } else { + // If the value has not changed, ensure the input value is reset + target.value = value; } }; @@ -103,17 +105,14 @@ export const DateInputSegment = React.forwardRef< /** Fire a custom change event when the up/down arrow keys are pressed */ e.preventDefault(); - const valueDiff = key === keyMap.ArrowUp ? 1 : -1; - const defaultVal = key === keyMap.ArrowUp ? min : max; - - const incrementedValue: number = value - ? Number(value) + valueDiff - : defaultVal; - const newValue = - segment === 'year' - ? incrementedValue - : rollover(incrementedValue, min, max); + const newValue = getNewSegmentValueFromArrowKeyPress({ + key, + value, + min, + max, + segment, + }); const valueString = formatter(newValue); onChange({ @@ -163,6 +162,7 @@ export const DateInputSegment = React.forwardRef< ref={inputRef} type="text" pattern={pattern} + maxLength={charsPerSegment[segment]} role="spinbutton" value={value} min={min} diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/calculateNewSegmentValue/index.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/calculateNewSegmentValue/index.ts deleted file mode 100644 index be09df0bdf..0000000000 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/calculateNewSegmentValue/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import last from 'lodash/last'; - -import { truncateStart } from '@leafygreen-ui/lib'; - -import { charsPerSegment } from '../../../../constants'; -import { DateSegment, DateSegmentValue } from '../../../../types'; -import { getValueFormatter, isValidValueForSegment } from '../../../../utils'; - -/** - * Calculates the new value for the segment given an incoming change - */ -export const calculateNewSegmentValue = ( - segmentName: DateSegment, - incomingValue: DateSegmentValue, -): DateSegmentValue => { - if ( - !isValidValueForSegment(segmentName, incomingValue) && - segmentName !== 'year' - ) { - const formatter = getValueFormatter(segmentName); - const typedChar = last(incomingValue.split('')); - const newValue = typedChar === '0' ? '0' : formatter(typedChar); - return newValue; - } - - return truncateStart(incomingValue, { - length: charsPerSegment[segmentName], - }); -}; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts new file mode 100644 index 0000000000..c52f81774c --- /dev/null +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts @@ -0,0 +1,31 @@ +import { keyMap, rollover } from '@leafygreen-ui/lib'; + +import { DateSegment, DateSegmentValue } from '../../../../../types'; + +export const getNewSegmentValueFromArrowKeyPress = ({ + value, + key, + segment, + min, + max, +}: { + value: DateSegmentValue; + key: typeof keyMap.ArrowUp | typeof keyMap.ArrowDown; + segment: DateSegment; + min: number; + max: number; +}) => { + const valueDiff = key === keyMap.ArrowUp ? 1 : -1; + const defaultVal = key === keyMap.ArrowUp ? min : max; + + const incrementedValue: number = value + ? Number(value) + valueDiff + : defaultVal; + + const newValue = + segment === 'year' + ? incrementedValue + : rollover(incrementedValue, min, max); + + return newValue; +}; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.spec.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.spec.ts new file mode 100644 index 0000000000..cdfedfb522 --- /dev/null +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.spec.ts @@ -0,0 +1,173 @@ +import { range } from 'lodash'; + +import { defaultMax, defaultMin } from '../../../../../constants'; +import { DateSegment } from '../../../../../types'; +import { getValueFormatter } from '../../../../../utils'; + +import { getNewSegmentValueFromInputValue } from './getNewSegmentValueFromInputValue'; + +describe('packages/date-picker/shared/date-input-segment/getNewSegmentValueFromInputValue', () => { + describe.each(['day', 'month', 'year'])( + 'For segment %p', + (segment: DateSegment) => { + describe('when current value is empty', () => { + test.each(range(10))('accepts %i character as input', i => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '', + `${i}`, + ); + expect(newValue).toEqual(`${i}`); + }); + + const validValues = [defaultMin[segment], defaultMax[segment]]; + test.each(validValues)(`accepts value "%i" as input`, v => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '', + `${v}`, + ); + expect(newValue).toEqual(`${v}`); + }); + + test('does not accept non-numeric characters', () => { + const newValue = getNewSegmentValueFromInputValue(segment, '', `b`); + expect(newValue).toEqual(''); + }); + + test('does not accept input with a period/decimal', () => { + const newValue = getNewSegmentValueFromInputValue(segment, '', `2.`); + expect(newValue).toEqual(''); + }); + }); + + describe('when current value is 0', () => { + if (segment !== 'year') { + test('rejects additional 0 as input', () => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '0', + `00`, + ); + expect(newValue).toEqual(`0`); + }); + } + test.each(range(1, 10))('accepts 0%i as input', i => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '0', + `0${i}`, + ); + expect(newValue).toEqual(`0${i}`); + }); + test('value can be deleted', () => { + const newValue = getNewSegmentValueFromInputValue(segment, '0', ``); + expect(newValue).toEqual(``); + }); + }); + + describe('when current value is 1', () => { + test('value can be deleted', () => { + const newValue = getNewSegmentValueFromInputValue(segment, '1', ``); + expect(newValue).toEqual(``); + }); + + if (segment === 'month') { + test.each(range(0, 3))('accepts 1%i as input', i => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '1', + `1${i}`, + ); + expect(newValue).toEqual(`1${i}`); + }); + describe.each(range(3, 10))('rejects 1%i', i => { + test(`and sets input "${i}"`, () => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '1', + `1${i}`, + ); + expect(newValue).toEqual(`${i}`); + }); + }); + } else { + test.each(range(10))('accepts 1%i as input', i => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '1', + `1${i}`, + ); + expect(newValue).toEqual(`1${i}`); + }); + } + }); + + describe('when current value is 3', () => { + test('value can be deleted', () => { + const newValue = getNewSegmentValueFromInputValue(segment, '3', ``); + expect(newValue).toEqual(``); + }); + + switch (segment) { + case 'day': { + test.each(range(0, 2))('accepts 3%i as input', i => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '3', + `3${i}`, + ); + expect(newValue).toEqual(`3${i}`); + }); + describe.each(range(3, 10))('rejects 3%i', i => { + test(`and sets input to ${i}`, () => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '3', + `3${i}`, + ); + expect(newValue).toEqual(`${i}`); + }); + }); + break; + } + + case 'month': { + describe.each(range(10))('rejects 3%i', i => { + test(`and sets input "${i}"`, () => { + const newValue = getNewSegmentValueFromInputValue( + segment, + '3', + `3${i}`, + ); + expect(newValue).toEqual(`${i}`); + }); + }); + break; + } + + default: + break; + } + }); + + describe('when current value is a full formatted value', () => { + const formatter = getValueFormatter(segment); + const testValues = [defaultMin[segment], defaultMax[segment]].map( + formatter, + ); + test.each(testValues)( + 'when current value is %p, rejects additional input', + val => { + const newValue = getNewSegmentValueFromInputValue( + segment, + val, + `${val}1`, + ); + expect(newValue).toEqual(val); + }, + ); + }); + }, + ); +}); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.ts new file mode 100644 index 0000000000..1aff779713 --- /dev/null +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue.ts @@ -0,0 +1,56 @@ +import last from 'lodash/last'; + +import { truncateStart } from '@leafygreen-ui/lib'; + +import { charsPerSegment } from '../../../../../constants'; +import { DateSegment, DateSegmentValue } from '../../../../../types'; +import { isValidValueForSegment } from '../../../../../utils'; + +/** + * Calculates the new value for the segment given an incoming change. + * + * Does not allow incoming values that + * - are not valid numbers + * - include a period + * - would cause the segment to overflow + */ +export const getNewSegmentValueFromInputValue = ( + segmentName: DateSegment, + currentValue: DateSegmentValue, + incomingValue: DateSegmentValue, +): DateSegmentValue => { + // If the incoming value is not a valid number + const isIncomingValueNumber = !isNaN(Number(incomingValue)); + // macOS adds a period when pressing SPACE twice inside a text input. + const doesIncomingValueContainPeriod = /\./.test(incomingValue); + + // if the current value is "full", do not allow any additional characters to be entered + const wouldCauseOverflow = + currentValue.length === charsPerSegment[segmentName] && + incomingValue.length > charsPerSegment[segmentName]; + + if ( + !isIncomingValueNumber || + doesIncomingValueContainPeriod || + wouldCauseOverflow + ) { + return currentValue; + } + + const isIncomingValueValid = isValidValueForSegment( + segmentName, + incomingValue, + ); + + if (isIncomingValueValid || segmentName === 'year') { + const newValue = truncateStart(incomingValue, { + length: charsPerSegment[segmentName], + }); + + return newValue; + } + + const typedChar = last(incomingValue.split('')); + const newValue = typedChar === '0' ? '0' : typedChar ?? ''; + return newValue; +}; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/index.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/index.ts new file mode 100644 index 0000000000..f71520a27c --- /dev/null +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/index.ts @@ -0,0 +1 @@ +export { getNewSegmentValueFromInputValue } from './getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue'; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index 924d0cea24..cf15e082e0 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -17,6 +17,7 @@ export { } from './getSegmentsFromDate'; export { getValueFormatter } from './getValueFormatter'; export { isElementInputSegment } from './isElementInputSegment'; +export { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit'; export { isExplicitSegmentValue } from './isExplicitSegmentValue'; export { isValidSegmentName, isValidSegmentValue } from './isValidSegment'; export { isValidValueForSegment } from './isValidValueForSegment'; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/index.ts b/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/index.ts new file mode 100644 index 0000000000..f808d60b69 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/index.ts @@ -0,0 +1 @@ +export { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit'; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts b/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts new file mode 100644 index 0000000000..10ec19bd54 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentValueExplicit/isEverySegmentValueExplicit.ts @@ -0,0 +1,14 @@ +import { DateSegment, DateSegmentsState } from '../../types'; +import { isExplicitSegmentValue } from '../isExplicitSegmentValue'; + +/** + * Returns whether every segment's value is explicit and unambiguous + * (see {@link isExplicitSegmentValue}) + */ +export const isEverySegmentValueExplicit = ( + segments: DateSegmentsState, +): boolean => { + return Object.entries(segments).every(([segment, value]) => + isExplicitSegmentValue(segment as DateSegment, value), + ); +}; From 95af767b939892d4c073e43ef10e9c7a04a3c179 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 18 Dec 2023 15:43:23 -0500 Subject: [PATCH 334/351] Date Picker chromatic fixes (#2141) * fix stories * remove space * update type, does this work?? * wip * fix type * rename ctx to storyContextProps * chromatic updates * change date in interaction test * remove time from story * add withVal date --- .../date-picker/src/DatePicker.stories.tsx | 6 ++ .../DatePickerInput.stories.tsx | 4 + .../DatePickerMenu/DatePickerMenu.stories.tsx | 78 ++++++++++++++----- .../CalendarCell/CalendarCell.stories.tsx | 5 +- .../CalendarGrid/CalendarGrid.stories.tsx | 6 +- .../DateFormField/DateFormField.stories.tsx | 32 ++++---- .../DateInputBox/DateInputBox.stories.tsx | 28 ++++--- .../DateInputSegment.stories.tsx | 19 +++-- .../context/SharedDatePickerContext.utils.ts | 1 + .../getProviderPropsFromStoryContext/index.ts | 13 +--- 10 files changed, 130 insertions(+), 62 deletions(-) diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 7b531334b7..b06747480c 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -66,6 +66,7 @@ const meta: StoryMetaType = { args: { locale: 'iso8601', label: 'Pick a date', + description: 'description', size: Size.Default, autoComplete: AutoComplete.Off, min: MIN_DATE, @@ -113,6 +114,11 @@ export const LiveExample: StoryFn = props => { export const Uncontrolled: StoryFn = props => { return ; }; +Uncontrolled.parameters = { + chromatic: { + disableSnapshots: true, + }, +}; export const InModal: StoryFn = props => { const [value, setValue] = useState(); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx index 40486dc334..8b3bf00f78 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.stories.tsx @@ -70,4 +70,8 @@ export default meta; export const Basic: StoryFn = () => ; +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Generated: StoryFn = () => <>; diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 2c73cc13fd..6bfd0212e9 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -82,25 +82,37 @@ export const Basic: DatePickerMenuStoryType = { MockDate.reset(); const [value, setValue] = useState(null); + const date = new Date(Date.now()); const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); return ( - - Today: {new Date(Date.now()).toUTCString()} - - +
+ + Today:{' '} + {new Intl.DateTimeFormat('en-GB', { + dateStyle: 'full', + }).format(date)} + + +
); }, }; +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const WithValue: DatePickerMenuStoryType = { render: args => { MockDate.reset(); const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); + const date = new Date(Date.now()); + const withValueDate = new Date(2023, Month.September, 10); return (
- Today: {new Date(Date.now()).toUTCString()} + Today:{' '} + {new Intl.DateTimeFormat('en-GB', { + dateStyle: 'full', + }).format(date)} + +

+ + Value:{' '} + {new Intl.DateTimeFormat('en-GB', { + dateStyle: 'full', + }).format(withValueDate)}
@@ -117,6 +139,14 @@ export const WithValue: DatePickerMenuStoryType = { }, }; +export const WithValueDarkMode: DatePickerMenuStoryType = { + ...WithValue, + args: { + // @ts-expect-error - DatePickerMenuStoryType does not include Context props + darkMode: true, + }, +}; + export const MockedToday: DatePickerMenuStoryType = { render: args => { // Force `new Date()` to return `mockToday` @@ -125,19 +155,25 @@ export const MockedToday: DatePickerMenuStoryType = { const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); + const date = new Date(Date.now()); return ( - - Today: {new Date(Date.now()).toUTCString()} - - +
+ + Today:{' '} + {new Intl.DateTimeFormat('en-GB', { + dateStyle: 'full', + }).format(date)} + + +
); }, }; -export const DarkMode: DatePickerMenuStoryType = { - ...WithValue, +export const MockedTodayDarkMode: DatePickerMenuStoryType = { + ...MockedToday, args: { // @ts-expect-error - DatePickerMenuStoryType does not include Context props darkMode: true, @@ -169,7 +205,7 @@ export const InitialFocusValue: DatePickerMenuInteractionTestType = { }; export const LeftArrowKey: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowleft}'); @@ -177,7 +213,7 @@ export const LeftArrowKey: DatePickerMenuInteractionTestType = { }; export const RightArrowKey: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowright}'); @@ -185,7 +221,7 @@ export const RightArrowKey: DatePickerMenuInteractionTestType = { }; export const UpArrowKey: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowup}'); @@ -193,7 +229,7 @@ export const UpArrowKey: DatePickerMenuInteractionTestType = { }; export const DownArrowKey: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowdown}'); @@ -201,7 +237,7 @@ export const DownArrowKey: DatePickerMenuInteractionTestType = { }; export const UpToPrevMonth: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowup}{arrowup}'); @@ -209,7 +245,7 @@ export const UpToPrevMonth: DatePickerMenuInteractionTestType = { }; export const DownToNextMonth: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await InitialFocusToday.play(ctx); userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}'); @@ -217,7 +253,7 @@ export const DownToNextMonth: DatePickerMenuInteractionTestType = { }; export const OpenMonthMenu: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { const canvas = within(ctx.canvasElement.parentElement!); await canvas.findByRole('listbox'); @@ -227,7 +263,7 @@ export const OpenMonthMenu: DatePickerMenuInteractionTestType = { }; export const SelectJanuary: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await OpenMonthMenu.play(ctx); const { findAllByRole } = within(ctx.canvasElement.parentElement!); @@ -238,7 +274,7 @@ export const SelectJanuary: DatePickerMenuInteractionTestType = { }; export const OpenYearMenu: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { const canvas = within(ctx.canvasElement.parentElement!); await canvas.findByRole('listbox'); @@ -248,7 +284,7 @@ export const OpenYearMenu: DatePickerMenuInteractionTestType = { }; export const Select2026: DatePickerMenuInteractionTestType = { - ...Basic, + ...WithValue, play: async ctx => { await OpenYearMenu.play(ctx); const { findAllByRole } = within(ctx.canvasElement.parentElement!); diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx index c50a9c6d8b..1b5a6b492d 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarCell/CalendarCell.stories.tsx @@ -62,7 +62,10 @@ const Template: StoryFn = props => ( export const Basic = Template.bind({}); -// export const Generated = () => {}; +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const DefaultCells: StoryFn = () => <>; DefaultCells.parameters = { generate: { diff --git a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx index 14b3ef43ae..1d268731f2 100644 --- a/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx +++ b/packages/date-picker/src/shared/components/Calendar/CalendarGrid/CalendarGrid.stories.tsx @@ -63,7 +63,7 @@ const meta: StoryMetaType = { export default meta; -export const Demo: StoryFn = ({ ...props }) => { +export const Basic: StoryFn = ({ ...props }) => { const { timeZone } = useSharedDatePickerContext(); const [month] = useState(newUTC(2023, Month.August, 1)); @@ -95,6 +95,10 @@ export const Demo: StoryFn = ({ ...props }) => { ); }; +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Generated: StoryFn = () => <>; Generated.parameters = { generate: { diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx index d30c5ee969..f945f25cef 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.stories.tsx @@ -5,6 +5,7 @@ import { StoryFn } from '@storybook/react'; import { css } from '@leafygreen-ui/emotion'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; +import { Size } from '@leafygreen-ui/tokens'; import { SharedDatePickerContextProps, @@ -14,6 +15,16 @@ import { DatePickerState } from '../../../types'; import { DateFormField } from './DateFormField'; +const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => { + return ( + + + + + + ); +}; + const meta: StoryMetaType< typeof DateFormField, Partial @@ -30,8 +41,9 @@ const meta: StoryMetaType< darkMode: [false, true], label: ['Label', undefined], description: [undefined, 'Description'], - // state: Object.values(DatePickerState), + state: Object.values(DatePickerState), disabled: [false, true], + size: Object.values(Size), }, excludeCombinations: [ { @@ -39,19 +51,7 @@ const meta: StoryMetaType< description: 'Description', }, ], - decorator: (Instance, ctx) => ( - - - - - - ), + decorator: ProviderWrapper, args: { children: ( = () => { export const Basic = Template.bind({}); +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Generated = () => {}; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index b685a9fa1e..1f084f8435 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -5,29 +5,29 @@ import { isValid } from 'date-fns'; import { Month, newUTC, testLocales } from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; -import { pickAndOmit, StoryMetaType, StoryType } from '@leafygreen-ui/lib'; +import { StoryMetaType, StoryType } from '@leafygreen-ui/lib'; import { - contextPropNames, SharedDatePickerContextProps, SharedDatePickerProvider, } from '../../../context'; -import { segmentRefsMock } from '../../../testutils'; +import { + getProviderPropsFromStoryContext, + segmentRefsMock, +} from '../../../testutils'; import { DateInputBox } from './DateInputBox'; const testDate = newUTC(1993, Month.December, 26); const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => { - const [{ darkMode, ...contextProps }, componentProps] = pickAndOmit( - ctx?.args, - contextPropNames, - ); + const { leafyGreenProviderProps, datePickerProviderProps, storyProps } = + getProviderPropsFromStoryContext(ctx?.args); return ( - - - + + + ); @@ -86,10 +86,18 @@ export const Basic: StoryFn = props => { ); }; +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Static: StoryFn = () => { return ; }; +Static.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Formats: StoryType< typeof DateInputBox, SharedDatePickerContextProps diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx index 833b025d3b..2f3964d88c 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.stories.tsx @@ -13,6 +13,14 @@ import { DateSegmentValue } from '../../../types'; import { DateInputSegment } from './DateInputSegment'; +const ProviderWrapper = (Story: StoryFn, ctx?: { args: any }) => ( + + + + + +); + const meta: StoryMetaType< typeof DateInputSegment, SharedDatePickerContextProps @@ -28,12 +36,7 @@ const meta: StoryMetaType< segment: ['day', 'month', 'year'], size: Object.values(Size), }, - decorator: (Instance, ctx) => ( - // @ts-expect-error - incomplete context value - - - - ), + decorator: ProviderWrapper, excludeCombinations: [ { value: '6', @@ -77,4 +80,8 @@ const Template: StoryFn = props => { export const Basic = Template.bind({}); +Basic.parameters = { + chromatic: { disableSnapshot: true }, +}; + export const Generated = () => {}; diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts index ac64623be3..f762191fdc 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts @@ -37,6 +37,7 @@ export const contextPropNames: Array = [ 'initialOpen', 'state', 'autoComplete', + 'darkMode', ]; /** The default context value */ diff --git a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts index 90cc2b7bff..633afcdcdc 100644 --- a/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts +++ b/packages/date-picker/src/shared/testutils/getProviderPropsFromStoryContext/index.ts @@ -1,14 +1,12 @@ -import { StoryContext } from '@storybook/react'; - import { LeafyGreenProviderProps } from '@leafygreen-ui/leafygreen-provider'; import { pickAndOmit } from '@leafygreen-ui/lib'; +import { DatePickerProps } from '../../../DatePicker/DatePicker.types'; import { ContextPropKeys, contextPropNames, SharedDatePickerProviderProps, } from '../../context'; -import { BaseDatePickerProps } from '../../types'; export interface ProviderPropsObject { leafyGreenProviderProps: LeafyGreenProviderProps; @@ -16,13 +14,13 @@ export interface ProviderPropsObject { storyProps: T; } -export const getProviderPropsFromStoryContext =

( - ctx: StoryContext>, +export const getProviderPropsFromStoryContext =

( + storyContextProps: Partial

, ): ProviderPropsObject>> => { const [ { darkMode, baseFontSize, ...datePickerProviderProps }, { ...storyProps }, - ] = pickAndOmit(ctx.args, [...contextPropNames]); + ] = pickAndOmit(storyContextProps, [...contextPropNames]); return { leafyGreenProviderProps: { @@ -30,9 +28,6 @@ export const getProviderPropsFromStoryContext =

( baseFontSize: baseFontSize === 13 ? 14 : baseFontSize, }, datePickerProviderProps: { - label: '', - 'aria-label': '', - 'aria-labelledby': '', ...datePickerProviderProps, }, storyProps, From ab16f9e54b136790591ac0091732472fbdb5ece5 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Mon, 18 Dec 2023 17:32:52 -0500 Subject: [PATCH 335/351] Date Picker [LG-3897] backspace (#2143) * add tests * move test --- .../src/DatePicker/DatePicker.spec.tsx | 94 ++++++++++++++----- .../DatePickerInput/DatePickerInput.tsx | 9 +- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index b60b7a66b0..57f9a24f4c 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1250,6 +1250,26 @@ describe('packages/date-picker', () => { */ describe('Arrow key', () => { describe('Input', () => { + describe('Left Arrow', () => { + test('moves the cursor when the value starts with 0', () => { + const { monthInput } = renderDatePicker({}); + userEvent.type(monthInput, '04{arrowleft}{arrowleft}'); + expect(monthInput).toHaveFocus(); + }); + + test('moves the cursor when the value is 0', () => { + const { monthInput } = renderDatePicker({}); + userEvent.type(monthInput, '0{arrowleft}'); + expect(monthInput).toHaveFocus(); + }); + + test('moves the cursor to the next segment when the value is 0', () => { + const { yearInput, monthInput } = renderDatePicker({}); + userEvent.type(monthInput, '0{arrowleft}{arrowleft}'); + expect(yearInput).toHaveFocus(); + }); + }); + test('right arrow moves focus through segments', () => { const { yearInput, monthInput, dayInput } = renderDatePicker(); userEvent.click(yearInput); @@ -2122,37 +2142,59 @@ describe('packages/date-picker', () => { }); describe('updating a segment', () => { - test('clearing the segment updates the input', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker({}); - userEvent.type(yearInput, '2020'); - userEvent.type(monthInput, '7'); - userEvent.type(dayInput, '4'); + describe('backspace', () => { + test('clearing the segment updates the input', () => { + const { yearInput, monthInput, dayInput } = renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '7'); + userEvent.type(dayInput, '4'); - yearInput.setSelectionRange(0, 4); - userEvent.type(yearInput, '{backspace}'); - expect(yearInput).toHaveValue(''); - }); + yearInput.setSelectionRange(0, 4); + userEvent.type(yearInput, '{backspace}'); + expect(yearInput).toHaveValue(''); + }); - test('clearing and typing a new value does not format the input', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker({}); - userEvent.type(yearInput, '2020'); - userEvent.type(monthInput, '7'); - userEvent.type(dayInput, '4'); + test('keeps the focus inside the segment if it is not empty', () => { + const { monthInput } = renderDatePicker({}); - yearInput.setSelectionRange(0, 4); - userEvent.type(yearInput, '{backspace}'); - userEvent.type(yearInput, '2'); - expect(yearInput).toHaveValue('2'); - }); + userEvent.type(monthInput, '0'); + userEvent.type(monthInput, '{backspace}'); - test('deleting characters does not format the segment', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker({}); - userEvent.type(yearInput, '2020'); - userEvent.type(monthInput, '7'); - userEvent.type(dayInput, '4'); + expect(monthInput).toHaveValue(''); + expect(monthInput).toHaveFocus(); + }); + + test('moves the focus to the next segment', () => { + const { yearInput, monthInput } = renderDatePicker({}); + + userEvent.type(monthInput, '0'); + userEvent.type(monthInput, '{backspace}{backspace}'); + + expect(monthInput).toHaveValue(''); + expect(yearInput).toHaveFocus(); + }); - userEvent.type(yearInput, '{backspace}{backspace}'); - expect(yearInput).toHaveValue('20'); + test('clearing and typing a new value does not format the input', () => { + const { yearInput, monthInput, dayInput } = renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '7'); + userEvent.type(dayInput, '4'); + + yearInput.setSelectionRange(0, 4); + userEvent.type(yearInput, '{backspace}'); + userEvent.type(yearInput, '2'); + expect(yearInput).toHaveValue('2'); + }); + + test('deleting characters does not format the segment', () => { + const { yearInput, monthInput, dayInput } = renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '7'); + userEvent.type(dayInput, '4'); + + userEvent.type(yearInput, '{backspace}{backspace}'); + expect(yearInput).toHaveValue('20'); + }); }); describe('typing new characters', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index c676f3f3e9..b0bb090d07 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -7,7 +7,6 @@ import React, { } from 'react'; import { isSameUTCDay } from '@leafygreen-ui/date-utils'; -import { isZeroLike } from '@leafygreen-ui/lib'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; @@ -88,7 +87,7 @@ export const DatePickerInput = forwardRef( // if target is not a segment, do nothing if (!isSegment) return; - const isSegmentEmpty = isZeroLike(target.value); + const isSegmentEmpty = !target.value; const { selectionStart, selectionEnd } = target; @@ -97,7 +96,7 @@ export const DatePickerInput = forwardRef( // if input is empty, // or the cursor is at the beginning of the input // set focus to prev. input (if it exists) - if (isSegmentEmpty || selectionStart === 0) { + if (selectionStart === 0) { const segmentToFocus = getRelativeSegmentRef('prev', { segment: target, formatParts, @@ -115,7 +114,7 @@ export const DatePickerInput = forwardRef( // if input is empty, // or the cursor is at the end of the input // set focus to next. input (if it exists) - if (isSegmentEmpty || selectionEnd === target.value.length) { + if (selectionEnd === target.value.length) { const segmentToFocus = getRelativeSegmentRef('next', { segment: target, formatParts, @@ -137,6 +136,8 @@ export const DatePickerInput = forwardRef( case keyMap.Backspace: { if (isSegmentEmpty) { + // prevent the backspace in the previous segment + e.preventDefault(); const segmentToFocus = getRelativeSegmentRef('prev', { segment: target, formatParts, From 9335adaa894af61cecce11ec2119deb21211745d Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:31:34 -0500 Subject: [PATCH 336/351] Date picker: restructure ixn tests (#2142) * removes interaction tests from Menu * Moves interaction tests to DatePicker.spec * moves arrow key tests from Input to DatePicker * Update DatePicker.spec.tsx * adds test for typing an invalid date * Update DatePicker.spec.tsx * test up/down input arrow keys on each segment * fix year rollover tests * queryByTestId * Add tests for arrow up/down errors * adds utils * adds error state persistence tests --- .../src/DatePicker/DatePicker.spec.tsx | 1450 ++++++++++++----- .../DatePickerInput/DatePickerInput.spec.tsx | 159 +- .../DatePickerMenu/DatePickerMenu.spec.tsx | 355 +--- .../doSegmentsFormValidDate.ts | 21 + .../utils/doSegmentsFormValidDate/index.ts | 1 + .../getFormattedDateString.ts | 9 + .../getFormattedDateStringFromSegments.ts | 25 + .../utils/getFormattedDateString/index.ts | 22 +- .../date-picker/src/shared/utils/index.ts | 8 +- .../utils/isEverySegmentFilled/index.ts | 1 + .../isEverySegmentFilled.ts | 7 + .../shared/utils/isEverySegmentValid/index.ts | 1 + .../isEverySegmentValid.ts | 11 + 13 files changed, 1162 insertions(+), 908 deletions(-) create mode 100644 packages/date-picker/src/shared/utils/doSegmentsFormValidDate/doSegmentsFormValidDate.ts create mode 100644 packages/date-picker/src/shared/utils/doSegmentsFormValidDate/index.ts create mode 100644 packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.ts create mode 100644 packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateStringFromSegments.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentFilled/index.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentFilled/isEverySegmentFilled.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentValid/index.ts create mode 100644 packages/date-picker/src/shared/utils/isEverySegmentValid/isEverySegmentValid.ts diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 57f9a24f4c..cde3711fa5 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -9,13 +9,7 @@ import { import userEvent from '@testing-library/user-event'; import { addDays, subDays } from 'date-fns'; -import { - getISODate, - Month, - newUTC, - setUTCMonth, - setUTCYear, -} from '@leafygreen-ui/date-utils'; +import { getISODate, Month, newUTC } from '@leafygreen-ui/date-utils'; import { mockTimeZone, testTimeZones, @@ -27,8 +21,13 @@ import { } from '@leafygreen-ui/testing-lib'; import { transitionDuration } from '@leafygreen-ui/tokens'; +import { DateSegment } from '../shared'; import { defaultMax, defaultMin } from '../shared/constants'; -import { getFormattedDateString, getValueFormatter } from '../shared/utils'; +import { + getFormattedDateString, + getFormattedSegmentsFromDate, + getValueFormatter, +} from '../shared/utils'; import { expectedTabStopLabels, @@ -39,6 +38,20 @@ import { } from './DatePicker.testutils'; import { DatePicker } from '.'; +/** + * There are HUNDREDS of tests for this component. + * To keep things organized we've attempted to adopt the following testing philosophy. + * + * Rendering Tests: + * Tests that assert that certain elements are rendered to the DOM. + * These tests should not have any user interaction (except when absolutely necessary to arrive in a certain state) + * These tests should exist on each sub-component to simplify test suites + * + * Interaction tests: + * Tests that assert some behavior following user interaction. + * Generally, this type of tests should _only_ exist in a test file for user-facing components. + */ + // Set the current time to noon UTC on 2023-12-25 const testToday = newUTC(2023, Month.December, 25, 12); @@ -152,18 +165,16 @@ describe('packages/date-picker', () => { expect(yearInput.value).toEqual('2023'); }); - describe('console', () => { - test('console warning when no labels are passed in', () => { - const consoleSpy = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); + test('console warning when no labels are passed in', () => { + const consoleSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); - /* @ts-expect-error - needs label/aria-label/aria-labelledby */ - render(); - expect(consoleSpy).toHaveBeenCalledWith( - 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', - ); - }); + /* @ts-expect-error - needs label/aria-label/aria-labelledby */ + render(); + expect(consoleSpy).toHaveBeenCalledWith( + 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', + ); }); describe('Error states', () => { @@ -203,13 +214,13 @@ describe('packages/date-picker', () => { }); test('renders with internal error state when value is out of range', () => { - const { getByTestId, getByRole } = render( + const { queryByTestId, getByRole } = render( , ); const inputContainer = getByRole('combobox'); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - const errorElement = getByTestId('lg-form_field-error_message'); + const errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toBeInTheDocument(); expect(errorElement).toHaveTextContent( 'Date must be before 2038-01-19', @@ -217,7 +228,7 @@ describe('packages/date-picker', () => { }); test('external error message overrides internal error message', () => { - const { getByTestId, getByRole } = renderDatePicker({ + const { queryByTestId, getByRole } = renderDatePicker({ value: newUTC(2100, 1, 1), state: 'error', errorMessage: 'Custom error message', @@ -225,54 +236,54 @@ describe('packages/date-picker', () => { const inputContainer = getByRole('combobox'); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - const errorElement = getByTestId('lg-form_field-error_message'); + const errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toBeInTheDocument(); expect(errorElement).toHaveTextContent('Custom error message'); }); test('renders internal message if external error message is not set', () => { - const { inputContainer, getByTestId } = renderDatePicker({ + const { inputContainer, queryByTestId } = renderDatePicker({ value: newUTC(2100, 1, 1), state: 'error', errorMessage: undefined, }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( - 'Date must be before 2038-01-19', - ); + expect( + queryByTestId('lg-form_field-error_message'), + ).toHaveTextContent('Date must be before 2038-01-19'); }); test('removing an external error displays an internal error when applicable', () => { - const { inputContainer, rerenderDatePicker, getByTestId } = + const { inputContainer, rerenderDatePicker, queryByTestId } = renderDatePicker({ value: newUTC(2100, Month.January, 1), }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( - 'Date must be before 2038-01-19', - ); + expect( + queryByTestId('lg-form_field-error_message'), + ).toHaveTextContent('Date must be before 2038-01-19'); rerenderDatePicker({ errorMessage: 'Some error', state: 'error' }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( - 'Some error', - ); + expect( + queryByTestId('lg-form_field-error_message'), + ).toHaveTextContent('Some error'); rerenderDatePicker({ state: 'none' }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - expect(getByTestId('lg-form_field-error_message')).toHaveTextContent( - 'Date must be before 2038-01-19', - ); + expect( + queryByTestId('lg-form_field-error_message'), + ).toHaveTextContent('Date must be before 2038-01-19'); }); test('internal error message updates when min value changes', () => { - const { inputContainer, rerenderDatePicker, getByTestId } = + const { inputContainer, rerenderDatePicker, queryByTestId } = renderDatePicker({ value: newUTC(1967, Month.March, 10), }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - const errorElement = getByTestId('lg-form_field-error_message'); + const errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toHaveTextContent( 'Date must be after 1970-01-01', ); @@ -285,12 +296,12 @@ describe('packages/date-picker', () => { }); test('internal error message updates when max value changes', () => { - const { inputContainer, rerenderDatePicker, getByTestId } = + const { inputContainer, rerenderDatePicker, queryByTestId } = renderDatePicker({ value: newUTC(2050, Month.January, 1), }); expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); - const errorElement = getByTestId('lg-form_field-error_message'); + const errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toHaveTextContent( 'Date must be before 2038-01-19', ); @@ -1241,16 +1252,51 @@ describe('packages/date-picker', () => { }); }); + describe('Backspace key', () => { + test('deletes any value in the input', () => { + const { dayInput } = renderDatePicker(); + userEvent.type(dayInput, '26{backspace}'); + expect(dayInput.value).toBe('2'); + userEvent.tab(); + expect(dayInput.value).toBe('02'); + }); + + test('deletes the whole value on multiple presses', () => { + const { monthInput } = renderDatePicker(); + userEvent.type(monthInput, '11'); + userEvent.type(monthInput, '{backspace}{backspace}'); + expect(monthInput.value).toBe(''); + }); + + test('focuses the previous segment if current segment is empty', () => { + const { yearInput, monthInput } = renderDatePicker(); + userEvent.type(monthInput, '{backspace}'); + expect(yearInput).toHaveFocus(); + }); + }); + /** - * Arrow Keys: - * Since arrow key behavior changes based on whether the input or menu is focused, - * more detailed tests suites are located in - * - DatePickerInput: (./DatePickerInput/DatePickerInput.spec.tsx) and - * - DatePickerMenu: (./DatePickerMenu/DatePickerMenu.spec.tsx) + * Arrow Keys behavior changes based on whether the input or menu is focused */ describe('Arrow key', () => { describe('Input', () => { describe('Left Arrow', () => { + test('focuses the previous segment when the segment is empty', () => { + const { yearInput, monthInput } = renderDatePicker(); + userEvent.click(monthInput); + userEvent.keyboard('{arrowleft}'); + expect(yearInput).toHaveFocus(); + }); + + test('moves the cursor when the segment has a value', () => { + const { monthInput } = renderDatePicker({ + value: testToday, + }); + userEvent.click(monthInput); + userEvent.keyboard('{arrowleft}'); + expect(monthInput).toHaveFocus(); + }); + test('moves the cursor when the value starts with 0', () => { const { monthInput } = renderDatePicker({}); userEvent.type(monthInput, '04{arrowleft}{arrowleft}'); @@ -1263,403 +1309,1003 @@ describe('packages/date-picker', () => { expect(monthInput).toHaveFocus(); }); - test('moves the cursor to the next segment when the value is 0', () => { + test('moves the cursor to the previous segment when the value is 0', () => { const { yearInput, monthInput } = renderDatePicker({}); userEvent.type(monthInput, '0{arrowleft}{arrowleft}'); expect(yearInput).toHaveFocus(); }); - }); - - test('right arrow moves focus through segments', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker(); - userEvent.click(yearInput); - userEvent.keyboard('{arrowright}'); - expect(monthInput).toHaveFocus(); - - userEvent.keyboard('{arrowright}'); - expect(dayInput).toHaveFocus(); - }); - - test('left arrow moves focus back through segments', () => { - const { yearInput, monthInput, dayInput } = renderDatePicker(); - userEvent.click(dayInput); - userEvent.keyboard('{arrowleft}'); - expect(monthInput).toHaveFocus(); - userEvent.keyboard('{arrowleft}'); - expect(yearInput).toHaveFocus(); + test('focuses the previous segment if the cursor is at the start of the input text', () => { + const { yearInput, monthInput } = renderDatePicker({ + value: testToday, + }); + userEvent.click(monthInput); + userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}'); + expect(yearInput).toHaveFocus(); + }); }); - describe('Up Arrow', () => { - describe('month input', () => { - const formatter = getValueFormatter('month'); - test('updates segment value to the default min', () => { - const { monthInput } = renderDatePicker(); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowup}`); - - expect(monthInput).toHaveValue(formatter(defaultMin.month)); - }); + describe('Right Arrow', () => { + test('focuses the next segment when the segment is empty', () => { + const { yearInput, monthInput } = renderDatePicker(); + userEvent.click(yearInput); + userEvent.keyboard('{arrowright}'); + expect(monthInput).toHaveFocus(); + }); - test(`fires segment change handler`, () => { - const onChange = jest.fn(); - const { monthInput } = renderDatePicker({ onChange }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowup}`); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue(formatter(defaultMin.month)), - ); + test('focuses the next segment if the cursor is at the start of the input text', () => { + const { yearInput, monthInput } = renderDatePicker({ + value: testToday, }); + userEvent.click(yearInput); + userEvent.keyboard('{arrowright}'); + expect(monthInput).toHaveFocus(); + }); - test(`does not fire value change handler`, () => { - const onDateChange = jest.fn(); - const { monthInput } = renderDatePicker({ onDateChange }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowup}`); - expect(onDateChange).not.toHaveBeenCalled(); + test('moves the cursor when the segment has a value', () => { + const { yearInput } = renderDatePicker({ + value: testToday, }); + userEvent.click(yearInput); + userEvent.keyboard('{arrowleft}{arrowright}'); + expect(yearInput).toHaveFocus(); }); + }); - describe('year input', () => { - const formatter = getValueFormatter('year'); - - test('updates segment value to the default min', () => { - const { yearInput } = renderDatePicker(); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowup}`); + const segmentCases = ['year', 'month', 'day'] as Array; + describe.each(segmentCases)('%p segment', segment => { + const formatter = getValueFormatter(segment); + /** Utility only for this suite. Returns the day|month|year element from the render result */ + const getRelevantInput = (renderResult: RenderDatePickerResult) => + segment === 'year' + ? renderResult.yearInput + : segment === 'month' + ? renderResult.monthInput + : renderResult.dayInput; + + describe('Up Arrow', () => { + describe('when no value has been set', () => { + test('keeps the focus in the current segment', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + expect(input).toHaveFocus(); + }); - expect(yearInput).toHaveValue(formatter(defaultMin.year)); - }); + test('updates segment value to the default min', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + const expectedValue = formatter(defaultMin[segment]); + expect(input).toHaveValue(expectedValue); + }); - test('updates segment value to the provided min year', () => { - const { yearInput } = renderDatePicker({ - min: newUTC(1969, Month.June, 20), + test('updates segment value to the provided min year', () => { + const result = renderDatePicker({ + min: newUTC(1967, Month.March, 10), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + const expectedValue = formatter( + segment === 'year' ? 1967 : defaultMin[segment], + ); + expect(input).toHaveValue(expectedValue); }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowup}`); - expect(yearInput).toHaveValue(formatter(1969)); - }); + test('keeps the focus in the current segment even if the value is valid', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}{arrowup}{arrowup}'); + expect(input).toHaveFocus(); + const expectedValue = formatter(defaultMin[segment] + 2); + expect(input).toHaveValue(expectedValue); + }); - test(`fires segment change handler`, () => { - const onChange = jest.fn(); - const { yearInput } = renderDatePicker({ onChange }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowup}`); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue(formatter(defaultMin.year)), - ); - }); + test(`fires segment change handler`, () => { + const onChange = jest.fn(); + const result = renderDatePicker({ onChange }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + const expectedValue = formatter(defaultMin[segment]); + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue(expectedValue), + ); + }); - test(`does not fire value change handler`, () => { - const onDateChange = jest.fn(); - const { yearInput } = renderDatePicker({ onDateChange }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowup}`); - expect(onDateChange).not.toHaveBeenCalled(); - }); - }); + test(`does not fire value change handler`, () => { + const onDateChange = jest.fn(); + const result = renderDatePicker({ onDateChange }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + expect(onDateChange).not.toHaveBeenCalled(); + }); - describe('when a value is set', () => { - test('fires value change handler', () => { - const onDateChange = jest.fn(); - const { monthInput } = renderDatePicker({ - onDateChange, - value: testToday, + describe('when segment value is max', () => { + if (segment === 'year') { + test('does not roll over to the min value', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + const initialValue = formatter(defaultMax[segment]); + const expectedValue = formatter(defaultMax[segment] + 1); + userEvent.type(input, initialValue); + expect(input).toHaveValue(initialValue); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + expect(input).toHaveValue(expectedValue); + }); + } else { + test('rolls over to the min value', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + const initialValue = formatter(defaultMax[segment]); + const expectedValue = formatter(defaultMin[segment]); + userEvent.type(input, initialValue); + expect(input).toHaveValue(initialValue); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + expect(input).toHaveValue(expectedValue); + }); + } }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowup}`); - expect(onDateChange).toHaveBeenCalled(); }); - test('does not roll over year', () => { - const onDateChange = jest.fn(); - const { yearInput } = renderDatePicker({ - onDateChange, - value: newUTC(2020, Month.July, 5), - min: newUTC(1969, Month.June, 20), - max: newUTC(2020, Month.September, 10), + describe('when a value is set', () => { + describe('when the value is valid', () => { + const onDateChange = jest.fn(); + const handleValidation = jest.fn(); + const initialValue = newUTC(2023, Month.September, 10); + const expectedValue = { + year: newUTC(2024, Month.September, 10), + month: newUTC(2023, Month.October, 10), + day: newUTC(2023, Month.September, 11), + }[segment]; + + beforeEach(() => { + const result = renderDatePicker({ + onDateChange, + handleValidation, + value: initialValue, + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + }); + + test('fires value change handler', () => { + expect(onDateChange).toHaveBeenCalledWith(expectedValue); + }); + + test('fires validation handler', () => { + expect(handleValidation).toHaveBeenCalledWith( + expectedValue, + ); + }); }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowup}`); - expect(onDateChange).toHaveBeenCalledWith( - newUTC(2021, Month.July, 5), - ); - }); - describe('if new value would be out of range', () => { - const onDateChange = jest.fn(); - const onSegmentChange = jest.fn(); - const handleValidation = jest.fn(); - const max = newUTC(2020, Month.July, 4); - const startValue = newUTC(2019, Month.August, 1); - const newYearVal = '2020'; - const expectedMessage = `Date must be before ${getFormattedDateString( - max, - 'iso8601', - )}`; + describe('if the new value would be invalid', () => { + // E.g. Feb 30 2020 or Feb 29 2021 + switch (segment) { + case 'year': { + test('changing year sets error state', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 29), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2021'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('29'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); - let renderResult: RenderDatePickerResult; + break; + } + + case 'month': { + test('changing month sets error state', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.January, 31), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2020'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('31'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); - beforeEach(() => { - onDateChange.mockReset(); - onSegmentChange.mockReset(); - handleValidation.mockReset(); + test('error state stays after menu is closed', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 29), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + userEvent.click(result.container.parentElement!); + await waitFor(() => { + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); - renderResult = renderDatePicker({ + break; + } + + case 'day': { + test('changing date rolls value over sooner', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 29), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowup}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2020'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('01'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'false', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).not.toBeInTheDocument(); + }); + }); + break; + } + + default: + break; + } + }); + + describe('if new value would be out of range', () => { + const onDateChange = jest.fn(); + const onSegmentChange = jest.fn(); + const handleValidation = jest.fn(); + const max = newUTC(2020, Month.August, 1); + const startValue = newUTC(2020, Month.August, 1); + const incrementedValues = { + year: newUTC(2021, Month.August, 1), + month: newUTC(2020, Month.September, 1), + day: newUTC(2020, Month.August, 2), + }; + const expectedMessage = `Date must be before ${getFormattedDateString( max, - value: startValue, - onDateChange, - onChange: onSegmentChange, - handleValidation, + 'iso8601', + )}`; + + let renderResult: RenderDatePickerResult; + let input: HTMLInputElement; + + beforeEach(() => { + onDateChange.mockReset(); + onSegmentChange.mockReset(); + handleValidation.mockReset(); + + renderResult = renderDatePicker({ + max, + value: startValue, + onDateChange, + onChange: onSegmentChange, + handleValidation, + }); + + input = getRelevantInput(renderResult); + + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + }); + + test('fires the segment change handler', () => { + const expectedInputValue = getFormattedSegmentsFromDate( + incrementedValues[segment], + )[segment]; + + expect(onSegmentChange).toHaveBeenCalledWith( + eventContainingTargetValue(expectedInputValue), + ); + }); + + test('updates the input', () => { + const expectedInputValue = getFormattedSegmentsFromDate( + incrementedValues[segment], + )[segment]; + + expect(input).toHaveValue(expectedInputValue); + }); + + test('fires the change handler', () => { + expect(onDateChange).toHaveBeenCalledWith( + incrementedValues[segment], + ); + }); + + test('fires the validation handler', () => { + expect(handleValidation).toHaveBeenCalledWith( + incrementedValues[segment], + ); + }); + + test('sets aria-invalid', () => { + expect(renderResult.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + }); + + test('sets error message', () => { + const errorMessageElement = within( + renderResult.formField, + ).queryByText(expectedMessage); + expect(errorMessageElement).toBeInTheDocument(); }); - userEvent.click(renderResult.yearInput); - userEvent.keyboard(`{arrowup}`); }); + }); + }); - test('updates the input', () => { - expect(renderResult.yearInput).toHaveValue(newYearVal); + describe('Down Arrow', () => { + describe('when no value has been set', () => { + test('keeps the focus in the current segment', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + expect(input).toHaveFocus(); }); - test('fires the change handler', () => { - expect(onDateChange).toHaveBeenCalledWith( - setUTCYear(startValue, Number(newYearVal)), - ); + test('updates segment value to the default max', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowdown}`); + const expectedValue = formatter(defaultMax[segment]); + expect(input).toHaveValue(expectedValue); }); - test('fires the segment change handler', () => { - expect(onSegmentChange).toHaveBeenCalledWith( - eventContainingTargetValue(newYearVal), + test('updates segment value to the provided max year', () => { + const result = renderDatePicker({ + max: newUTC(2067, Month.March, 10), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowdown}`); + const expectedValue = formatter( + segment === 'year' ? 2067 : defaultMax[segment], ); + expect(input).toHaveValue(expectedValue); }); - test('fires the validation handler', () => { - expect(handleValidation).toHaveBeenCalledWith( - setUTCYear(startValue, Number(newYearVal)), - ); + test('keeps the focus in the current segment even if the value is valid', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}'); + expect(input).toHaveFocus(); + const expectedValue = formatter(defaultMax[segment] - 2); + expect(input).toHaveValue(expectedValue); }); - test('sets aria-invalid', () => { - expect(renderResult.inputContainer).toHaveAttribute( - 'aria-invalid', - 'true', + test(`fires segment change handler`, () => { + const onChange = jest.fn(); + const result = renderDatePicker({ onChange }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowdown}`); + const expectedValue = formatter(defaultMax[segment]); + expect(onChange).toHaveBeenCalledWith( + eventContainingTargetValue(expectedValue), ); }); - test('sets error message', () => { - const errorMessageElement = within( - renderResult.formField, - ).queryByText(expectedMessage); - expect(errorMessageElement).toBeInTheDocument(); + test(`does not fire value change handler`, () => { + const onDateChange = jest.fn(); + const result = renderDatePicker({ onDateChange }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowdown}`); + expect(onDateChange).not.toHaveBeenCalled(); + }); + + describe('when segment value is min', () => { + if (segment === 'year') { + test('does not roll over to the max value', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + const initialValue = formatter(defaultMin[segment]); + const expectedValue = formatter(defaultMin[segment] - 1); + userEvent.type(input, initialValue); + expect(input).toHaveValue(initialValue); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + expect(input).toHaveValue(expectedValue); + }); + } else { + test('rolls over to the max value', () => { + const result = renderDatePicker(); + const input = getRelevantInput(result); + const initialValue = formatter(defaultMin[segment]); + const expectedValue = formatter(defaultMax[segment]); + userEvent.type(input, initialValue); + expect(input).toHaveValue(initialValue); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + expect(input).toHaveValue(expectedValue); + }); + } + }); + }); + + describe('when a value is set', () => { + describe('when the value is valid', () => { + const onDateChange = jest.fn(); + const handleValidation = jest.fn(); + const initialValue = newUTC(2023, Month.September, 10); + const expectedValue = { + year: newUTC(2022, Month.September, 10), + month: newUTC(2023, Month.August, 10), + day: newUTC(2023, Month.September, 9), + }[segment]; + + beforeEach(() => { + const result = renderDatePicker({ + onDateChange, + handleValidation, + value: initialValue, + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard(`{arrowdown}`); + }); + + test('fires value change handler', () => { + expect(onDateChange).toHaveBeenCalledWith(expectedValue); + }); + + test('fires validation handler', () => { + expect(handleValidation).toHaveBeenCalledWith( + expectedValue, + ); + }); + }); + + // TODO: + describe('if the new value would be invalid', () => { + // E.g. Feb 30 2020 or Feb 29 2021 + switch (segment) { + case 'year': { + test('changing year sets error state', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 29), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2019'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('29'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); + + break; + } + + case 'month': { + test('changing month sets error state', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.March, 31), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2020'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('31'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); + + test('error state stays after menu is closed', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 29), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + userEvent.click(result.container.parentElement!); + await waitFor(() => { + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); + }); + }); + + break; + } + + case 'day': { + // There is no case where decrementing a day results in an invalid date value + break; + } + + default: + break; + } + }); + + describe('if new value would be out of range', () => { + const onDateChange = jest.fn(); + const onSegmentChange = jest.fn(); + const handleValidation = jest.fn(); + const max = newUTC(2020, Month.August, 1); + const startValue = newUTC(2020, Month.August, 1); + const incrementedValues = { + year: newUTC(2021, Month.August, 1), + month: newUTC(2020, Month.September, 1), + day: newUTC(2020, Month.August, 2), + }; + const expectedMessage = `Date must be before ${getFormattedDateString( + max, + 'iso8601', + )}`; + + let renderResult: RenderDatePickerResult; + let input: HTMLInputElement; + + beforeEach(() => { + onDateChange.mockReset(); + onSegmentChange.mockReset(); + handleValidation.mockReset(); + + renderResult = renderDatePicker({ + max, + value: startValue, + onDateChange, + onChange: onSegmentChange, + handleValidation, + }); + + input = getRelevantInput(renderResult); + + userEvent.click(input); + userEvent.keyboard(`{arrowup}`); + }); + + test('fires the segment change handler', () => { + const expectedInputValue = getFormattedSegmentsFromDate( + incrementedValues[segment], + )[segment]; + + expect(onSegmentChange).toHaveBeenCalledWith( + eventContainingTargetValue(expectedInputValue), + ); + }); + + test('updates the input', () => { + const expectedInputValue = getFormattedSegmentsFromDate( + incrementedValues[segment], + )[segment]; + + expect(input).toHaveValue(expectedInputValue); + }); + + test('fires the change handler', () => { + expect(onDateChange).toHaveBeenCalledWith( + incrementedValues[segment], + ); + }); + + test('fires the validation handler', () => { + expect(handleValidation).toHaveBeenCalledWith( + incrementedValues[segment], + ); + }); + + test('sets aria-invalid', () => { + expect(renderResult.inputContainer).toHaveAttribute( + 'aria-invalid', + 'true', + ); + }); + + test('sets error message', () => { + const errorMessageElement = within( + renderResult.formField, + ).queryByText(expectedMessage); + expect(errorMessageElement).toBeInTheDocument(); + }); }); }); }); }); + }); - describe('Down Arrow', () => { - describe('month input', () => { - const formatter = getValueFormatter('month'); + describe('Menu', () => { + beforeEach(() => { + jest.setSystemTime(testToday); + mockTimeZone('America/New_York', -5); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); - test('updates segment value to the default max', () => { - const { monthInput } = renderDatePicker(); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowdown}`); + describe('basic arrow key behavior', () => { + let menuResult: RenderMenuResult; - expect(monthInput).toHaveValue(formatter(defaultMax.month)); + beforeEach(async () => { + const renderResult = renderDatePicker({ + value: newUTC(2023, Month.September, 10), }); + menuResult = await renderResult.openMenu(); + }); - test(`fires segment change handler`, () => { - const onChange = jest.fn(); - const { monthInput } = renderDatePicker({ onChange }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowdown}`); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue(formatter(defaultMax.month)), - ); - }); + test('left arrow moves focus to the previous day', async () => { + const { queryCellByISODate } = menuResult; + userEvent.keyboard('{arrowleft}'); + const prevDay = queryCellByISODate('2023-09-09'); + await waitFor(() => expect(prevDay).toHaveFocus()); + }); - test(`does not fire value change handler`, () => { - const onDateChange = jest.fn(); - const { monthInput } = renderDatePicker({ onDateChange }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowdown}`); - expect(onDateChange).not.toHaveBeenCalled(); - }); + test('right arrow moves focus to the next day', async () => { + const { queryCellByISODate } = menuResult; + userEvent.keyboard('{arrowright}'); + const nextDay = queryCellByISODate('2023-09-11'); + await waitFor(() => expect(nextDay).toHaveFocus()); + }); + + test('up arrow moves focus to the previous week', async () => { + const { queryCellByISODate } = menuResult; + userEvent.keyboard('{arrowup}'); + const prevWeek = queryCellByISODate('2023-09-03'); + await waitFor(() => expect(prevWeek).toHaveFocus()); }); - describe('year input', () => { - const formatter = getValueFormatter('year'); + test('down arrow moves focus to the next week', async () => { + const { queryCellByISODate } = menuResult; + userEvent.keyboard('{arrowdown}'); + const nextWeek = queryCellByISODate('2023-09-17'); + await waitFor(() => expect(nextWeek).toHaveFocus()); + }); + }); + + describe('when switching between daylight savings and standard time', () => { + // DST: Sun, Mar 12, 2023 – Sun, Nov 5, 2023 - test('updates segment value to the default max', () => { - const { yearInput } = renderDatePicker(); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowdown}`); + const standardTimeEndDate = newUTC(2023, Month.March, 11, 22); + const weekBeforeDTStart = newUTC(2023, Month.March, 5, 22); + const daylightTimeStartDate = newUTC(2023, Month.March, 12, 22); + const daylightTimeEndDate = newUTC(2023, Month.November, 5, 22); + const weekAfterDTEnd = newUTC(2023, Month.November, 12, 22); + const standardTimeStartDate = newUTC(2023, Month.November, 6, 22); - expect(yearInput).toHaveValue(formatter(defaultMax.year)); + describe('DST start (Mar 12 2023)', () => { + test('left arrow moves focus to prev day', async () => { + jest.setSystemTime(daylightTimeStartDate); // Mar 12 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + const currentDayCell = queryCellByISODate('2023-03-12'); // Mar 12 + await waitFor(() => expect(currentDayCell).toHaveFocus()); + + userEvent.keyboard('{arrowleft}'); + const prevDayCell = queryCellByISODate('2023-03-11'); // Mar 11 + await waitFor(() => expect(prevDayCell).toHaveFocus()); }); - test('updates segment value to the provided min year', () => { - const { yearInput } = renderDatePicker({ - max: newUTC(2020, Month.March, 13), - }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowdown}`); + test('right arrow moves focus to next day', async () => { + jest.setSystemTime(standardTimeEndDate); // Mar 11 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + const currentDayCell = queryCellByISODate('2023-03-11'); // Mar 11 + await waitFor(() => expect(currentDayCell).toHaveFocus()); - expect(yearInput).toHaveValue(formatter(2020)); + userEvent.keyboard('{arrowright}'); + const nextDayCell = queryCellByISODate('2023-03-12'); // Mar 12 + await waitFor(() => expect(nextDayCell).toHaveFocus()); }); - test(`fires segment change handler`, () => { - const onChange = jest.fn(); - const { yearInput } = renderDatePicker({ onChange }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowdown}`); - expect(onChange).toHaveBeenCalledWith( - eventContainingTargetValue(formatter(defaultMax.year)), - ); + test('up arrow moves focus to the previous week', async () => { + jest.setSystemTime(daylightTimeStartDate); // Mar 12 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowup}'); + const prevWeekCell = queryCellByISODate('2023-03-05'); // Mar 5 + await waitFor(() => expect(prevWeekCell).toHaveFocus()); }); - test(`does not fire value change handler`, () => { - const onDateChange = jest.fn(); - const { yearInput } = renderDatePicker({ onDateChange }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowdown}`); - expect(onDateChange).not.toHaveBeenCalled(); + test('down arrow moves focus to the next week', async () => { + jest.setSystemTime(weekBeforeDTStart); // Mar 5 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowdown}'); + const nextWeekCell = queryCellByISODate('2023-03-12'); // Mar 12 + await waitFor(() => expect(nextWeekCell).toHaveFocus()); }); }); - describe('when a value is set', () => { - test('fires value change handler', () => { - const onDateChange = jest.fn(); - const testVal = newUTC(2023, Month.September, 10); - const { monthInput } = renderDatePicker({ - onDateChange, - value: testVal, - }); - userEvent.click(monthInput); - userEvent.keyboard(`{arrowdown}`); - expect(onDateChange).toHaveBeenCalledWith( - setUTCMonth(testVal, testVal.getUTCMonth() - 1), - ); - }); + describe('DST end (Nov 5 2023)', () => { + test('left arrow moves focus to prev day', async () => { + jest.setSystemTime(standardTimeStartDate); // Nov 6 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowleft}'); + const prevDayCell = queryCellByISODate('2023-11-05'); // Nov 5 - test('does not roll over year', () => { - const onDateChange = jest.fn(); - const { yearInput } = renderDatePicker({ - onDateChange, - value: newUTC(1969, Month.July, 5), - min: newUTC(1969, Month.June, 20), - max: newUTC(2020, Month.September, 10), - }); - userEvent.click(yearInput); - userEvent.keyboard(`{arrowdown}`); - expect(onDateChange).toHaveBeenCalledWith( - newUTC(1968, Month.July, 5), - ); + await waitFor(() => expect(prevDayCell).toHaveFocus()); }); - describe('if new value would be out of range', () => { - const onDateChange = jest.fn(); - const onSegmentChange = jest.fn(); - const handleValidation = jest.fn(); - const min = newUTC(1999, Month.November, 11); - const startValue = newUTC(2000, Month.August, 1); - const newYearVal = '1999'; + test('right arrow moves focus to next day', async () => { + jest.setSystemTime(daylightTimeEndDate); // Nov 5 - const expectedMessage = `Date must be after ${getFormattedDateString( - min, - 'iso8601', - )}`; + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowright}'); - let renderResult: RenderDatePickerResult; + const nextDayCell = queryCellByISODate('2023-11-06'); // Nov 6 + await waitFor(() => expect(nextDayCell).toHaveFocus()); + }); - beforeEach(() => { - onDateChange.mockReset(); - onSegmentChange.mockReset(); - handleValidation.mockReset(); - - renderResult = renderDatePicker({ - min, - value: startValue, - onDateChange, - onChange: onSegmentChange, - handleValidation, - }); - userEvent.click(renderResult.yearInput); - userEvent.keyboard(`{arrowdown}`); - }); + test('up arrow moves focus to the previous week', async () => { + jest.setSystemTime(weekAfterDTEnd); // Nov 12 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowup}'); - test('updates the input', () => { - expect(renderResult.yearInput).toHaveValue(newYearVal); - }); + const prevWeekCell = queryCellByISODate('2023-11-05'); // Nov 5 + await waitFor(() => expect(prevWeekCell).toHaveFocus()); + }); - test('fires the change handler', () => { - expect(onDateChange).toHaveBeenCalledWith( - setUTCYear(startValue, Number(newYearVal)), - ); - }); + test('down arrow moves focus to the next week', async () => { + jest.setSystemTime(daylightTimeEndDate); // Nov 5 + const { openMenu } = renderDatePicker(); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowdown}'); - test('fires the segment change handler', () => { - expect(onSegmentChange).toHaveBeenCalledWith( - eventContainingTargetValue(newYearVal), - ); - }); + const nextWeekCell = queryCellByISODate('2023-11-12'); // Nov 12 + await waitFor(() => expect(nextWeekCell).toHaveFocus()); + }); + }); + }); - test('fires the validation handler', () => { - expect(handleValidation).toHaveBeenCalledWith( - setUTCYear(startValue, Number(newYearVal)), - ); - }); + describe('when next day would be out of range', () => { + const testValue = newUTC(2023, Month.September, 10); + const isoString = '2023-09-10'; - test('sets aria-invalid', () => { - expect(renderResult.inputContainer).toHaveAttribute( - 'aria-invalid', - 'true', - ); - }); + test('left arrow does nothing', async () => { + const { openMenu } = renderDatePicker({ + value: testValue, + min: testValue, + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowleft}'); + await waitFor(() => + expect(queryCellByISODate(isoString)).toHaveFocus(), + ); + }); - test('sets error message', () => { - const errorMessageElement = within( - renderResult.formField, - ).queryByText(expectedMessage); - expect(errorMessageElement).toBeInTheDocument(); - }); + test('right arrow does nothing', async () => { + const { openMenu } = renderDatePicker({ + value: testValue, + max: testValue, + }); + const { queryCellByISODate } = await openMenu(); + + userEvent.keyboard('{arrowright}'); + await waitFor(() => + expect(queryCellByISODate(isoString)).toHaveFocus(), + ); + }); + + test('up arrow does nothing', async () => { + const { openMenu } = renderDatePicker({ + value: testValue, + min: addDays(testValue, -6), }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowup}'); + await waitFor(() => + expect(queryCellByISODate(isoString)).toHaveFocus(), + ); + }); + test('down arrow does nothing', async () => { + const { openMenu } = renderDatePicker({ + value: testValue, + max: addDays(testValue, 6), + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowdown}'); + await waitFor(() => + expect(queryCellByISODate(isoString)).toHaveFocus(), + ); }); }); - }); - describe('Menu', () => { - test('left arrow moves focus to the previous day', async () => { - const { openMenu } = renderDatePicker(); - const { todayCell, queryCellByDate } = await openMenu(); - expect(todayCell).toHaveFocus(); + describe('update the displayed month', () => { + test('left arrow updates displayed month to previous', async () => { + const value = new Date(Date.UTC(2023, Month.September, 1)); + const { openMenu } = renderDatePicker({ value }); + const { calendarGrid } = await openMenu(); - userEvent.keyboard('{arrowleft}'); - const prevDayCell = queryCellByDate(subDays(testToday, 1)); - await waitFor(() => expect(prevDayCell).toHaveFocus()); - }); + userEvent.keyboard('{arrowleft}'); + expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); + }); - test('down arrow moves focus to next week', async () => { - const { openMenu } = renderDatePicker(); - const { todayCell, queryCellByDate } = await openMenu(); - expect(todayCell).toHaveFocus(); + test('right arrow updates displayed month to next', async () => { + const value = new Date(Date.UTC(2023, Month.September, 30)); + const { openMenu } = renderDatePicker({ value }); + const { calendarGrid } = await openMenu(); + userEvent.keyboard('{arrowright}'); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); + + test('up arrow updates displayed month to previous', async () => { + const value = new Date(Date.UTC(2023, Month.September, 6)); + const { openMenu } = renderDatePicker({ value }); + const { calendarGrid } = await openMenu(); + + userEvent.keyboard('{arrowup}'); + expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); + }); - userEvent.keyboard('{arrowdown}'); - const nextWeekCell = queryCellByDate(addDays(testToday, 7)); - await waitFor(() => expect(nextWeekCell).toHaveFocus()); + test('down arrow updates displayed month to next', async () => { + const value = new Date(Date.UTC(2023, Month.September, 25)); + const { openMenu } = renderDatePicker({ value }); + const { calendarGrid } = await openMenu(); + + userEvent.keyboard('{arrowdown}'); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'October 2023', + ); + }); + + test('does not update month when month does not need to change', async () => { + const { openMenu } = renderDatePicker({ + value: newUTC(2023, Month.September, 10), + }); + const { calendarGrid } = await openMenu(); + + userEvent.tab(); + userEvent.keyboard('{arrowleft}{arrowright}{arrowup}{arrowdown}'); + expect(calendarGrid).toHaveAttribute( + 'aria-label', + 'September 2023', + ); + }); }); - test('down arrow can change month', async () => { - const { calendarButton, findByRole } = renderDatePicker(); - userEvent.click(calendarButton); - const menuContainerEl = await findByRole('listbox'); - const calendarGrid = within(menuContainerEl).getByRole('grid'); - fireEvent.transitionEnd(menuContainerEl!); + describe('when month should be updated', () => { + test('left arrow focuses the previous day', async () => { + const value = newUTC(2023, Month.September, 1); + const { openMenu } = renderDatePicker({ + value, + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowleft}'); + const highlightedCell = queryCellByISODate('2023-08-31'); + await waitFor(() => expect(highlightedCell).toHaveFocus()); + }); + test('right arrow focuses the next day', async () => { + const value = newUTC(2023, Month.September, 30); + const { openMenu } = renderDatePicker({ + value, + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowright}'); + const highlightedCell = queryCellByISODate('2023-10-01'); + await waitFor(() => expect(highlightedCell).toHaveFocus()); + }); + test('up arrow focuses the previous week', async () => { + const value = newUTC(2023, Month.September, 7); + const { openMenu } = renderDatePicker({ + value, + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowup}'); + const highlightedCell = queryCellByISODate('2023-08-31'); + await waitFor(() => expect(highlightedCell).toHaveFocus()); + }); + test('down arrow focuses the next week', async () => { + const value = newUTC(2023, Month.September, 24); + const { openMenu } = renderDatePicker({ + value, + }); + const { queryCellByISODate } = await openMenu(); + userEvent.keyboard('{arrowdown}'); + const highlightedCell = queryCellByISODate('2023-10-01'); + await waitFor(() => expect(highlightedCell).toHaveFocus()); + }); + }); - expect(calendarGrid).toHaveAttribute('aria-label', 'December 2023'); + describe('focus-trap', () => { + test('when a cell is focused, pressing tab moves the focus to the left chevron', async () => { + const { openMenu } = renderDatePicker(); + const { todayCell, leftChevron } = await openMenu(); + expect(todayCell).toHaveFocus(); + userEvent.tab(); + expect(leftChevron).toHaveFocus(); + }); - userEvent.keyboard('{arrowdown}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'January 2024'); + test('when a cell is focused, pressing tab + shift moves the focus to the right chevron', async () => { + const { openMenu } = renderDatePicker(); + const { todayCell, rightChevron } = await openMenu(); + expect(todayCell).toHaveFocus(); + userEvent.tab({ shift: true }); + expect(rightChevron).toHaveFocus(); + }); + + test('when the left chevron is focused, pressing tab + shift moves the focus to todays cell', async () => { + const { openMenu } = renderDatePicker(); + const { todayCell, leftChevron } = await openMenu(); + userEvent.tab(); + expect(leftChevron).toHaveFocus(); + userEvent.tab({ shift: true }); + expect(todayCell).toHaveFocus(); + }); + + test('when the right chevron is focused, pressing tab moves the focus to todays cell', async () => { + const { openMenu } = renderDatePicker(); + const { todayCell, rightChevron } = await openMenu(); + userEvent.tab({ shift: true }); + expect(rightChevron).toHaveFocus(); + userEvent.tab(); + expect(todayCell).toHaveFocus(); + }); }); }); }); @@ -2034,46 +2680,80 @@ describe('packages/date-picker', () => { }); describe('typing a full date value', () => { - test('fires value change handler for explicit values', async () => { - const onDateChange = jest.fn(); - const { yearInput, monthInput, dayInput } = renderDatePicker({ - onDateChange, + describe('if the date is valid', () => { + test('fires value change handler for explicit values', async () => { + const onDateChange = jest.fn(); + const { yearInput, monthInput, dayInput } = renderDatePicker({ + onDateChange, + }); + userEvent.type(yearInput, '2003'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '26'); + + await waitFor(() => + expect(onDateChange).toHaveBeenCalledWith( + expect.objectContaining(newUTC(2003, Month.December, 26)), + ), + ); }); - userEvent.type(yearInput, '2003'); - userEvent.type(monthInput, '12'); - userEvent.type(dayInput, '26'); - await waitFor(() => - expect(onDateChange).toHaveBeenCalledWith( - expect.objectContaining(newUTC(2003, Month.December, 26)), - ), - ); - }); + test('does not fire value change handler for ambiguous values', async () => { + const onDateChange = jest.fn(); + const { yearInput, monthInput, dayInput } = renderDatePicker({ + onDateChange, + }); + userEvent.type(yearInput, '2003'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '2'); - test('does not fire value change handler for ambiguous values', async () => { - const onDateChange = jest.fn(); - const { yearInput, monthInput, dayInput } = renderDatePicker({ - onDateChange, + await waitFor(() => expect(onDateChange).not.toHaveBeenCalled()); }); - userEvent.type(yearInput, '2003'); - userEvent.type(monthInput, '12'); - userEvent.type(dayInput, '2'); - await waitFor(() => expect(onDateChange).not.toHaveBeenCalled()); + test('properly renders the input', async () => { + const onDateChange = jest.fn(); + const { yearInput, monthInput, dayInput } = renderDatePicker({ + onDateChange, + }); + userEvent.type(yearInput, '2003'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '26'); + await waitFor(() => { + expect(yearInput).toHaveValue('2003'); + expect(monthInput).toHaveValue('12'); + expect(dayInput).toHaveValue('26'); + }); + }); }); - test('properly renders the input', async () => { - const onDateChange = jest.fn(); - const { yearInput, monthInput, dayInput } = renderDatePicker({ - onDateChange, + // TODO: + describe('if the value is not a valid date', () => { + // E.g. Feb 31 2020 + test('the input is rendered with the typed date', async () => { + const { yearInput, monthInput, dayInput } = renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '02'); + userEvent.type(dayInput, '31'); + await waitFor(() => { + expect(yearInput).toHaveValue('2020'); + expect(monthInput).toHaveValue('02'); + expect(dayInput).toHaveValue('31'); + }); }); - userEvent.type(yearInput, '2003'); - userEvent.type(monthInput, '12'); - userEvent.type(dayInput, '26'); - await waitFor(() => { - expect(yearInput).toHaveValue('2003'); - expect(monthInput).toHaveValue('12'); - expect(dayInput).toHaveValue('26'); + + test('an error is displayed', () => { + const { + yearInput, + monthInput, + dayInput, + inputContainer, + queryByTestId, + } = renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '02'); + userEvent.type(dayInput, '31'); + expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); + const errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toBeInTheDocument(); }); }); @@ -2310,7 +2990,7 @@ describe('packages/date-picker', () => { ); }); - describe('setting the date to an invalid value', () => { + describe('setting the date to an out-of-range value', () => { describe('with initial value', () => { let menuElements: RenderMenuResult; diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx index 405e49f903..6b456d95d5 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.spec.tsx @@ -74,161 +74,24 @@ describe('packages/date-picker/date-picker-input', () => { userEvent.tab(); expect(dayInput.value).toBe('02'); }); - }); - - describe('Keyboard interaction', () => { - // yyyy-mm-dd - describe('Left Arrow', () => { - test('focuses the previous segment when the segment is empty', () => { - const { yearInput, monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowleft}'); - expect(yearInput).toHaveFocus(); - }); - - test('moves the cursor when the segment has a value', () => { - const { monthInput } = renderDatePickerInput(null, { - value: testDate, - }); - userEvent.click(monthInput); - userEvent.keyboard('{arrowleft}'); - expect(monthInput).toHaveFocus(); - }); - - test('focuses the previous segment if the cursor is at the start of the input text', () => { - const { yearInput, monthInput } = renderDatePickerInput(null, { - value: testDate, - }); - userEvent.click(monthInput); - userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}'); - expect(yearInput).toHaveFocus(); - }); - }); - - describe('Right Arrow', () => { - test('focuses the next segment when the segment is empty', () => { - const { yearInput, monthInput } = renderDatePickerInput(); - userEvent.click(yearInput); - userEvent.keyboard('{arrowright}'); - expect(monthInput).toHaveFocus(); - }); - - test('focuses the next segment if the cursor is at the start of the input text', () => { - const { yearInput, monthInput } = renderDatePickerInput(null, { - value: testDate, - }); - userEvent.click(yearInput); - userEvent.keyboard('{arrowright}'); - expect(monthInput).toHaveFocus(); - }); - - test('moves the cursor when the segment has a value', () => { - const { yearInput } = renderDatePickerInput(null, { - value: new Date(), - }); - userEvent.click(yearInput); - userEvent.keyboard('{arrowleft}{arrowright}'); - expect(yearInput).toHaveFocus(); - }); - }); - - describe('Backspace key', () => { - test('deletes any value in the input', () => { + describe('allows only 2 characters', () => { + test('in day input', () => { const { dayInput } = renderDatePickerInput(); - userEvent.type(dayInput, '26{backspace}'); - expect(dayInput.value).toBe('2'); - userEvent.tab(); - expect(dayInput.value).toBe('02'); - }); - - test('deletes the whole value on multiple presses', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.type(monthInput, '11'); - userEvent.type(monthInput, '{backspace}{backspace}'); - expect(monthInput.value).toBe(''); - }); - - test('focuses the previous segment if current segment is empty', () => { - const { yearInput, monthInput } = renderDatePickerInput(); - userEvent.type(monthInput, '{backspace}'); - expect(yearInput).toHaveFocus(); - }); - }); - - describe('Up Arrow', () => { - test('keeps the focus in the current segment', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowup}'); - expect(monthInput).toHaveFocus(); + userEvent.type(dayInput, '22222222'); + expect(dayInput.value.length).toBe(2); }); - test('keeps the focus in the current segment even if the value is valid', () => { + test('in month input', () => { const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowup}{arrowup}{arrowup}'); - expect(monthInput).toHaveValue('03'); - expect(monthInput).toHaveFocus(); - }); - - test('Resets the value to the min value when the new value is greater than the max value', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowup}'); - expect(monthInput).toHaveValue('01'); - userEvent.keyboard( - '{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}{arrowup}', - ); - expect(monthInput).toHaveValue('01'); + userEvent.type(monthInput, '22222222'); + expect(monthInput.value.length).toBe(2); }); }); - describe('Down Arrow', () => { - test('keeps the focus in the current segment', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowdown}'); - expect(monthInput).toHaveFocus(); - }); - - test('keeps the focus in the current segment even if the value is valid', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowdown}{arrowdown}{arrowdown}'); - expect(monthInput).toHaveValue('10'); - expect(monthInput).toHaveFocus(); - }); - - test('Resets the value to the max value when the new value is less than the min value', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.click(monthInput); - userEvent.keyboard('{arrowdown}'); - expect(monthInput).toHaveValue('12'); - }); - }); - - describe('typing', () => { - describe('allows only 2 characters', () => { - test('in day input', () => { - const { dayInput } = renderDatePickerInput(); - userEvent.type(dayInput, '22222222'); - expect(dayInput.value.length).toBe(2); - }); - - test('in month input', () => { - const { monthInput } = renderDatePickerInput(); - userEvent.type(monthInput, '22222222'); - expect(monthInput.value.length).toBe(2); - }); - }); - - describe('allows only 4 characters', () => { - test('in year input', () => { - const { yearInput } = renderDatePickerInput(); - userEvent.type(yearInput, '22222222'); - expect(yearInput.value.length).toBe(4); - }); - }); + test('allows only 4 characters in year input', () => { + const { yearInput } = renderDatePickerInput(); + userEvent.type(yearInput, '22222222'); + expect(yearInput.value.length).toBe(4); }); }); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx index 08d046410a..088c4e73f7 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.spec.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { addDays } from 'date-fns'; +import { render } from '@testing-library/react'; import { getISODate, getISODateTZ, Month, newUTC, - setUTCDate, } from '@leafygreen-ui/date-utils'; import { mockTimeZone, @@ -27,7 +24,6 @@ import { import { DatePickerMenu, DatePickerMenuProps } from '.'; const testToday = newUTC(2023, Month.September, 10); -const testValue = newUTC(2023, Month.September, 14); const renderDatePickerMenu = ( props?: Partial | null, @@ -272,353 +268,4 @@ describe('packages/date-picker/date-picker-menu', () => { }, ); }); - - describe('Keyboard navigation', () => { - describe('Arrow Keys', () => { - beforeEach(() => { - jest.setSystemTime(testToday); - mockTimeZone('America/New_York', -5); - }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('left arrow moves focus to the previous day', async () => { - const { getCellWithValue } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - const prevDay = getCellWithValue(setUTCDate(testValue, 13)); - - await waitFor(() => expect(prevDay).toHaveFocus()); - }); - - test('right arrow moves focus to the next day', async () => { - const { getCellWithValue } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - userEvent.keyboard('{arrowright}'); - - const nextDay = getCellWithValue(setUTCDate(testValue, 15)); - await waitFor(() => expect(nextDay).toHaveFocus()); - }); - - test('up arrow moves focus to the previous week', async () => { - const { getCellWithValue } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - - const prevWeek = getCellWithValue(setUTCDate(testValue, 7)); - await waitFor(() => expect(prevWeek).toHaveFocus()); - }); - - test('down arrow moves focus to the next week', async () => { - const { getCellWithValue } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - - const nextWeek = getCellWithValue(setUTCDate(testValue, 21)); - await waitFor(() => expect(nextWeek).toHaveFocus()); - }); - - describe('when switching between daylight savings and standard time', () => { - // DST: Sun, Mar 12, 2023 – Sun, Nov 5, 2023 - - const standardTimeEndDate = newUTC(2023, Month.March, 11, 22); - const weekBeforeDTStart = newUTC(2023, Month.March, 5, 22); - const daylightTimeStartDate = newUTC(2023, Month.March, 12, 22); - const daylightTimeEndDate = newUTC(2023, Month.November, 5, 22); - const weekAfterDTEnd = newUTC(2023, Month.November, 12, 22); - const standardTimeStartDate = newUTC(2023, Month.November, 6, 22); - - describe('DST start (Mar 12 2023)', () => { - test('left arrow moves focus to prev day', async () => { - jest.setSystemTime(daylightTimeStartDate); // Mar 12 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - const currentDayCell = getCellWithISOString('2023-03-12'); // Mar 12 - await waitFor(() => expect(currentDayCell).toHaveFocus()); - - userEvent.keyboard('{arrowleft}'); - const prevDayCell = getCellWithISOString('2023-03-11'); // Mar 11 - await waitFor(() => expect(prevDayCell).toHaveFocus()); - }); - - test('right arrow moves focus to next day', async () => { - jest.setSystemTime(standardTimeEndDate); // Mar 11 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - const currentDayCell = getCellWithISOString('2023-03-11'); // Mar 11 - await waitFor(() => expect(currentDayCell).toHaveFocus()); - - userEvent.keyboard('{arrowright}'); - const nextDayCell = getCellWithISOString('2023-03-12'); // Mar 12 - await waitFor(() => expect(nextDayCell).toHaveFocus()); - }); - - test('up arrow moves focus to the previous week', async () => { - jest.setSystemTime(daylightTimeStartDate); // Mar 12 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - const prevWeekCell = getCellWithISOString('2023-03-05'); // Mar 5 - await waitFor(() => expect(prevWeekCell).toHaveFocus()); - }); - - test('down arrow moves focus to the next week', async () => { - jest.setSystemTime(weekBeforeDTStart); // Mar 5 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - const nextWeekCell = getCellWithISOString('2023-03-12'); // Mar 12 - await waitFor(() => expect(nextWeekCell).toHaveFocus()); - }); - }); - - describe('DST end (Nov 5 2023)', () => { - test('left arrow moves focus to prev day', async () => { - jest.setSystemTime(standardTimeStartDate); // Nov 6 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - const prevDayCell = getCellWithISOString('2023-11-05'); // Nov 5 - - await waitFor(() => expect(prevDayCell).toHaveFocus()); - }); - - test('right arrow moves focus to next day', async () => { - jest.setSystemTime(daylightTimeEndDate); // Nov 5 - - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowright}'); - - const nextDayCell = getCellWithISOString('2023-11-06'); // Nov 6 - await waitFor(() => expect(nextDayCell).toHaveFocus()); - }); - - test('up arrow moves focus to the previous week', async () => { - jest.setSystemTime(weekAfterDTEnd); // Nov 12 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - - const prevWeekCell = getCellWithISOString('2023-11-05'); // Nov 5 - await waitFor(() => expect(prevWeekCell).toHaveFocus()); - }); - - test('down arrow moves focus to the next week', async () => { - jest.setSystemTime(daylightTimeEndDate); // Nov 5 - const { getCellWithISOString } = renderDatePickerMenu(); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - - const nextWeekCell = getCellWithISOString('2023-11-12'); // Nov 12 - await waitFor(() => expect(nextWeekCell).toHaveFocus()); - }); - }); - }); - - describe('when next day would be out of range', () => { - const testValue = newUTC(2023, Month.September, 10); - const isoString = '2023-09-10'; - const singleCtx = { - value: testValue, - }; - test('left arrow does nothing', async () => { - const { getCellWithISOString } = renderDatePickerMenu( - null, - singleCtx, - { - min: testValue, - }, - ); - userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - await waitFor(() => - expect(getCellWithISOString(isoString)).toHaveFocus(), - ); - }); - - test('right arrow does nothing', async () => { - const { getCellWithISOString } = renderDatePickerMenu( - null, - singleCtx, - { - max: testValue, - }, - ); - userEvent.tab(); - userEvent.keyboard('{arrowright}'); - await waitFor(() => - expect(getCellWithISOString(isoString)).toHaveFocus(), - ); - }); - - test('up arrow does nothing', async () => { - const { getCellWithISOString } = renderDatePickerMenu( - null, - singleCtx, - { - min: addDays(testValue, -6), - }, - ); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - await waitFor(() => - expect(getCellWithISOString(isoString)).toHaveFocus(), - ); - }); - test('down arrow does nothing', async () => { - const { getCellWithISOString } = renderDatePickerMenu( - null, - singleCtx, - { - max: addDays(testValue, 6), - }, - ); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - await waitFor(() => - expect(getCellWithISOString(isoString)).toHaveFocus(), - ); - }); - }); - - describe('update the displayed month', () => { - test('left arrow updates displayed month to previous', () => { - const value = new Date(Date.UTC(2023, Month.September, 1)); - const { calendarGrid } = renderDatePickerMenu(null, { value }); - userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); - }); - - test('right arrow updates displayed month to next', () => { - const value = new Date(Date.UTC(2023, Month.September, 30)); - const { calendarGrid } = renderDatePickerMenu(null, { value }); - userEvent.tab(); - userEvent.keyboard('{arrowright}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'October 2023'); - }); - - test('up arrow updates displayed month to previous', () => { - const value = new Date(Date.UTC(2023, Month.September, 6)); - const { calendarGrid } = renderDatePickerMenu(null, { value }); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'August 2023'); - }); - - test('down arrow updates displayed month to next', () => { - const value = new Date(Date.UTC(2023, Month.September, 25)); - const { calendarGrid } = renderDatePickerMenu(null, { value }); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'October 2023'); - }); - - test('does not update month when month does not need to change', () => { - const { calendarGrid } = renderDatePickerMenu(null, { - value: testValue, - }); - userEvent.tab(); - userEvent.keyboard('{arrowleft}{arrowright}{arrowup}{arrowdown}'); - expect(calendarGrid).toHaveAttribute('aria-label', 'September 2023'); - }); - }); - - describe('when month should be updated', () => { - test('left arrow focuses the previous day', async () => { - const value = newUTC(2023, Month.September, 1); - const { getCellWithValue } = renderDatePickerMenu(null, { - value, - }); - userEvent.tab(); - userEvent.keyboard('{arrowleft}'); - const highlightedCell = getCellWithValue( - newUTC(2023, Month.August, 31), - ); - - await waitFor(() => expect(highlightedCell).toHaveFocus()); - }); - test('right arrow focuses the next day', async () => { - const value = newUTC(2023, Month.September, 30); - const { getCellWithValue } = renderDatePickerMenu(null, { - value, - }); - userEvent.tab(); - userEvent.keyboard('{arrowright}'); - const highlightedCell = getCellWithValue( - newUTC(2023, Month.October, 1), - ); - await waitFor(() => expect(highlightedCell).toHaveFocus()); - }); - test('up arrow focuses the previous week', async () => { - const value = newUTC(2023, Month.September, 7); - const { getCellWithValue } = renderDatePickerMenu(null, { - value, - }); - userEvent.tab(); - userEvent.keyboard('{arrowup}'); - const highlightedCell = getCellWithValue( - newUTC(2023, Month.August, 31), - ); - await waitFor(() => expect(highlightedCell).toHaveFocus()); - }); - test('down arrow focuses the next week', async () => { - const value = newUTC(2023, Month.September, 24); - const { getCellWithValue } = renderDatePickerMenu(null, { - value, - }); - userEvent.tab(); - userEvent.keyboard('{arrowdown}'); - const highlightedCell = getCellWithValue( - newUTC(2023, Month.October, 1), - ); - await waitFor(() => expect(highlightedCell).toHaveFocus()); - }); - }); - - describe('focus-trap', () => { - test('when a cell is focused, pressing tab moves the focus to the left chevron', () => { - const { todayCell, leftChevron } = renderDatePickerMenu(); - userEvent.tab(); - expect(todayCell).toHaveFocus(); - userEvent.tab(); - expect(leftChevron).toHaveFocus(); - }); - - test('when a cell is focused, pressing tab + shift moves the focus to the right chevron', () => { - const { todayCell, rightChevron } = renderDatePickerMenu(); - userEvent.tab(); - expect(todayCell).toHaveFocus(); - userEvent.tab({ shift: true }); - expect(rightChevron).toHaveFocus(); - }); - - test('when the left chevron is focused, pressing tab + shift moves the focus to todays cell', () => { - const { todayCell, leftChevron } = renderDatePickerMenu(); - leftChevron?.focus(); - expect(leftChevron).toHaveFocus(); - userEvent.tab({ shift: true }); - expect(todayCell).toHaveFocus(); - }); - - test('when the right chevron is focused, pressing tab moves the focus to todays cell', () => { - const { todayCell, rightChevron } = renderDatePickerMenu(); - rightChevron?.focus(); - expect(rightChevron).toHaveFocus(); - userEvent.tab(); - expect(todayCell).toHaveFocus(); - }); - }); - }); - }); }); diff --git a/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/doSegmentsFormValidDate.ts b/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/doSegmentsFormValidDate.ts new file mode 100644 index 0000000000..32ce1263ba --- /dev/null +++ b/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/doSegmentsFormValidDate.ts @@ -0,0 +1,21 @@ +import { DateSegmentsState } from '../..//types'; +import { isEverySegmentFilled } from '../isEverySegmentFilled'; +import { isEverySegmentValid } from '../isEverySegmentValid'; +import { newDateFromSegments } from '../newDateFromSegments'; + +/** + * Returns whether the provided {@link DateSegmentsState} forms a valid date + */ +export const doSegmentsFormValidDate = ( + segments: DateSegmentsState, +): boolean => { + const areAllFilled = isEverySegmentFilled(segments); + const areAllValid = isEverySegmentValid(segments); + + if (areAllFilled && areAllValid) { + const utcDate = newDateFromSegments(segments); + return !!utcDate; + } + + return false; +}; diff --git a/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/index.ts b/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/index.ts new file mode 100644 index 0000000000..a59efb6314 --- /dev/null +++ b/packages/date-picker/src/shared/utils/doSegmentsFormValidDate/index.ts @@ -0,0 +1 @@ +export { doSegmentsFormValidDate } from './doSegmentsFormValidDate'; diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.ts new file mode 100644 index 0000000000..ba59e6b5dd --- /dev/null +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateString.ts @@ -0,0 +1,9 @@ +import { getFormattedSegmentsFromDate } from '../getSegmentsFromDate'; + +import { getFormattedDateStringFromSegments } from './getFormattedDateStringFromSegments'; + +export const getFormattedDateString = (date: Date, locale: string) => { + const dateSegments = getFormattedSegmentsFromDate(date); + + return getFormattedDateStringFromSegments(dateSegments, locale); +}; diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateStringFromSegments.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateStringFromSegments.ts new file mode 100644 index 0000000000..0b9d7ac777 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/getFormattedDateStringFromSegments.ts @@ -0,0 +1,25 @@ +import { DateSegment, DateSegmentsState } from '../../../shared/types'; +import { getFormatParts } from '../getFormatParts'; +import { getValueFormatter } from '../getValueFormatter'; + +export const getFormattedDateStringFromSegments = ( + segments: DateSegmentsState, + locale: string, +): string | undefined => { + const formatParts = getFormatParts(locale); + + // Note: looping through `formatParts`, instead of using `Intl.DateTimeFormat(locale).format(date)` + // since the locale `iso8601` does not return a valid formatter + const formattedDate = formatParts?.reduce((dateString, part) => { + if (part.type === 'literal') { + return dateString + part.value; + } + + const segment = part.type as DateSegment; + const formatter = getValueFormatter(segment); + const formattedSegment = formatter(segments[segment]); + return dateString + formattedSegment; + }, ''); + + return formattedDate; +}; diff --git a/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts b/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts index f5e8293ca6..3e8df435aa 100644 --- a/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts +++ b/packages/date-picker/src/shared/utils/getFormattedDateString/index.ts @@ -1,20 +1,2 @@ -import { DateSegment } from '../../types'; -import { getFormatParts } from '../getFormatParts'; -import { getFormattedSegmentsFromDate } from '../getSegmentsFromDate'; - -export const getFormattedDateString = (date: Date, locale: string) => { - const formatParts = getFormatParts(locale); - const dateSegments = getFormattedSegmentsFromDate(date); - - // Note: looping through `formatParts`, instead of using `Intl.DateTimeFormat(locale).format(date)` - // since the locale `iso8601` does not return a valid formatter - const formattedDate = formatParts?.reduce((dateString, part) => { - const partString = - part.type === 'literal' - ? part.value - : dateSegments[part.type as DateSegment]; - return dateString + partString; - }, ''); - - return formattedDate; -}; +export { getFormattedDateString } from './getFormattedDateString'; +export { getFormattedDateStringFromSegments } from './getFormattedDateStringFromSegments'; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index cf15e082e0..f31a0841ec 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -1,8 +1,12 @@ export { doesSomeSegmentExist } from './doesSomeSegmentExist'; +export { doSegmentsFormValidDate } from './doSegmentsFormValidDate'; export { getAutoComplete } from './getAutoComplete'; export { getFirstEmptySegment } from './getFirstEmptySegment'; export { getFormatParts, getFormatter } from './getFormatParts'; -export { getFormattedDateString } from './getFormattedDateString'; +export { + getFormattedDateString, + getFormattedDateStringFromSegments, +} from './getFormattedDateString'; export { getMaxSegmentValue } from './getMaxSegmentValue'; export { getMinSegmentValue } from './getMinSegmentValue'; export { @@ -17,6 +21,8 @@ export { } from './getSegmentsFromDate'; export { getValueFormatter } from './getValueFormatter'; export { isElementInputSegment } from './isElementInputSegment'; +export { isEverySegmentFilled } from './isEverySegmentFilled'; +export { isEverySegmentValid } from './isEverySegmentValid'; export { isEverySegmentValueExplicit } from './isEverySegmentValueExplicit'; export { isExplicitSegmentValue } from './isExplicitSegmentValue'; export { isValidSegmentName, isValidSegmentValue } from './isValidSegment'; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentFilled/index.ts b/packages/date-picker/src/shared/utils/isEverySegmentFilled/index.ts new file mode 100644 index 0000000000..9e54a49314 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentFilled/index.ts @@ -0,0 +1 @@ +export { isEverySegmentFilled } from './isEverySegmentFilled'; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentFilled/isEverySegmentFilled.ts b/packages/date-picker/src/shared/utils/isEverySegmentFilled/isEverySegmentFilled.ts new file mode 100644 index 0000000000..e4f2ea6212 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentFilled/isEverySegmentFilled.ts @@ -0,0 +1,7 @@ +import { isNotZeroLike } from '@leafygreen-ui/lib'; + +import { DateSegmentsState } from '../../types'; + +export const isEverySegmentFilled = (segments: DateSegmentsState) => { + return Object.values(segments).every(isNotZeroLike); +}; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentValid/index.ts b/packages/date-picker/src/shared/utils/isEverySegmentValid/index.ts new file mode 100644 index 0000000000..3dec1d2d9c --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentValid/index.ts @@ -0,0 +1 @@ +export { isEverySegmentValid } from './isEverySegmentValid'; diff --git a/packages/date-picker/src/shared/utils/isEverySegmentValid/isEverySegmentValid.ts b/packages/date-picker/src/shared/utils/isEverySegmentValid/isEverySegmentValid.ts new file mode 100644 index 0000000000..6e338ec5b9 --- /dev/null +++ b/packages/date-picker/src/shared/utils/isEverySegmentValid/isEverySegmentValid.ts @@ -0,0 +1,11 @@ +import { DateSegment, DateSegmentsState } from '../../types'; +import { isValidValueForSegment } from '../isValidValueForSegment'; + +/** + * Whether every segment in a {@link DateSegmentsState} object is valid + */ +export const isEverySegmentValid = (segments: DateSegmentsState): boolean => { + return Object.entries(segments).every(([segment, value]) => + isValidValueForSegment(segment as DateSegment, value), + ); +}; From 363a15d09b17c6d4afcdfaa565bc70cb4b19a18c Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:38:53 -0500 Subject: [PATCH 337/351] DatePicker [LG-3899] arrow key errors (#2145) * getSegmentStateFromRefs * Adds error handling * adds getSegmentStateFromRefs tests * updates how dates rollover * Update DatePicker.spec.tsx * Test errors don't appear from tabbing * setIsDirty. show error only if areAllFilled * Update DateInputBox.tsx --- .../date-picker/src/DatePicker.stories.tsx | 8 ++- .../src/DatePicker/DatePicker.spec.tsx | 72 ++++++++++++++----- .../DatePickerContext/DatePickerContext.tsx | 30 +++++++- .../DatePickerInput/DatePickerInput.tsx | 2 +- .../DateInput/DateInputBox/DateInputBox.tsx | 65 ++++++++++++++--- .../context/SharedDatePickerContext.tsx | 3 - .../getSegmentStateFromRefs.spec.ts | 34 +++++++++ .../getSegmentStateFromRefs.ts | 15 ++++ .../utils/getSegmentStateFromRefs/index.ts | 1 + .../date-picker/src/shared/utils/index.ts | 1 + 10 files changed, 195 insertions(+), 36 deletions(-) create mode 100644 packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.spec.ts create mode 100644 packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.ts create mode 100644 packages/date-picker/src/shared/utils/getSegmentStateFromRefs/index.ts diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index b06747480c..3711a9edc4 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -4,6 +4,8 @@ import { StoryFn } from '@storybook/react'; import Button from '@leafygreen-ui/button'; import { + DateType, + isValidDate, Month, newUTC, testLocales, @@ -92,7 +94,7 @@ const meta: StoryMetaType = { export default meta; export const LiveExample: StoryFn = props => { - const [value, setValue] = useState(); + const [value, setValue] = useState(); return ( = props => { onDateChange={v => { // eslint-disable-next-line no-console console.log('Storybook: onDateChange', { v }); - setValue(v); + if (isValidDate(v)) { + setValue(v); + } }} handleValidation={date => // eslint-disable-next-line no-console diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index cde3711fa5..67f2c921e4 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { fireEvent, + // prettyDOM, render, waitFor, waitForElementToBeRemoved, @@ -1052,6 +1053,13 @@ describe('packages/date-picker', () => { ).toBeFalsy(); } + const errorElement = renderResult.queryByTestId( + 'lg-form_field-error_message', + ); + + await waitFor(() => + expect(errorElement).not.toBeInTheDocument(), + ); userEvent.tab(); } }); @@ -1079,6 +1087,14 @@ describe('packages/date-picker', () => { ).toBeFalsy(); } + const errorElement = renderResult.queryByTestId( + 'lg-form_field-error_message', + ); + + await waitFor(() => + expect(errorElement).not.toBeInTheDocument(), + ); + userEvent.tab(); // There are side-effects triggered on CSS transition-end events. // Fire this event here to ensure these side-effects don't impact Tab order @@ -1544,18 +1560,20 @@ describe('packages/date-picker', () => { test('error state stays after menu is closed', async () => { const result = renderDatePicker({ - value: newUTC(2020, Month.February, 29), + value: newUTC(2020, Month.January, 31), }); const input = getRelevantInput(result); userEvent.click(input); userEvent.keyboard('{arrowup}'); + const { menuContainerEl } = + await result.findMenuElements(); userEvent.click(result.container.parentElement!); - await waitFor(() => { - const errorElement = result.queryByTestId( - 'lg-form_field-error_message', - ); - expect(errorElement).toBeInTheDocument(); - }); + + await waitForElementToBeRemoved(menuContainerEl); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); }); break; @@ -1802,7 +1820,6 @@ describe('packages/date-picker', () => { }); }); - // TODO: describe('if the new value would be invalid', () => { // E.g. Feb 30 2020 or Feb 29 2021 switch (segment) { @@ -1859,25 +1876,48 @@ describe('packages/date-picker', () => { test('error state stays after menu is closed', async () => { const result = renderDatePicker({ - value: newUTC(2020, Month.February, 29), + value: newUTC(2020, Month.March, 31), }); const input = getRelevantInput(result); userEvent.click(input); userEvent.keyboard('{arrowdown}'); + const { menuContainerEl } = + await result.findMenuElements(); userEvent.click(result.container.parentElement!); - await waitFor(() => { - const errorElement = result.queryByTestId( - 'lg-form_field-error_message', - ); - expect(errorElement).toBeInTheDocument(); - }); + + await waitForElementToBeRemoved(menuContainerEl); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).toBeInTheDocument(); }); break; } case 'day': { - // There is no case where decrementing a day results in an invalid date value + test('changing date rolls over to number of days-in-month', async () => { + const result = renderDatePicker({ + value: newUTC(2020, Month.February, 1), + }); + const input = getRelevantInput(result); + userEvent.click(input); + userEvent.keyboard('{arrowdown}'); + + await waitFor(() => { + expect(result.yearInput).toHaveValue('2020'); + expect(result.monthInput).toHaveValue('02'); + expect(result.dayInput).toHaveValue('29'); + expect(result.inputContainer).toHaveAttribute( + 'aria-invalid', + 'false', + ); + const errorElement = result.queryByTestId( + 'lg-form_field-error_message', + ); + expect(errorElement).not.toBeInTheDocument(); + }); + }); break; } diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx index 24ccae1239..47b5cb45e7 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx @@ -19,7 +19,13 @@ import { import { usePrevious } from '@leafygreen-ui/hooks'; import { useSharedDatePickerContext } from '../../shared/context'; -import { getFormattedDateString } from '../../shared/utils'; +import { + doSegmentsFormValidDate, + getFormattedDateString, + getFormattedDateStringFromSegments, + getSegmentStateFromRefs, + isEverySegmentFilled, +} from '../../shared/utils'; import { getInitialHighlight } from '../utils/getInitialHighlight'; import { @@ -107,7 +113,7 @@ export const DatePickerProvider = ({ * Handles internal validation, * and calls the provided `handleValidation` callback */ - const handleValidation = (val?: DateType) => { + const handleValidation = (val?: DateType): void => { // Set an internal error state if necessary if (val && !isInRange(val)) { if (isOnOrBefore(val, min)) { @@ -120,7 +126,25 @@ export const DatePickerProvider = ({ ); } } else { - clearInternalErrorMessage(); + // Wait for the inputs to update, then check they're valid + setTimeout(() => { + const segments = getSegmentStateFromRefs(refs.segmentRefs); + const areAllFilled = isEverySegmentFilled(segments); + const areSegmentsValidDate = doSegmentsFormValidDate(segments); + + // If the segments are valid, clear any error messages + if (areSegmentsValidDate) { + clearInternalErrorMessage(); + } else if (areAllFilled) { + // Show an error iff areAllFilled + const dateString = getFormattedDateStringFromSegments( + segments, + locale, + ); + // Setting the error message here is likely redundant (handled by DateInputBox) + setInternalErrorMessage(`${dateString} is not a valid date`); + } + }); } _handleValidation?.(val); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index b0bb090d07..e682bb8a16 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -197,10 +197,10 @@ export const DatePickerInput = forwardRef( /** * Fire a simulated `change` event */ - const changeEvent = new Event('change'); const target = segmentRefs[segment].current; if (target) { + const changeEvent = new Event('change'); const reactEvent = createSyntheticEvent< ChangeEvent >(changeEvent, target); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index c915db5a03..8067c37d71 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -1,6 +1,8 @@ import React, { FocusEventHandler } from 'react'; +import { getDaysInMonth } from 'date-fns'; import isEqual from 'lodash/isEqual'; +import { newUTC } from '@leafygreen-ui/date-utils'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -16,10 +18,13 @@ import { } from '../../../types'; import { doesSomeSegmentExist, + doSegmentsFormValidDate, + getFormattedDateStringFromSegments, getMaxSegmentValue, getMinSegmentValue, getRelativeSegment, getValueFormatter, + isEverySegmentFilled, isEverySegmentValueExplicit, isExplicitSegmentValue, newDateFromSegments, @@ -60,7 +65,15 @@ export const DateInputBox = React.forwardRef( }: DateInputBoxProps, fwdRef, ) => { - const { formatParts, disabled, min, max } = useSharedDatePickerContext(); + const { + formatParts, + disabled, + min, + max, + locale, + setIsDirty, + setInternalErrorMessage, + } = useSharedDatePickerContext(); const { theme } = useDarkMode(); const containerRef = useForwardedRef(fwdRef, null); @@ -87,16 +100,30 @@ export const DateInputBox = React.forwardRef( const hasAnySegmentChanged = !isEqual(newSegments, prevSegments); if (hasAnySegmentChanged) { - const areAllSegmentsEmpty = !doesSomeSegmentExist(newSegments); - const areAllExplicit = isEverySegmentValueExplicit(newSegments); - const utcDate = newDateFromSegments(newSegments); + const areAllEmpty = !doesSomeSegmentExist(newSegments); + const areAllFilled = isEverySegmentFilled(newSegments); - if (areAllSegmentsEmpty) { - // otherwise, if no segment exists, set the external value to null + if (areAllEmpty) { + // if no segment exists, set the external value to null setValue?.(null); - } else if (areAllExplicit && !!utcDate) { - // Update the value iff all segments create a valid date. - setValue?.(utcDate); + } else if (areAllFilled) { + const areAllExplicit = isEverySegmentValueExplicit(newSegments); + const utcDate = newDateFromSegments(newSegments); + const isValidDate = doSegmentsFormValidDate(newSegments); + + if (areAllExplicit && !!utcDate) { + // Update the value iff all segments create a valid date. + setValue?.(utcDate); + } else if (!isValidDate) { + const dateString = getFormattedDateStringFromSegments( + newSegments, + locale, + ); + // This error state will be removed by `handleValidation` once a value is set + setInternalErrorMessage(`${dateString} is not a valid date`); + } + // If all values are filled, set the input as dirty + setIsDirty(true); } } }; @@ -114,14 +141,30 @@ export const DateInputBox = React.forwardRef( const changedViaArrowKeys = meta?.key === keyMap.ArrowDown || meta?.key === keyMap.ArrowUp; - // Auto-format the segment + // If we've updated the "day" segment via arrow keys, + // we can update it's rollover behavior based on the month + if (changedViaArrowKeys && segmentName === 'day') { + const year = Number(segments['year']); + const month = Number(segments['month']); + const daysInMonth = getDaysInMonth(newUTC(year, month, 1)); + + if (Number(segmentValue) > daysInMonth) { + if (meta?.key === keyMap.ArrowDown) { + segmentValue = String(daysInMonth); + } else if (meta?.key === keyMap.ArrowUp) { + segmentValue = '01'; + } + } + } + + // Auto-format the segment if it is explicit and was not changed via arrow-keys if ( !changedViaArrowKeys && isExplicitSegmentValue(segmentName, segmentValue) ) { segmentValue = getFormattedSegmentValue(segmentName, segmentValue); - // Auto-advance focus + // Auto-advance focus (if possible) const nextSegmentName = getRelativeSegment('next', { segment: segmentName, formatParts, diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx b/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx index eceba0ca40..a9c38ac2f6 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.tsx @@ -19,9 +19,6 @@ import { useDatePickerErrorNotifications } from './useDatePickerErrorNotificatio export const SharedDatePickerContext = createContext(defaultSharedDatePickerContext); -// TODO: Consider renaming this to `SharedDatePickerContext`, -// and use `SharedDatePickerContext` for what's currently `DatePickerContext` - /** The Provider component for SharedDatePickerContext */ export const SharedDatePickerProvider = ({ children, diff --git a/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.spec.ts b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.spec.ts new file mode 100644 index 0000000000..0f114a1bf7 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.spec.ts @@ -0,0 +1,34 @@ +import { SegmentRefs } from '../../hooks'; +import { segmentRefsMock } from '../../testutils'; + +import { getSegmentStateFromRefs } from '.'; + +describe('packages/date-picker/utils/getSegmentStateFromRefs', () => { + test('empty refs', () => { + expect(getSegmentStateFromRefs(segmentRefsMock)).toEqual({ + day: '', + month: '', + year: '', + }); + }); + + test('Refs with values', () => { + const refs: SegmentRefs = { ...segmentRefsMock }; + // @ts-expect-error - current is read-only + refs.day.current = document.createElement('input'); + // @ts-expect-error - current is read-only + refs.month.current = document.createElement('input'); + // @ts-expect-error - current is read-only + refs.year.current = document.createElement('input'); + + refs.day.current.value = '02'; + refs.month.current.value = '02'; + refs.year.current.value = '2020'; + + expect(getSegmentStateFromRefs(refs)).toEqual({ + day: '02', + month: '02', + year: '2020', + }); + }); +}); diff --git a/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.ts b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.ts new file mode 100644 index 0000000000..d8becca997 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/getSegmentStateFromRefs.ts @@ -0,0 +1,15 @@ +import { SegmentRefs } from '../../hooks'; +import { DateSegmentsState } from '../../types'; + +/** + * Returns a {@link DateSegmentsState} object given a {@link SegmentRefs} Ref object + */ +export const getSegmentStateFromRefs = ( + refs: SegmentRefs, +): DateSegmentsState => { + return { + day: refs.day.current?.value ?? '', + month: refs.month.current?.value ?? '', + year: refs.year.current?.value ?? '', + }; +}; diff --git a/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/index.ts b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/index.ts new file mode 100644 index 0000000000..0d0ea88407 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getSegmentStateFromRefs/index.ts @@ -0,0 +1 @@ +export { getSegmentStateFromRefs } from './getSegmentStateFromRefs'; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index f31a0841ec..354af9cf99 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -19,6 +19,7 @@ export { getFormattedSegmentsFromDate, getSegmentsFromDate, } from './getSegmentsFromDate'; +export { getSegmentStateFromRefs } from './getSegmentStateFromRefs'; export { getValueFormatter } from './getValueFormatter'; export { isElementInputSegment } from './isElementInputSegment'; export { isEverySegmentFilled } from './isEverySegmentFilled'; From 8ad30637c84d42cf846542363083e1712e0a16a0 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 20 Dec 2023 15:12:50 -0500 Subject: [PATCH 338/351] Date Picker [LG-3899] arrow key errors (#2144) * removes interaction tests from Menu * Moves interaction tests to DatePicker.spec * moves arrow key tests from Input to DatePicker * Update DatePicker.spec.tsx * adds test for typing an invalid date * Update DatePicker.spec.tsx * test up/down input arrow keys on each segment * fix year rollover tests * queryByTestId * Add tests for arrow up/down errors * adds utils * adds error state persistence tests From e7f66529b69146f7da09f61399a0365ff6ad8f0e Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:26:40 -0500 Subject: [PATCH 339/351] Date picker [LG-3912] Test for invalid dates (#2146) * add component tests for invalid date * move types * creates InvalidDate * update isValidDate * update checks in getISODateTZ & getISODate * update stories TS * updates getSegmentsFromDate * add test to ensure mocked dates are valid * updates some utils to accept DateType as param * update sameDay check in useDateSegment * updates Context * Update DatePickerInput.tsx * fixes menu TS * updates & tests useDateSegments * Add re-render tests to DateInputBox * Adds DateInputBox test for invalid date * adds invalidDate tests for DatePicker * update builds --- packages/date-picker/package.json | 1 - .../date-picker/src/DatePicker.stories.tsx | 2 +- .../src/DatePicker/DatePicker.spec.tsx | 125 +++++++++-- .../DatePickerContext/DatePickerContext.tsx | 21 +- .../DatePickerContext.types.ts | 4 +- .../DatePickerInput/DatePickerInput.tsx | 4 +- .../DatePickerMenu/DatePickerMenu.stories.tsx | 5 +- .../DatePickerMenu/DatePickerMenu.tsx | 12 +- .../DateInputBox/DateInputBox.spec.tsx | 127 +++++++++-- .../DateInputBox/DateInputBox.stories.tsx | 17 +- .../context/SharedDatePickerContext.types.ts | 11 +- .../context/SharedDatePickerContext.utils.ts | 11 +- .../useDateSegments/useDateSegments.spec.ts | 201 ++++++++++++++++++ .../hooks/useDateSegments/useDateSegments.ts | 14 +- .../getFormattedSegmentsFromDate.ts | 19 ++ .../getSegmentsFromDate.spec.ts | 43 +++- .../getSegmentsFromDate.ts | 12 ++ .../shared/utils/getSegmentsFromDate/index.ts | 26 +-- .../date-utils/src/getISODate/getISODate.ts | 2 +- .../src/getISODateTZ/getISODateTZ.ts | 3 +- .../isCurrentUTCDay/isCurrentUTCDay.spec.ts | 9 +- .../src/isCurrentUTCDay/isCurrentUTCDay.ts | 3 +- .../src/isOnOrAfter/isOnOrAfter.spec.ts | 6 + .../date-utils/src/isOnOrAfter/isOnOrAfter.ts | 10 +- .../src/isOnOrBefore/isOnOrBefore.spec.ts | 6 + .../src/isOnOrBefore/isOnOrBefore.ts | 10 +- .../src/isSameUTCDay/isSameUTCDay.spec.ts | 13 +- .../src/isSameUTCDay/isSameUTCDay.ts | 10 +- .../src/isSameUTCMonth/isSameUTCMonth.spec.ts | 16 +- .../src/isSameUTCMonth/isSameUTCMonth.ts | 10 +- .../src/isSameUTCRange/isSameUTCRange.ts | 5 +- packages/date-utils/src/isValidDate/index.ts | 6 +- .../src/isValidDate/isValidDate.spec.ts | 40 ++-- .../date-utils/src/isValidDate/isValidDate.ts | 40 ++-- packages/date-utils/src/toDate/toDate.ts | 4 +- packages/date-utils/src/types/InvalidDate.ts | 52 +++++ .../src/{types.ts => types/index.ts} | 3 +- 37 files changed, 726 insertions(+), 177 deletions(-) create mode 100644 packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts create mode 100644 packages/date-picker/src/shared/utils/getSegmentsFromDate/getFormattedSegmentsFromDate.ts create mode 100644 packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.ts create mode 100644 packages/date-utils/src/types/InvalidDate.ts rename packages/date-utils/src/{types.ts => types/index.ts} (88%) diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index 6675699eae..b53935c609 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -42,7 +42,6 @@ "@leafygreen-ui/testing-lib": "^0.3.4", "mockdate": "^3.0.5", "storybook-mock-date-decorator": "^1.0.1", - "timezone-mock": "^1.3.6", "@jest/globals": "^29.6.2" }, "resolutions": { diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 3711a9edc4..06d7e01533 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -125,7 +125,7 @@ Uncontrolled.parameters = { }; export const InModal: StoryFn = props => { - const [value, setValue] = useState(); + const [value, setValue] = useState(); const [isModalOpen, setIsModalOpen] = useState(false); return ( diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index 67f2c921e4..a13deb026d 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -77,6 +77,18 @@ describe('packages/date-picker', () => { expect(label).toBeInTheDocument(); }); + test('warn when no labels are passed in', () => { + const consoleSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + /* @ts-expect-error - needs label/aria-label/aria-labelledby */ + render(); + expect(consoleSpy).toHaveBeenCalledWith( + 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', + ); + }); + test('renders description', () => { const { getByText } = render( , @@ -148,34 +160,101 @@ describe('packages/date-picker', () => { expect(yearInput).toBeInTheDocument(); }); - test('renders `value` prop', () => { - const { dayInput, monthInput, yearInput } = renderDatePicker({ - value: newUTC(2023, Month.December, 25), + describe('rendering values', () => { + test('renders `value` prop', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + value: newUTC(2023, Month.December, 25), + }); + expect(dayInput.value).toEqual('25'); + expect(monthInput.value).toEqual('12'); + expect(yearInput.value).toEqual('2023'); }); - expect(dayInput.value).toEqual('25'); - expect(monthInput.value).toEqual('12'); - expect(yearInput.value).toEqual('2023'); - }); - test('renders `initialValue` prop', () => { - const { dayInput, monthInput, yearInput } = renderDatePicker({ - initialValue: newUTC(2023, Month.December, 25), + test('renders `initialValue` prop', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + initialValue: newUTC(2023, Month.December, 25), + }); + expect(dayInput.value).toEqual('25'); + expect(monthInput.value).toEqual('12'); + expect(yearInput.value).toEqual('2023'); }); - expect(dayInput.value).toEqual('25'); - expect(monthInput.value).toEqual('12'); - expect(yearInput.value).toEqual('2023'); - }); - test('console warning when no labels are passed in', () => { - const consoleSpy = jest - .spyOn(console, 'warn') - .mockImplementation(() => {}); + test('renders nothing when `value` is null', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + value: null, + }); + expect(dayInput.value).toEqual(''); + expect(monthInput.value).toEqual(''); + expect(yearInput.value).toEqual(''); + }); - /* @ts-expect-error - needs label/aria-label/aria-labelledby */ - render(); - expect(consoleSpy).toHaveBeenCalledWith( - 'For screen-reader accessibility, label, aria-labelledby, or aria-label must be provided to DatePicker component', - ); + test('renders nothing when `initialValue` is null', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + initialValue: null, + }); + expect(dayInput.value).toEqual(''); + expect(monthInput.value).toEqual(''); + expect(yearInput.value).toEqual(''); + }); + + test('renders nothing when `value` is an invalid date', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + value: new Date('invalid'), + }); + expect(dayInput.value).toEqual(''); + expect(monthInput.value).toEqual(''); + expect(yearInput.value).toEqual(''); + }); + + test('renders nothing when `initialValue` is an invalid date', () => { + const { dayInput, monthInput, yearInput } = renderDatePicker({ + initialValue: new Date('invalid'), + }); + expect(dayInput.value).toEqual(''); + expect(monthInput.value).toEqual(''); + expect(yearInput.value).toEqual(''); + }); + + describe('re-rendering with a new value', () => { + test('updates inputs with new valid value', () => { + const { dayInput, monthInput, yearInput, rerenderDatePicker } = + renderDatePicker({ + value: newUTC(2023, Month.December, 25), + }); + + rerenderDatePicker({ value: newUTC(2024, Month.September, 10) }); + + expect(dayInput.value).toEqual('10'); + expect(monthInput.value).toEqual('09'); + expect(yearInput.value).toEqual('2024'); + }); + + test('clears inputs when value is `null`', () => { + const { dayInput, monthInput, yearInput, rerenderDatePicker } = + renderDatePicker({ + value: newUTC(2023, Month.December, 25), + }); + + rerenderDatePicker({ value: null }); + + expect(dayInput.value).toEqual(''); + expect(monthInput.value).toEqual(''); + expect(yearInput.value).toEqual(''); + }); + + test('renders previous input if value is invalid', () => { + const { dayInput, monthInput, yearInput, rerenderDatePicker } = + renderDatePicker({ + value: newUTC(2023, Month.December, 25), + }); + + rerenderDatePicker({ value: new Date('invalid') }); + + expect(dayInput.value).toEqual('25'); + expect(monthInput.value).toEqual('12'); + expect(yearInput.value).toEqual('2023'); + }); + }); }); describe('Error states', () => { diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx index 47b5cb45e7..5028c07dfb 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx @@ -15,6 +15,7 @@ import { getISODate, isOnOrBefore, isSameUTCDay, + isValidDate, } from '@leafygreen-ui/date-utils'; import { usePrevious } from '@leafygreen-ui/hooks'; @@ -71,15 +72,21 @@ export const DatePickerProvider = ({ [hour], ); + /** Internal callback to get a valid `month` from a given date value */ + const getMonthFromValue = useCallback( + (val?: DateType) => getFirstOfUTCMonth(isValidDate(val) ? val : today), + [today], + ); + /** * Keep track of the displayed month */ - const [month, _setMonth] = useState(getFirstOfUTCMonth(value ?? today)); + const [month, _setMonth] = useState(getMonthFromValue(value)); /** * Keep track of the element the user is highlighting with the keyboard */ - const [highlight, _setHighlight] = useState( + const [highlight, _setHighlight] = useState( getInitialHighlight(value, today, timeZone), ); @@ -92,7 +99,7 @@ export const DatePickerProvider = ({ */ const setValue = (newVal?: DateType) => { _setValue(newVal ?? null); - setMonth(getFirstOfUTCMonth(newVal ?? today)); + setMonth(getMonthFromValue(newVal)); }; /** @@ -105,7 +112,7 @@ export const DatePickerProvider = ({ /** * Set the `highlight` value & handle side effects */ - const setHighlight = useCallback((newHighlight: DateType) => { + const setHighlight = useCallback((newHighlight: Date) => { _setHighlight(newHighlight); }, []); @@ -175,7 +182,7 @@ export const DatePickerProvider = ({ refs.calendarButtonRef.current?.focus(); } // update month to something valid - setMonth(getFirstOfUTCMonth(value ?? today)); + setMonth(getMonthFromValue(value)); // update highlight to something valid setHighlight(getInitialHighlight(value, today, timeZone)); }); @@ -225,9 +232,9 @@ export const DatePickerProvider = ({ */ useEffect(() => { if (!isSameUTCDay(value, prevValue)) { - setMonth(getFirstOfUTCMonth(value ?? today)); + setMonth(getMonthFromValue(value)); } - }, [prevValue, setMonth, today, value]); + }, [getMonthFromValue, prevValue, setMonth, today, value]); return ( void; + setHighlight: (newHighlight: Date) => void; /** * Opens the menu and handles side effects diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index e682bb8a16..9d2b71fd4e 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -6,7 +6,7 @@ import React, { MouseEventHandler, } from 'react'; -import { isSameUTCDay } from '@leafygreen-ui/date-utils'; +import { DateType, isSameUTCDay } from '@leafygreen-ui/date-utils'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; @@ -43,7 +43,7 @@ export const DatePickerInput = forwardRef( } = useDatePickerContext(); /** Called when the input's Date value has changed */ - const handleInputValueChange = (inputVal?: Date | null) => { + const handleInputValueChange = (inputVal?: DateType) => { if (!isSameUTCDay(inputVal, value)) { handleValidation?.(inputVal); setValue(inputVal || null); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx index 6bfd0212e9..04dfec8fa6 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.stories.tsx @@ -6,6 +6,7 @@ import { last, omit } from 'lodash'; import MockDate from 'mockdate'; import { + DateType, Month, newUTC, testLocales, @@ -80,7 +81,7 @@ type DatePickerMenuStoryType = StoryObj; export const Basic: DatePickerMenuStoryType = { render: args => { MockDate.reset(); - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); const date = new Date(Date.now()); const props = omit(args, [...contextPropNames, 'isOpen']); @@ -151,7 +152,7 @@ export const MockedToday: DatePickerMenuStoryType = { render: args => { // Force `new Date()` to return `mockToday` MockDate.set(mockToday); - const [value, setValue] = useState(null); + const [value, setValue] = useState(null); const props = omit(args, [...contextPropNames, 'isOpen']); const refEl = useRef(null); diff --git a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx index 744ed4a530..4537871f66 100644 --- a/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerMenu/DatePickerMenu.tsx @@ -16,6 +16,7 @@ import { isSameTZDay, isSameUTCDay, isSameUTCMonth, + isValidDate, } from '@leafygreen-ui/date-utils'; import { useForwardedRef, usePrevious } from '@leafygreen-ui/hooks'; import { keyMap } from '@leafygreen-ui/lib'; @@ -119,7 +120,11 @@ export const DatePickerMenu = forwardRef( /** Set the highlighted cell when the value changes in the input */ useEffect(() => { - if (value && !isSameUTCDay(value, prevValue) && isInRange(value)) { + if ( + isValidDate(value) && + !isSameUTCDay(value, prevValue) && + isInRange(value) + ) { setHighlight(value); } }, [value, isInRange, setHighlight, prevValue]); @@ -129,7 +134,7 @@ export const DatePickerMenu = forwardRef( */ useEffect(() => { if ( - value && + isValidDate(value) && !isSameUTCDay(value, prevValue) && !isSameUTCMonth(value, month) ) { @@ -243,7 +248,8 @@ export const DatePickerMenu = forwardRef( const handleCalendarKeyDown: KeyboardEventHandler = e => { const { key } = e; - const currentHighlight = highlight || value || today; + const currentHighlight = + highlight || (isValidDate(value) ? value : today); let nextHighlight = currentHighlight; switch (key) { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index 94dfe8c27d..8e16735c09 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -20,15 +20,24 @@ const renderDateInputBox = ( context?: Partial, ) => { const result = render( - - + + , ); + const rerenderDateInputBox = ( + newProps?: Omit, + ) => { + result.rerender( + + + , + ); + }; + const dayInput = result.container.querySelector( 'input[aria-label="day"]', ) as HTMLInputElement; @@ -43,7 +52,7 @@ const renderDateInputBox = ( throw new Error('Some or all input segments are missing'); } - return { ...result, dayInput, monthInput, yearInput }; + return { ...result, rerenderDateInputBox, dayInput, monthInput, yearInput }; }; describe('packages/date-picker/shared/date-input-box', () => { @@ -95,7 +104,7 @@ describe('packages/date-picker/shared/date-input-box', () => { }); }); - test('renders an empty text box when no value is passed', () => { + test('renders empty segments when no props are passed', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( undefined, testContext, @@ -105,7 +114,17 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(yearInput).toHaveValue(''); }); - test('renders a filled text box when value is passed', () => { + test('renders empty segments when value is null', () => { + const { dayInput, monthInput, yearInput } = renderDateInputBox( + { value: null }, + testContext, + ); + expect(dayInput).toHaveValue(''); + expect(monthInput).toHaveValue(''); + expect(yearInput).toHaveValue(''); + }); + + test('renders filled segments when a value is passed', () => { const { dayInput, monthInput, yearInput } = renderDateInputBox( { value: newUTC(1993, Month.December, 26) }, testContext, @@ -115,6 +134,61 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(monthInput.value).toBe('12'); expect(yearInput.value).toBe('1993'); }); + + test('renders empty segments when an invalid value is passed', () => { + const { dayInput, monthInput, yearInput } = renderDateInputBox( + { value: new Date('invalid') }, + testContext, + ); + + expect(dayInput.value).toBe(''); + expect(monthInput.value).toBe(''); + expect(yearInput.value).toBe(''); + }); + + describe('re-rendering', () => { + test('with new value updates the segments', () => { + const { rerenderDateInputBox, dayInput, monthInput, yearInput } = + renderDateInputBox( + { value: newUTC(1993, Month.December, 26) }, + testContext, + ); + + rerenderDateInputBox({ value: newUTC(1994, Month.September, 10) }); + + expect(dayInput.value).toBe('10'); + expect(monthInput.value).toBe('09'); + expect(yearInput.value).toBe('1994'); + }); + + test('with null clears the segments', () => { + const { rerenderDateInputBox, dayInput, monthInput, yearInput } = + renderDateInputBox( + { value: newUTC(1993, Month.December, 26) }, + testContext, + ); + + rerenderDateInputBox({ value: null }); + + expect(dayInput.value).toBe(''); + expect(monthInput.value).toBe(''); + expect(yearInput.value).toBe(''); + }); + + test('with invalid value does not update the segments', () => { + const { rerenderDateInputBox, dayInput, monthInput, yearInput } = + renderDateInputBox( + { value: newUTC(1993, Month.December, 26) }, + testContext, + ); + + rerenderDateInputBox({ value: new Date('invalid') }); + + expect(dayInput.value).toBe('26'); + expect(monthInput.value).toBe('12'); + expect(yearInput.value).toBe('1993'); + }); + }); }); describe('Typing', () => { @@ -137,7 +211,7 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(dayInput.value).toBe('02'); }); - test('deleting characters works as expected', () => { + test('backspace deletes characters', () => { const { dayInput, yearInput } = renderDateInputBox( { value: newUTC(1993, Month.December, 26) }, testContext, @@ -148,7 +222,22 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(yearInput.value).toBe('199'); }); - test('typing into a segment does not immediately fire the value setter', () => { + test('segment change handler is called when typing into a segment', () => { + const { yearInput } = renderDateInputBox( + { + value: null, + onSegmentChange, + }, + testContext, + ); + userEvent.type(yearInput, '1993'); + + expect(onSegmentChange).toHaveBeenCalledWith( + expect.objectContaining({ value: '1993' }), + ); + }); + + test('value setter is not called when typing into a segment', () => { const setValue = jest.fn(); const { dayInput } = renderDateInputBox( { @@ -162,22 +251,22 @@ describe('packages/date-picker/shared/date-input-box', () => { expect(setValue).not.toHaveBeenCalled(); }); - test('typing into a segment fires the segment change handler', () => { - const { yearInput } = renderDateInputBox( + test('value setter is not called when an ambiguous date is entered', () => { + const setValue = jest.fn(); + const { dayInput, monthInput, yearInput } = renderDateInputBox( { value: null, - onSegmentChange, + setValue, }, testContext, ); userEvent.type(yearInput, '1993'); - - expect(onSegmentChange).toHaveBeenCalledWith( - expect.objectContaining({ value: '1993' }), - ); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '2'); + expect(setValue).not.toHaveBeenCalled(); }); - test('value setter is called when a complete date is entered', () => { + test('value setter is only called when an explicit date is entered', () => { const setValue = jest.fn(); const { dayInput, monthInput, yearInput } = renderDateInputBox( { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index 1f084f8435..adf60460b5 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -1,9 +1,14 @@ /* eslint-disable react/prop-types */ import React, { useEffect, useState } from 'react'; import { StoryFn } from '@storybook/react'; -import { isValid } from 'date-fns'; -import { Month, newUTC, testLocales } from '@leafygreen-ui/date-utils'; +import { + DateType, + isValidDate, + Month, + newUTC, + testLocales, +} from '@leafygreen-ui/date-utils'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType, StoryType } from '@leafygreen-ui/lib'; @@ -65,15 +70,15 @@ const meta: StoryMetaType = { export default meta; export const Basic: StoryFn = props => { - const [date, setDate] = useState(null); + const [date, setDate] = useState(null); useEffect(() => { - if (props.value && isValid(new Date(props.value))) { - setDate(new Date(props.value)); + if (props.value && isValidDate(props.value)) { + setDate(props.value); } }, [props.value]); - const updateDate = (date: Date | null) => { + const updateDate = (date: DateType) => { setDate(date); }; diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts index 7a1b8772de..0581d08379 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.types.ts @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; import { AriaLabelPropsWithLabel } from '@leafygreen-ui/a11y'; +import { DateType } from '@leafygreen-ui/date-utils'; import { BaseDatePickerProps, DatePickerState } from '../types'; @@ -10,19 +11,19 @@ export interface StateNotification { state: DatePickerState; message: string; } -type AriaLabelkeys = keyof AriaLabelPropsWithLabel; +type AriaLabelKeys = keyof AriaLabelPropsWithLabel; /** The props expected to pass int the provider */ export type SharedDatePickerProviderProps = Omit< BaseDatePickerProps, - AriaLabelkeys + AriaLabelKeys > & { label?: ReactNode; 'aria-label'?: string; 'aria-labelledby'?: string; }; -type AriaLabelkeysWithoutLabel = Exclude; +type AriaLabelKeysWithoutLabel = Exclude; /** * The values in context @@ -30,7 +31,7 @@ type AriaLabelkeysWithoutLabel = Exclude; export interface SharedDatePickerContextProps extends Omit< Required, - 'state' | AriaLabelkeysWithoutLabel + 'state' | AriaLabelKeysWithoutLabel >, UseDatePickerErrorNotificationsReturnObject { /** The earliest date accepted */ @@ -42,7 +43,7 @@ export interface SharedDatePickerContextProps /** * Returns whether the given date is within the component's min/max dates */ - isInRange: (d?: Date | null) => boolean; + isInRange: (d?: DateType) => boolean; /** * An array of {@link Intl.DateTimeFormatPart}, diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts index f762191fdc..e7d4981ae5 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.utils.ts @@ -2,7 +2,12 @@ import { isBefore, isWithinInterval } from 'date-fns'; import defaults from 'lodash/defaults'; import defaultTo from 'lodash/defaultTo'; -import { getISODate, toDate } from '@leafygreen-ui/date-utils'; +import { + DateType, + getISODate, + isValidDate, + toDate, +} from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; import { BaseFontSize, Size } from '@leafygreen-ui/tokens'; @@ -79,9 +84,9 @@ export const defaultSharedDatePickerContext: SharedDatePickerContextProps = { */ export const getIsInRange = (min: Date, max: Date) => - (d?: Date | null): boolean => + (d?: DateType): boolean => !!( - d && + isValidDate(d) && isWithinInterval(d, { start: min, end: max, diff --git a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts new file mode 100644 index 0000000000..7a4c2e6d25 --- /dev/null +++ b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts @@ -0,0 +1,201 @@ +import { renderHook } from '@testing-library/react'; + +import { DateType, Month, newUTC } from '@leafygreen-ui/date-utils'; + +import { useDateSegments } from './useDateSegments'; +import { OnUpdateCallback } from './useDateSegments.types'; + +const renderUseDateSegmentsHook = ( + initialDate: DateType, + callback?: OnUpdateCallback, +) => { + return renderHook( + props => useDateSegments(props.date, { onUpdate: props.callback }), + { + initialProps: { + date: initialDate, + callback, + }, + }, + ); +}; + +describe('packages/date-picker/shared/useDateSegments', () => { + describe('initial render', () => { + test('returns segments object and setter function', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { result } = renderUseDateSegmentsHook(testDate, callback); + + const { segments, setSegment } = result.current; + + expect(segments).toBeDefined(); + expect(setSegment).toBeDefined(); + expect(segments.day).toEqual('26'); + expect(segments.month).toEqual('12'); + expect(segments.year).toEqual('2023'); + }); + + test('returns empty segments when date is null', () => { + const callback = jest.fn(); + const { result } = renderUseDateSegmentsHook(null, callback); + + const { segments, setSegment } = result.current; + + expect(segments).toBeDefined(); + expect(setSegment).toBeDefined(); + expect(segments.day).toEqual(''); + expect(segments.month).toEqual(''); + expect(segments.year).toEqual(''); + }); + + test('returns empty segments when date is invalid', () => { + const invalidDate = new Date('invalid'); + + const callback = jest.fn(); + const { result } = renderUseDateSegmentsHook(invalidDate, callback); + + const { segments, setSegment } = result.current; + + expect(segments).toBeDefined(); + expect(setSegment).toBeDefined(); + expect(segments.day).toEqual(''); + expect(segments.month).toEqual(''); + expect(segments.year).toEqual(''); + }); + }); + + describe('re-rendering', () => { + describe('with a valid value', () => { + test('calls callback with new segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender } = renderUseDateSegmentsHook(testDate, callback); + + rerender({ date: newUTC(2024, Month.September, 10), callback }); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + day: '10', + month: '09', + year: '2024', + }), + expect.objectContaining({ + day: '26', + month: '12', + year: '2023', + }), + ); + }); + test('returns new segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender, result } = renderUseDateSegmentsHook( + testDate, + callback, + ); + + rerender({ date: newUTC(2024, Month.September, 10), callback }); + expect(result.current.segments.day).toEqual('10'); + expect(result.current.segments.month).toEqual('09'); + expect(result.current.segments.year).toEqual('2024'); + }); + }); + + describe('with a null value', () => { + test('calls callback with empty segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender } = renderUseDateSegmentsHook(testDate, callback); + + rerender({ date: null, callback }); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + day: '', + month: '', + year: '', + }), + expect.objectContaining({ + day: '26', + month: '12', + year: '2023', + }), + ); + }); + test('returns empty segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender, result } = renderUseDateSegmentsHook( + testDate, + callback, + ); + + rerender({ date: null, callback }); + expect(result.current.segments.day).toEqual(''); + expect(result.current.segments.month).toEqual(''); + expect(result.current.segments.year).toEqual(''); + }); + }); + + describe('with an invalid Date value', () => { + test('calls callback with previous segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender } = renderUseDateSegmentsHook(testDate, callback); + + rerender({ date: new Date('invalid'), callback }); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + day: '26', + month: '12', + year: '2023', + }), + expect.objectContaining({ + day: '26', + month: '12', + year: '2023', + }), + ); + }); + test('returns previous segments', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { rerender, result } = renderUseDateSegmentsHook( + testDate, + callback, + ); + + rerender({ date: new Date('invalid'), callback }); + expect(result.current.segments.day).toEqual('26'); + expect(result.current.segments.month).toEqual('12'); + expect(result.current.segments.year).toEqual('2023'); + }); + }); + }); + + describe('setSegment', () => { + test('calls callback when setSegment is called', () => { + const testDate = newUTC(2023, Month.December, 26); + const callback = jest.fn(); + const { result } = renderUseDateSegmentsHook(testDate, callback); + + result.current.setSegment('day', '25'); + + expect(callback).toHaveBeenCalledWith( + expect.objectContaining({ + day: '25', + month: '12', + year: '2023', + }), + expect.objectContaining({ + day: '26', + month: '12', + year: '2023', + }), + 'day', + ); + }); + }); +}); diff --git a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts index e28eecc541..90ecb1b6f7 100644 --- a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts +++ b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.ts @@ -1,7 +1,7 @@ import { useEffect, useReducer } from 'react'; -import { isSameDay } from 'date-fns'; +import { isNull, isUndefined } from 'lodash'; -import { DateType } from '@leafygreen-ui/date-utils'; +import { DateType, isSameUTCDay, isValidDate } from '@leafygreen-ui/date-utils'; import { usePrevious } from '@leafygreen-ui/hooks'; import { DateSegment, DateSegmentsState, DateSegmentValue } from '../../types'; @@ -44,7 +44,15 @@ export const useDateSegments = ( // If `date` prop changes, update the segments useEffect(() => { - if (date && !(prevDate && isSameDay(date, prevDate))) { + // If the date has changed to a valid value + const hasDateValueChanged = + isValidDate(date) && !isSameUTCDay(date, prevDate); + + // If the date has been set to null from a previously valid date + const hasDateBeenCleared = + (isNull(date) || isUndefined(date)) && isValidDate(prevDate); + + if (hasDateValueChanged || hasDateBeenCleared) { const newSegments = getFormattedSegmentsFromDate(date); onUpdate?.(newSegments, { ...segments }); dispatch(newSegments); diff --git a/packages/date-picker/src/shared/utils/getSegmentsFromDate/getFormattedSegmentsFromDate.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getFormattedSegmentsFromDate.ts new file mode 100644 index 0000000000..bcbf01f260 --- /dev/null +++ b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getFormattedSegmentsFromDate.ts @@ -0,0 +1,19 @@ +import { DateType } from '@leafygreen-ui/date-utils'; + +import { DateSegmentsState } from '../../types'; +import { getValueFormatter } from '../getValueFormatter'; + +import { getSegmentsFromDate } from './getSegmentsFromDate'; + +/** Returns a single object with _formatted_ day, month & year segments */ +export const getFormattedSegmentsFromDate = ( + date: DateType, +): DateSegmentsState => { + const segments = getSegmentsFromDate(date); + + return { + day: getValueFormatter('day')(segments['day']), + month: getValueFormatter('month')(segments['month']), + year: getValueFormatter('year')(segments['year']), + }; +}; diff --git a/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts index 418fb428f6..d1fa08a8a5 100644 --- a/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts +++ b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.spec.ts @@ -1,13 +1,13 @@ -import { getSegmentsFromDate } from '.'; +import { getFormattedSegmentsFromDate, getSegmentsFromDate } from '.'; describe('packages/date-picker/utils/getSegmentsFromDate', () => { test('returns UTC d/m/y values', () => { const utc = new Date(Date.UTC(2023, 8, 1)); const segments = getSegmentsFromDate(utc); expect(segments).toBeDefined(); - expect(segments.day).toBe(1); - expect(segments.month).toBe(9); - expect(segments.year).toBe(2023); + expect(segments.day).toBe('1'); + expect(segments.month).toBe('9'); + expect(segments.year).toBe('2023'); }); test('returns empty string for each segment when date is null', () => { @@ -17,4 +17,39 @@ describe('packages/date-picker/utils/getSegmentsFromDate', () => { expect(segments.month).toEqual(''); expect(segments.year).toEqual(''); }); + + test('returns empty string for each segment when date is invalid', () => { + const segments = getSegmentsFromDate(new Date('invalid')); + expect(segments).toBeDefined(); + expect(segments.day).toEqual(''); + expect(segments.month).toEqual(''); + expect(segments.year).toEqual(''); + }); +}); + +describe('packages/date-picker/utils/getFormattedSegmentsFromDate', () => { + test('returns UTC d/m/y values', () => { + const utc = new Date(Date.UTC(2023, 8, 1)); + const segments = getFormattedSegmentsFromDate(utc); + expect(segments).toBeDefined(); + expect(segments.day).toBe('01'); + expect(segments.month).toBe('09'); + expect(segments.year).toBe('2023'); + }); + + test('returns empty string for each segment when date is null', () => { + const segments = getFormattedSegmentsFromDate(null); + expect(segments).toBeDefined(); + expect(segments.day).toEqual(''); + expect(segments.month).toEqual(''); + expect(segments.year).toEqual(''); + }); + + test('returns empty string for each segment when date is invalid', () => { + const segments = getFormattedSegmentsFromDate(new Date('invalid')); + expect(segments).toBeDefined(); + expect(segments.day).toEqual(''); + expect(segments.month).toEqual(''); + expect(segments.year).toEqual(''); + }); }); diff --git a/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.ts new file mode 100644 index 0000000000..cfa06dc9db --- /dev/null +++ b/packages/date-picker/src/shared/utils/getSegmentsFromDate/getSegmentsFromDate.ts @@ -0,0 +1,12 @@ +import { DateType, isValidDate } from '@leafygreen-ui/date-utils'; + +import { DateSegmentsState } from '../../types'; + +/** Returns a single object with day, month & year segments */ +export const getSegmentsFromDate = (date: DateType): DateSegmentsState => { + return { + day: isValidDate(date) ? String(date.getUTCDate()) : '', + month: isValidDate(date) ? String(date.getUTCMonth() + 1) : '', + year: isValidDate(date) ? String(date.getUTCFullYear()) : '', + }; +}; diff --git a/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts b/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts index 24af6209bc..8d4fde2b8d 100644 --- a/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts +++ b/packages/date-picker/src/shared/utils/getSegmentsFromDate/index.ts @@ -1,24 +1,2 @@ -import { DateSegmentsState } from '../../types'; -import { getValueFormatter } from '../getValueFormatter'; - -/** Returns a single object with day, month & year segments */ -export const getSegmentsFromDate = (date: Date | null): DateSegmentsState => { - return { - day: date ? date.getUTCDate() : '', - month: date ? date.getUTCMonth() + 1 : '', - year: date ? date.getUTCFullYear() : '', - } as DateSegmentsState; -}; - -/** Returns a single object with _formatted_ day, month & year segments */ -export const getFormattedSegmentsFromDate = ( - date: Date | null, -): DateSegmentsState => { - const segments = getSegmentsFromDate(date); - - return { - day: getValueFormatter('day')(segments['day']), - month: getValueFormatter('month')(segments['month']), - year: getValueFormatter('year')(segments['year']), - }; -}; +export { getFormattedSegmentsFromDate } from './getFormattedSegmentsFromDate'; +export { getSegmentsFromDate } from './getSegmentsFromDate'; diff --git a/packages/date-utils/src/getISODate/getISODate.ts b/packages/date-utils/src/getISODate/getISODate.ts index 7d40c7a8b2..afce73f6ca 100644 --- a/packages/date-utils/src/getISODate/getISODate.ts +++ b/packages/date-utils/src/getISODate/getISODate.ts @@ -5,7 +5,7 @@ import { DateType } from '../types'; * Returns only the Date portion of the ISOString for a given date * i.e. 2023-11-01T00:00:00.000Z => 2023-11-01 */ -export const getISODate = (date?: DateType): string => { +export const getISODate = (date: DateType): string => { if (!isValidDate(date)) return ''; const isoString = date.toISOString(); diff --git a/packages/date-utils/src/getISODateTZ/getISODateTZ.ts b/packages/date-utils/src/getISODateTZ/getISODateTZ.ts index 754c63b5d7..5a504c82c3 100644 --- a/packages/date-utils/src/getISODateTZ/getISODateTZ.ts +++ b/packages/date-utils/src/getISODateTZ/getISODateTZ.ts @@ -2,12 +2,13 @@ import { addMilliseconds } from 'date-fns'; import { getTimezoneOffset } from 'date-fns-tz'; import { getISODate } from '../getISODate'; +import { isValidDate } from '../isValidDate'; import { DateType } from '../types'; export const getISODateTZ = (date: DateType, timeZone: string) => { const offsetMs = getTimezoneOffset(timeZone); - if (!date || isNaN(offsetMs)) return getISODate(date); + if (!isValidDate(date) || isNaN(offsetMs)) return getISODate(date); // a date object that, when printed in ISO format, // _looks like_ the local time for the given time zone. diff --git a/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts index 698fcb4364..18acfd16f2 100644 --- a/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts +++ b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.spec.ts @@ -8,17 +8,22 @@ describe('packages/date-utils/isCurrentUTCDay', () => { jest.useFakeTimers(); }); - test('returns true when given UTC dates', () => { + test('returns true with UTC dates', () => { const midnightUTC = newUTC(2020, Month.December, 25, 0, 0); const elevenUTC = newUTC(2020, Month.December, 25, 23, 59); jest.setSystemTime(midnightUTC); expect(isCurrentUTCDay(elevenUTC)).toBe(true); }); - test('returns false when different UTC dates', () => { + test('returns false with different UTC dates', () => { const midnightUTC = newUTC(2020, Month.December, 25, 0, 0); const elevenUTC_prev = newUTC(2020, Month.December, 24, 23, 59); jest.setSystemTime(midnightUTC); expect(isCurrentUTCDay(elevenUTC_prev)).toBe(false); }); + + test('returns false with invalid dates', () => { + const date = new Date('invalid'); + expect(isCurrentUTCDay(date)).toBe(false); + }); }); diff --git a/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts index 130f91de57..0c65a5db92 100644 --- a/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts +++ b/packages/date-utils/src/isCurrentUTCDay/isCurrentUTCDay.ts @@ -1,11 +1,12 @@ import { isSameUTCDay } from '../isSameUTCDay'; +import { DateType } from '../types'; /** * Returns whether a given day is today, in UTC * * Compare to `date-fns.isToday`, which compares using local time */ -export const isCurrentUTCDay = (day?: Date | null): day is Date => { +export const isCurrentUTCDay = (day?: DateType): day is Date => { const today = new Date(Date.now()); return isSameUTCDay(day, today); }; diff --git a/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts index 9d744b0dcc..be1e7fa260 100644 --- a/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts +++ b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.spec.ts @@ -19,4 +19,10 @@ describe('packages/date-utils/isOnOrAfter', () => { test('d1 is before d2', () => { expect(isOnOrAfter(jan, jul)).toBe(false); }); + + test('returns false when one or both dates is invalid', () => { + expect(isOnOrAfter(new Date(), new Date('invalid'))).toBe(false); + expect(isOnOrAfter(new Date('invalid'), new Date())).toBe(false); + expect(isOnOrAfter(new Date('invalid'), new Date('invalid'))).toBe(false); + }); }); diff --git a/packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts index ccf012439b..5327c48960 100644 --- a/packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts +++ b/packages/date-utils/src/isOnOrAfter/isOnOrAfter.ts @@ -1,8 +1,14 @@ import { isAfter } from 'date-fns'; import { isSameUTCDay } from '../isSameUTCDay'; +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; // TODO: tests -export const isOnOrAfter = (day: Date, dayToCompare: Date) => { - return isSameUTCDay(day, dayToCompare) || isAfter(day, dayToCompare); +export const isOnOrAfter = (day: DateType, dayToCompare: Date) => { + return ( + isValidDate(day) && + isValidDate(dayToCompare) && + (isSameUTCDay(day, dayToCompare) || isAfter(day, dayToCompare)) + ); }; diff --git a/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts index d0135000f5..7ce2495533 100644 --- a/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts +++ b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.spec.ts @@ -19,4 +19,10 @@ describe('packages/date-utils/isOnOrBefore', () => { test('d1 is after as d2', () => { expect(isOnOrBefore(dec, jul)).toBe(false); }); + + test('returns false when one or both dates is invalid', () => { + expect(isOnOrBefore(new Date(), new Date('invalid'))).toBe(false); + expect(isOnOrBefore(new Date('invalid'), new Date())).toBe(false); + expect(isOnOrBefore(new Date('invalid'), new Date('invalid'))).toBe(false); + }); }); diff --git a/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts index 3fcc704466..c8a9acc167 100644 --- a/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts +++ b/packages/date-utils/src/isOnOrBefore/isOnOrBefore.ts @@ -1,7 +1,13 @@ import { isBefore } from 'date-fns'; import { isSameUTCDay } from '../isSameUTCDay'; +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; -export const isOnOrBefore = (day: Date, dayToCompare: Date) => { - return isSameUTCDay(day, dayToCompare) || isBefore(day, dayToCompare); +export const isOnOrBefore = (day: DateType, dayToCompare: DateType) => { + return ( + isValidDate(day) && + isValidDate(dayToCompare) && + (isSameUTCDay(day, dayToCompare) || isBefore(day, dayToCompare)) + ); }; diff --git a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts index 256bda59f9..3edfb4c3d6 100644 --- a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts +++ b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts @@ -1,10 +1,13 @@ -import tzMock from 'timezone-mock'; +import { mockTimeZone } from '../testing/mockTimeZone'; import { isSameUTCDay } from '.'; describe('packages/date-utils/isSameUTCDay', () => { beforeEach(() => { - tzMock.register('US/Eastern'); + mockTimeZone('America/New_York', -5); + }); + afterEach(() => { + jest.resetAllMocks(); }); describe(' when both dates are defined in UTC', () => { @@ -54,4 +57,10 @@ describe('packages/date-utils/isSameUTCDay', () => { expect(isSameUTCDay(null, new Date())).toBe(false); expect(isSameUTCDay(null, null)).toBe(false); }); + + test('returns false when one or both dates is invalid', () => { + expect(isSameUTCDay(new Date(), new Date('invalid'))).toBe(false); + expect(isSameUTCDay(new Date('invalid'), new Date())).toBe(false); + expect(isSameUTCDay(new Date('invalid'), new Date('invalid'))).toBe(false); + }); }); diff --git a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts index 015b1ea4a3..7df93d93bb 100644 --- a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts +++ b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.ts @@ -1,13 +1,13 @@ +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; + /** * Returns whether 2 Dates are the same day in UTC. * * Compare to `date-fns.isSameDay`, which uses local time */ -export const isSameUTCDay = ( - day1?: Date | null, - day2?: Date | null, -): boolean => { - if (!day1 || !day2) return false; +export const isSameUTCDay = (day1?: DateType, day2?: DateType): boolean => { + if (!isValidDate(day1) || !isValidDate(day2)) return false; return ( day1.getUTCDate() === day2.getUTCDate() && diff --git a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts index 8e648e00b9..2bcfa91693 100644 --- a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts +++ b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts @@ -1,12 +1,14 @@ -import tzMock from 'timezone-mock'; - import { Month } from '../constants'; +import { mockTimeZone } from '../testing/mockTimeZone'; import { isSameUTCMonth } from '.'; describe('packages/date-utils/isSameUTCMonth', () => { beforeEach(() => { - tzMock.register('US/Eastern'); + mockTimeZone('America/New_York', -5); + }); + afterEach(() => { + jest.resetAllMocks(); }); describe('when both dates are defined in UTC', () => { @@ -62,4 +64,12 @@ describe('packages/date-utils/isSameUTCMonth', () => { const utc2 = new Date(Date.UTC(2024, Month.September, 10)); expect(isSameUTCMonth(utc1, utc2)).toBe(false); }); + + test('returns false when one or both dates is invalid', () => { + expect(isSameUTCMonth(new Date(), new Date('invalid'))).toBe(false); + expect(isSameUTCMonth(new Date('invalid'), new Date())).toBe(false); + expect(isSameUTCMonth(new Date('invalid'), new Date('invalid'))).toBe( + false, + ); + }); }); diff --git a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts index ac937a09ff..fad9583b56 100644 --- a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts +++ b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.ts @@ -1,13 +1,13 @@ +import { isValidDate } from '../isValidDate'; +import { DateType } from '../types'; + /** * Returns whether 2 Dates are the same Month in UTC. * * Compare to `date-fns.isSameDay`, which uses local time */ -export const isSameUTCMonth = ( - day1?: Date | null, - day2?: Date | null, -): boolean => { - if (!day1 || !day2) return false; +export const isSameUTCMonth = (day1?: DateType, day2?: DateType): boolean => { + if (!isValidDate(day1) || !isValidDate(day2)) return false; return ( day1.getUTCMonth() === day2.getUTCMonth() && diff --git a/packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts b/packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts index 2fb6b1f514..2d91041dd7 100644 --- a/packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts +++ b/packages/date-utils/src/isSameUTCRange/isSameUTCRange.ts @@ -1,10 +1,11 @@ import isUndefined from 'lodash/isUndefined'; import { isSameUTCDay } from '../isSameUTCDay'; +import { DateType } from '../types'; export const isSameUTCRange = ( - range1?: [Date | null, Date | null], - range2?: [Date | null, Date | null], + range1?: [DateType, DateType], + range2?: [DateType, DateType], ): boolean => { if (isUndefined(range1) || isUndefined(range2)) return false; diff --git a/packages/date-utils/src/isValidDate/index.ts b/packages/date-utils/src/isValidDate/index.ts index 960580c147..5afda48f1d 100644 --- a/packages/date-utils/src/isValidDate/index.ts +++ b/packages/date-utils/src/isValidDate/index.ts @@ -1,5 +1 @@ -export { - isValidDate, - isValidDateObject, - isValidDateString, -} from './isValidDate'; +export { isValidDate, isValidDateString } from './isValidDate'; diff --git a/packages/date-utils/src/isValidDate/isValidDate.spec.ts b/packages/date-utils/src/isValidDate/isValidDate.spec.ts index 854974cd24..801e4afdb9 100644 --- a/packages/date-utils/src/isValidDate/isValidDate.spec.ts +++ b/packages/date-utils/src/isValidDate/isValidDate.spec.ts @@ -1,3 +1,5 @@ +import { mockTimeZone } from '../testing/mockTimeZone'; + import { isValidDate, isValidDateString } from '.'; describe('packages/date-utils/isValidDate', () => { @@ -6,31 +8,35 @@ describe('packages/date-utils/isValidDate', () => { expect(isValidDate(new Date(Date.UTC(2023, 1, 1)))).toBe(true); }); - test('accepts numbers', () => { - expect(isValidDate(Date.now())).toBe(true); - expect(isValidDate(0)).toBe(true); + test('accepts Date objects when the time zone is mocked', () => { + mockTimeZone('America/Los_Angeles', -8); + expect(isValidDate(new Date())).toBe(true); + jest.resetAllMocks(); }); - test('accepts strings', () => { - expect(isValidDate('1993-12-26')).toBe(true); - expect(isValidDate('not a date')).toBe(false); + test('rejects invalid date objects', () => { + expect(isValidDate(new Date('invalid'))).toBe(false); + expect(isValidDate({} as Date)).toBe(false); }); - test('accepts null', () => { + test('rejects undefined', () => { + expect(isValidDate(undefined)).toBe(false); + }); + test('rejects null', () => { expect(isValidDate(null)).toBe(false); }); +}); - describe('isValidDateString', () => { - test('us format is valid', () => { - expect(isValidDateString('12/26/1993')).toBeTruthy(); - }); +describe('packages/date-utils/isValidDateString', () => { + test('us format is valid', () => { + expect(isValidDateString('12/26/1993')).toBeTruthy(); + }); - test('iso format is valid', () => { - expect(isValidDateString('1993-12-26')).toBeTruthy(); - }); + test('iso format is valid', () => { + expect(isValidDateString('1993-12-26')).toBeTruthy(); + }); - test('undefined format is not valid', () => { - expect(isValidDateString(undefined)).toBeFalsy(); - }); + test('undefined format is not valid', () => { + expect(isValidDateString(undefined)).toBeFalsy(); }); }); diff --git a/packages/date-utils/src/isValidDate/isValidDate.ts b/packages/date-utils/src/isValidDate/isValidDate.ts index 2eba483799..274b204310 100644 --- a/packages/date-utils/src/isValidDate/isValidDate.ts +++ b/packages/date-utils/src/isValidDate/isValidDate.ts @@ -2,31 +2,31 @@ import { isValid } from 'date-fns'; import isNull from 'lodash/isNull'; import isUndefined from 'lodash/isUndefined'; -export const isValidDate = ( - maybeDate?: Date | string | number | null, -): maybeDate is Date | string => { - if (isUndefined(maybeDate) || isNull(maybeDate)) return false; +import { DateType } from '../types'; - switch (typeof maybeDate) { - case 'string': - return isValidDateString(maybeDate); - case 'number': - return isValid(maybeDate); +/** + * An extension of `date-fns` {@link isValid} + * that accepts a {@link DateType} + */ +export const isValidDate = (date?: DateType): date is Date => { + // Enumerating all cases to ensure test coverage + if (isUndefined(date)) return false; + if (isNull(date)) return false; + if (date.constructor.name !== 'Date') return false; - default: - return isValidDateObject(maybeDate); + try { + date?.toISOString(); + return isValid(date); + } catch (error) { + return false; } }; -export const isValidDateObject = (date?: any): date is Date => { - return ( - !isUndefined(date) && - typeof date === 'object' && - date.constructor.name === 'Date' - ); -}; - -/** Returns whether the provided string is a valid date */ +/** + * Returns whether the provided string is a valid date + * + * @deprecated Prefer {@link isValid} from `date-fns` + */ export const isValidDateString = (str?: any): str is string => { return ( !isUndefined(str) && typeof str === 'string' && !isNaN(Date.parse(str)) diff --git a/packages/date-utils/src/toDate/toDate.ts b/packages/date-utils/src/toDate/toDate.ts index 00fc82df73..61951fc1d5 100644 --- a/packages/date-utils/src/toDate/toDate.ts +++ b/packages/date-utils/src/toDate/toDate.ts @@ -2,12 +2,10 @@ import { isValid, toDate as _toDate } from 'date-fns'; import isNull from 'lodash/isNull'; import isUndefined from 'lodash/isUndefined'; -import { isValidDateObject } from '../isValidDate'; - /** A wrapper around `date-fns.toDate` that also accepts strings */ export function toDate(dateLike?: string | number | Date | null): Date | null { if (isUndefined(dateLike) || isNull(dateLike)) return null; - if (isValidDateObject(dateLike)) return new Date(dateLike); + if (isValid(dateLike)) return new Date(dateLike); if (typeof dateLike === 'number') return _toDate(dateLike); const newDate = new Date(dateLike); return isValid(newDate) ? newDate : null; diff --git a/packages/date-utils/src/types/InvalidDate.ts b/packages/date-utils/src/types/InvalidDate.ts new file mode 100644 index 0000000000..8f1d4d8bd0 --- /dev/null +++ b/packages/date-utils/src/types/InvalidDate.ts @@ -0,0 +1,52 @@ +export type InvalidDate = Omit< + Date, + // | 'getUTCDate' + // | 'getUTCDay' + // | 'getUTCFullYear' + // | 'getUTCHours' + // | 'getUTCMilliseconds' + // | 'getUTCMinutes' + // | 'getUTCMonth' + // | 'getUTCSeconds' + // | 'getDate' + // | 'getDay' + // | 'getFullYear' + // | 'getHours' + // | 'getMilliseconds' + // | 'getMinutes' + // | 'getMonth' + // | 'getSeconds' + | 'toLocaleDateString' + | 'toLocaleString' + | 'toLocaleTimeString' + | 'toString' + | 'toTimeString' + | 'toUTCString' + | 'toISOString' + | 'toJSON' +> & { + // getUTCDate: () => number | undefined; + // getUTCDay: () => number | undefined; + // getUTCFullYear: () => number | undefined; + // getUTCHours: () => number | undefined; + // getUTCMilliseconds: () => number | undefined; + // getUTCMinutes: () => number | undefined; + // getUTCMonth: () => number | undefined; + // getUTCSeconds: () => number | undefined; + // getDate: () => number | undefined; + // getDay: () => number | undefined; + // getFullYear: () => number | undefined; + // getHours: () => number | undefined; + // getMilliseconds: () => number | undefined; + // getMinutes: () => number | undefined; + // getMonth: () => number | undefined; + // getSeconds: () => number | undefined; + toLocaleDateString: () => 'Invalid Date'; + toLocaleString: () => 'Invalid Date'; + toLocaleTimeString: () => 'Invalid Date'; + toString: () => 'Invalid Date'; + toTimeString: () => 'Invalid Date'; + toUTCString: () => 'Invalid Date'; + toISOString: () => Error; + toJSON: () => null; +}; diff --git a/packages/date-utils/src/types.ts b/packages/date-utils/src/types/index.ts similarity index 88% rename from packages/date-utils/src/types.ts rename to packages/date-utils/src/types/index.ts index 2bb0b1c94a..357b7b46e5 100644 --- a/packages/date-utils/src/types.ts +++ b/packages/date-utils/src/types/index.ts @@ -1,4 +1,5 @@ -export type DateType = Date | null; +import { InvalidDate } from './InvalidDate'; +export type DateType = Date | InvalidDate | null; export type DateRangeType = [DateType, DateType]; export type LocaleString = 'iso8601' | string; From 09ae2156019d7e934ae791a70234f88b2f91fdf3 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Fri, 5 Jan 2024 16:36:08 -0500 Subject: [PATCH 340/351] move menu position (#2157) --- .../shared/components/DateInput/DateFormField/DateFormField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx index 3f5aa93f07..624f7c600d 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateFormField/DateFormField.tsx @@ -41,7 +41,6 @@ export const DateFormField = React.forwardRef< return ( Date: Mon, 8 Jan 2024 13:53:39 -0800 Subject: [PATCH 341/351] creates newTZDate --- packages/date-utils/src/index.ts | 1 + packages/date-utils/src/newTZDate/index.ts | 1 + .../src/newTZDate/newTZDate.spec.ts | 44 +++++++++++++++++++ .../date-utils/src/newTZDate/newTZDate.ts | 11 +++++ 4 files changed, 57 insertions(+) create mode 100644 packages/date-utils/src/newTZDate/index.ts create mode 100644 packages/date-utils/src/newTZDate/newTZDate.spec.ts create mode 100644 packages/date-utils/src/newTZDate/newTZDate.ts diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index e9f4c920fe..6f79645133 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -29,6 +29,7 @@ export { isValidDate } from './isValidDate'; export { isValidLocale } from './isValidLocale'; export { maxDate } from './maxDate'; export { minDate } from './minDate'; +export { newTZDate } from './newTZDate'; export { newUTC } from './newUTC'; export { setToUTCMidnight } from './setToUTCMidnight'; export { setUTCDate } from './setUTCDate'; diff --git a/packages/date-utils/src/newTZDate/index.ts b/packages/date-utils/src/newTZDate/index.ts new file mode 100644 index 0000000000..ac631bccf1 --- /dev/null +++ b/packages/date-utils/src/newTZDate/index.ts @@ -0,0 +1 @@ +export { newTZDate } from './newTZDate'; diff --git a/packages/date-utils/src/newTZDate/newTZDate.spec.ts b/packages/date-utils/src/newTZDate/newTZDate.spec.ts new file mode 100644 index 0000000000..7879bea539 --- /dev/null +++ b/packages/date-utils/src/newTZDate/newTZDate.spec.ts @@ -0,0 +1,44 @@ +import { Month } from '../constants'; + +import { newTZDate } from './newTZDate'; + +describe('packages/date-utils/newTZDate', () => { + test('creates a UTC date when 0 offset is provided', () => { + const date = newTZDate(0, 2020, Month.January, 5); + expect(date.getUTCFullYear()).toBe(2020); + expect(date.getUTCMonth()).toBe(0); + expect(date.getUTCDate()).toBe(5); + expect(date.getUTCHours()).toBe(0); + }); + + test('positive UTC offset (2020-01-05 UTC => 2020-01-04T19:00 UTC+5)', () => { + const date = newTZDate(5, 2020, Month.January, 5); + expect(date.getUTCFullYear()).toBe(2020); + expect(date.getUTCMonth()).toBe(0); + expect(date.getUTCDate()).toBe(4); + expect(date.getUTCHours()).toBe(19); + }); + + test('positive UTC offset with hours (2020-01-05T23:00 UTC => 2020-01-05T18:00 UTC+5)', () => { + const date = newTZDate(5, 2020, Month.January, 5, 23); + expect(date.getUTCFullYear()).toBe(2020); + expect(date.getUTCMonth()).toBe(0); + expect(date.getUTCDate()).toBe(5); + expect(date.getUTCHours()).toBe(18); + }); + + test('negative UTC offset (2020-01-05 UTC => 2020-01-05T05:00 UTC-5)', () => { + const date = newTZDate(-5, 2020, Month.January, 5); + expect(date.getUTCFullYear()).toBe(2020); + expect(date.getUTCMonth()).toBe(0); + expect(date.getUTCDate()).toBe(5); + expect(date.getUTCHours()).toBe(5); + }); + test('negative UTC offset with hours (2020-01-05T23:00 UTC => 2020-01-06T04:00 UTC-5)', () => { + const date = newTZDate(-5, 2020, Month.January, 5, 23); + expect(date.getUTCFullYear()).toBe(2020); + expect(date.getUTCMonth()).toBe(0); + expect(date.getUTCDate()).toBe(6); + expect(date.getUTCHours()).toBe(4); + }); +}); diff --git a/packages/date-utils/src/newTZDate/newTZDate.ts b/packages/date-utils/src/newTZDate/newTZDate.ts new file mode 100644 index 0000000000..b0d36d5cc9 --- /dev/null +++ b/packages/date-utils/src/newTZDate/newTZDate.ts @@ -0,0 +1,11 @@ +/** + * Creates a Date object for a given UTC offset + */ +export const newTZDate = ( + UTCOffset = 0, + ...args: Parameters +): Date => { + const [y, m, d, h, ...rest] = args; + const hour = (h ?? 0) - UTCOffset; + return new Date(Date.UTC(y, m, d, hour, ...rest)); +}; From f5f315f0062331eb99b821443a5dbfa52577d55c Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 8 Jan 2024 14:00:49 -0800 Subject: [PATCH 342/351] updates isSameUTCDay & isSameUTCMonth tests --- .../src/isSameUTCDay/isSameUTCDay.spec.ts | 33 +++++++++++------- .../src/isSameUTCMonth/isSameUTCMonth.spec.ts | 34 +++++++++++-------- .../date-utils/src/newTZDate/newTZDate.ts | 6 +++- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts index 3edfb4c3d6..d6385c4c52 100644 --- a/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts +++ b/packages/date-utils/src/isSameUTCDay/isSameUTCDay.spec.ts @@ -1,7 +1,11 @@ +import { newTZDate } from '../newTZDate'; +import { newUTC } from '../newUTC'; import { mockTimeZone } from '../testing/mockTimeZone'; import { isSameUTCDay } from '.'; +const TZOffset = -5; + describe('packages/date-utils/isSameUTCDay', () => { beforeEach(() => { mockTimeZone('America/New_York', -5); @@ -10,44 +14,47 @@ describe('packages/date-utils/isSameUTCDay', () => { jest.resetAllMocks(); }); - describe(' when both dates are defined in UTC', () => { + describe('when both dates are defined in UTC', () => { test('returns true', () => { - const utc1 = new Date(Date.UTC(2023, 8, 1, 0, 0, 0)); - const utc2 = new Date(Date.UTC(2023, 8, 1, 21, 0, 0)); + const utc1 = newUTC(2023, 8, 1, 0, 0, 0); + const utc2 = newUTC(2023, 8, 1, 21, 0, 0); expect(isSameUTCDay(utc1, utc2)).toBe(true); }); test('returns false', () => { - const utc1 = new Date(Date.UTC(2023, 8, 1, 0, 0, 0)); - const utc2 = new Date(Date.UTC(2023, 8, 2, 0, 0, 0)); + const utc1 = newUTC(2023, 8, 1, 0, 0, 0); + const utc2 = newUTC(2023, 8, 2, 0, 0, 0); expect(isSameUTCDay(utc1, utc2)).toBe(false); }); }); describe('when one date is defined locally', () => { test('returns true ', () => { - const utc = new Date(Date.UTC(2023, 8, 10, 0, 0, 0)); - const local = new Date('2023-09-09T21:00:00'); //2023-09-10T01:00:00Z + const utc = newUTC(2023, 8, 10, 0, 0, 0); + // '2023-09-09T21:00:00' NY time + const local = newTZDate(TZOffset, 2023, 8, 9, 21, 0); //2023-09-10 02:00:00 UTC + expect(isSameUTCDay(utc, local)).toBe(true); }); test('returns false', () => { - const utc = new Date(Date.UTC(2023, 8, 10)); - const local = new Date('2023-09-09T12:00'); //2023-09-09T16:00:00Z + const utc = newUTC(2023, 8, 10); + // '2023-09-09T12:00' NY time + const local = newTZDate(TZOffset, 2023, 8, 9, 12, 0); //2023-09-09 17:00:00 UTC expect(isSameUTCDay(utc, local)).toBe(false); }); }); describe('when both dates are defined locally', () => { test('returns true', () => { - const local1 = new Date('2023-09-09T00:00'); - const local2 = new Date('2023-09-09T19:00'); + const local1 = newTZDate(TZOffset, 2023, 8, 8, 20, 0); // 02:00 UTC + 1day + const local2 = newTZDate(TZOffset, 2023, 8, 9, 18, 0); // 23:00 UTC expect(isSameUTCDay(local1, local2)).toBe(true); }); test('returns false', () => { - const local1 = new Date('2023-09-09T00:00'); - const local2 = new Date('2023-09-09T20:00'); + const local1 = newTZDate(TZOffset, 2023, 8, 9, 0, 0); // 05:00 UTC + const local2 = newTZDate(TZOffset, 2023, 8, 9, 20, 0); // 02:00 UTC +1 day expect(isSameUTCDay(local1, local2)).toBe(false); }); }); diff --git a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts index 2bcfa91693..b10e224ca9 100644 --- a/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts +++ b/packages/date-utils/src/isSameUTCMonth/isSameUTCMonth.spec.ts @@ -1,11 +1,15 @@ import { Month } from '../constants'; +import { newTZDate } from '../newTZDate'; +import { newUTC } from '../newUTC'; import { mockTimeZone } from '../testing/mockTimeZone'; import { isSameUTCMonth } from '.'; +const TZOffset = -5; + describe('packages/date-utils/isSameUTCMonth', () => { beforeEach(() => { - mockTimeZone('America/New_York', -5); + mockTimeZone('America/New_York', TZOffset); }); afterEach(() => { jest.resetAllMocks(); @@ -13,42 +17,42 @@ describe('packages/date-utils/isSameUTCMonth', () => { describe('when both dates are defined in UTC', () => { test('true', () => { - const utc1 = new Date(Date.UTC(2023, Month.September, 1)); - const utc2 = new Date(Date.UTC(2023, Month.September, 10)); + const utc1 = newUTC(2023, Month.September, 1); + const utc2 = newUTC(2023, Month.September, 10); expect(isSameUTCMonth(utc1, utc2)).toBe(true); }); test('false', () => { - const utc1 = new Date(Date.UTC(2023, Month.September, 1)); - const utc2 = new Date(Date.UTC(2023, Month.August, 31)); + const utc1 = newUTC(2023, Month.September, 1); + const utc2 = newUTC(2023, Month.August, 31); expect(isSameUTCMonth(utc1, utc2)).toBe(false); }); }); describe('when one date is defined locally', () => { test('true', () => { - const utc = new Date(Date.UTC(2023, Month.September, 10)); - const local = new Date('2023-08-31T21:00:00'); + const utc = newUTC(2023, Month.September, 10); + const local = newTZDate(TZOffset, 2023, Month.August, 31, 21, 0, 0); expect(isSameUTCMonth(utc, local)).toBe(true); }); test('false', () => { - const utc = new Date(Date.UTC(2023, Month.September, 10)); - const local = new Date('2023-08-31T12:00'); + const utc = newUTC(2023, Month.September, 10); + const local = newTZDate(TZOffset, 2023, Month.August, 31, 12, 0); expect(isSameUTCMonth(utc, local)).toBe(false); }); }); describe(' when both dates are defined locally', () => { test('true', () => { - const local1 = new Date('2023-09-01T00:00'); - const local2 = new Date('2023-09-30T19:00'); + const local1 = newTZDate(TZOffset, 2023, Month.September, 1, 0, 0); + const local2 = newTZDate(TZOffset, 2023, Month.September, 30, 18, 0); expect(isSameUTCMonth(local1, local2)).toBe(true); }); test('false', () => { - const local1 = new Date('2023-09-01T00:00'); - const local2 = new Date('2023-09-30T20:00'); + const local1 = newTZDate(TZOffset, 2023, Month.September, 1, 0, 0); + const local2 = newTZDate(TZOffset, 2023, Month.September, 30, 20, 0); expect(isSameUTCMonth(local1, local2)).toBe(false); }); }); @@ -60,8 +64,8 @@ describe('packages/date-utils/isSameUTCMonth', () => { }); test('false for different years', () => { - const utc1 = new Date(Date.UTC(2023, Month.September, 1)); - const utc2 = new Date(Date.UTC(2024, Month.September, 10)); + const utc1 = newUTC(2023, Month.September, 1); + const utc2 = newUTC(2024, Month.September, 10); expect(isSameUTCMonth(utc1, utc2)).toBe(false); }); diff --git a/packages/date-utils/src/newTZDate/newTZDate.ts b/packages/date-utils/src/newTZDate/newTZDate.ts index b0d36d5cc9..97c67ce2d7 100644 --- a/packages/date-utils/src/newTZDate/newTZDate.ts +++ b/packages/date-utils/src/newTZDate/newTZDate.ts @@ -1,6 +1,10 @@ /** - * Creates a Date object for a given UTC offset + * Creates a Date object for a given UTC offset. + * @internal */ +// This API is less than perfect, +// but we shouldn't be constructing many (if any) +// TZ dependent dates other than for testing export const newTZDate = ( UTCOffset = 0, ...args: Parameters From a117c9b5f2f8c49929598363ef45c48cd2c64150 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 8 Jan 2024 12:30:53 -1000 Subject: [PATCH 343/351] moves special rollover logic to getMaxSegmentValue --- .../DateInput/DateInputBox/DateInputBox.tsx | 18 -------------- .../DateInputSegment/DateInputSegment.tsx | 3 +-- .../getNewSegmentValueFromArrowKeyPress.ts | 19 +++++++++------ .../getMaxSegmentValue.spec.ts | 12 ++++++---- .../shared/utils/getMaxSegmentValue/index.ts | 24 +++++++++++++------ 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index 8067c37d71..7a55005d68 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -1,8 +1,6 @@ import React, { FocusEventHandler } from 'react'; -import { getDaysInMonth } from 'date-fns'; import isEqual from 'lodash/isEqual'; -import { newUTC } from '@leafygreen-ui/date-utils'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -141,22 +139,6 @@ export const DateInputBox = React.forwardRef( const changedViaArrowKeys = meta?.key === keyMap.ArrowDown || meta?.key === keyMap.ArrowUp; - // If we've updated the "day" segment via arrow keys, - // we can update it's rollover behavior based on the month - if (changedViaArrowKeys && segmentName === 'day') { - const year = Number(segments['year']); - const month = Number(segments['month']); - const daysInMonth = getDaysInMonth(newUTC(year, month, 1)); - - if (Number(segmentValue) > daysInMonth) { - if (meta?.key === keyMap.ArrowDown) { - segmentValue = String(daysInMonth); - } else if (meta?.key === keyMap.ArrowUp) { - segmentValue = '01'; - } - } - } - // Auto-format the segment if it is explicit and was not changed via arrow-keys if ( !changedViaArrowKeys && diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx index 57c23f8aac..c722fe5515 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/DateInputSegment.tsx @@ -102,8 +102,6 @@ export const DateInputSegment = React.forwardRef< switch (key) { case keyMap.ArrowUp: case keyMap.ArrowDown: { - /** Fire a custom change event when the up/down arrow keys are pressed */ - e.preventDefault(); const newValue = getNewSegmentValueFromArrowKeyPress({ @@ -115,6 +113,7 @@ export const DateInputSegment = React.forwardRef< }); const valueString = formatter(newValue); + /** Fire a custom change event when the up/down arrow keys are pressed */ onChange({ segment, value: valueString, diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts index c52f81774c..832c7c978a 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputSegment/utils/getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress.ts @@ -2,19 +2,24 @@ import { keyMap, rollover } from '@leafygreen-ui/lib'; import { DateSegment, DateSegmentValue } from '../../../../../types'; +interface DateSegmentKeypressContext { + value: DateSegmentValue; + key: typeof keyMap.ArrowUp | typeof keyMap.ArrowDown; + segment: DateSegment; + min: number; + max: number; +} + +/** + * Returns a new segment value given the current state + */ export const getNewSegmentValueFromArrowKeyPress = ({ value, key, segment, min, max, -}: { - value: DateSegmentValue; - key: typeof keyMap.ArrowUp | typeof keyMap.ArrowDown; - segment: DateSegment; - min: number; - max: number; -}) => { +}: DateSegmentKeypressContext): number => { const valueDiff = key === keyMap.ArrowUp ? 1 : -1; const defaultVal = key === keyMap.ArrowUp ? min : max; diff --git a/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts b/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts index 8fe7cbbf80..68b1baf519 100644 --- a/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts +++ b/packages/date-picker/src/shared/utils/getMaxSegmentValue/getMaxSegmentValue.spec.ts @@ -2,16 +2,20 @@ import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { getMaxSegmentValue } from '.'; -describe('packages/date-picker/utils/getMinSegmentValue', () => { +describe('packages/date-picker/utils/getMaxSegmentValue', () => { describe('day', () => { - test('returns 1 by default', () => { + test('returns 31 by default', () => { expect(getMaxSegmentValue('day')).toBe(31); }); - test.todo('returns max day in provided month'); + test('returns max day in provided month', () => { + expect( + getMaxSegmentValue('day', { date: newUTC(2023, Month.February, 14) }), + ).toBe(28); + }); }); describe('month', () => { - test('returns 1 by default', () => { + test('returns 12 by default', () => { expect(getMaxSegmentValue('month')).toBe(12); }); }); diff --git a/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts b/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts index 96882e0db0..a33974f57e 100644 --- a/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts +++ b/packages/date-picker/src/shared/utils/getMaxSegmentValue/index.ts @@ -1,4 +1,8 @@ -import { DateType } from '@leafygreen-ui/date-utils'; +import { + DateType, + getDaysInUTCMonth, + isValidDate, +} from '@leafygreen-ui/date-utils'; import { defaultMax } from '../../constants'; import { DateSegment } from '../../types'; @@ -12,11 +16,17 @@ export const getMaxSegmentValue = ( max?: Date; }, ): number => { - if (segment === 'year') { - return context?.max - ? Number(getSegmentsFromDate(context.max)['year']) - : defaultMax['year']; - } + switch (segment) { + case 'year': + return context?.max + ? Number(getSegmentsFromDate(context.max)['year']) + : defaultMax['year']; + case 'month': + return defaultMax['month']; - return defaultMax[segment]; + case 'day': + return context && isValidDate(context?.date) + ? getDaysInUTCMonth(context.date) + : defaultMax['day']; + } }; From 429fcaa213bb3daeb316f1ab9b09e9eb95926738 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Thu, 11 Jan 2024 12:48:06 -0500 Subject: [PATCH 344/351] add readme (#2164) --- packages/date-picker/README.md | 45 +++++++++++++++++++ .../shared/types/BaseDatePickerProps.types.ts | 6 ++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/date-picker/README.md b/packages/date-picker/README.md index f6e3e2c39d..b79b6c888c 100644 --- a/packages/date-picker/README.md +++ b/packages/date-picker/README.md @@ -17,3 +17,48 @@ yarn add @leafygreen-ui/date-picker ```shell npm install @leafygreen-ui/date-picker ``` + +## Example + +```js +import { DatePicker } from '@leafygreen-ui/date-picker'; +import { setToUTCMidnight } from '@leafygreen-ui/date-utils'; + + {}} + locale="iso8601" + timeZone="utc" +/>; +``` + +## Properties + +| Prop | Type | Description | Default | +| ------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `label` | `ReactNode` | Label shown above the number input. | | +| `description` | `ReactNode` | A description for the date picker. It's recommended to set a meaningful time zone representation as the description. (e.g. "Coordinated Universal Time") | | +| `locale` | `'iso8601'`\| `'string'` | Sets the _presentation format_ for the displayed date, and localizes month & weekday labels. Defaults to the user’s browser preference (if available), otherwise ISO-8601. | `iso8601` | +| `timeZone` | `string` | A valid IANA timezone string, or UTC offset, used to calculate initial values. Defaults to the user’s browser settings. | +| `min` | `Date` | The earliest date accepted, in UTC | | +| `max` | `Date` | The latest date accepted, in UTC | | +| `value` | `'Date'` \| `'InvalidDate'` \| `'null'` | The selected date. Note that this Date object will be read as UTC time. Providing `Date.now()` could result in the incorrect date being displayed, depending on the system time zone.

To set `value` to today, regardless of timeZone, use `setToUTCMidnight(new Date(Date.now()))`.

e.g. `2023-12-31` at 20:00 in Los Angeles, will be `2024-01-01` at 04:00 in UTC. To set the correct day (`2023-12-31`) as the DatePicker value we must first convert our local timestamp to `2023-12-31` at midnight | | +| `onDateChange` | `(value?: Date \| InvalidDate \| null) => void` | Callback fired when the user makes a value change. Fired on click of a new date in the menu, or on keydown if the input contains a valid date.

_Not_ fired when a date segment changes, but does not create a full date

Callback date argument will be a Date object in UTC time, or `null` | | +| `initialValue` | `'Date'` \| `'InvalidDate'` \| `'null'` | The initial selected date. Ignored if `value` is provided

Note that this Date object will be read as UTC time. See `value` prop documentation for more details | | +| `handleValidation` | `(value?: Date \| InvalidDate \| null) => void` | A callback fired when validation should run, based on [form validation guidelines](https://www.mongodb.design/foundation/forms/#form-validation-error-handling). Use this callback to compute the correct `state` and `errorMessage` value.

Callback date argument will be a Date object in UTC time, or `null` | | +| `onChange` | `(event: ChangeEvent) => void` | Callback fired when any segment changes, (but not necessarily a full value) | | +| `baseFontSize` | `'13'` \| `'16'` | The base font size of the input. Inherits from the nearest LeafyGreenProvider | | +| `disabled` | `boolean` | Whether the input is disabled. _Note_: will not set the `disabled` attribute on an input and the calendar menu will not open if disabled is set to true. | `false` | +| `size` | `'small'` \| `'xsmall'` \| `'default'` \| `'large'` | Whether the input is disabled. Note: will not set the `disabled` attribute on an input and the calendar menu will not open if disabled is set to true. | `default` | +| `state` | `'none'` \| `'error'` | Whether to show an error message | `none` | +| `errorMessage` | `string` | A message to show in red underneath the input when state is `Error` | | +| `initialOpen` | `boolean` | Whether the calendar menu is initially open. _Note_: The calendar menu will not open if disabled is set to `true`. | `false` | +| `autoComplete` | `'off'` \| `'on'` \| `'bday'` | Whether the input should autofill | `off` | +| `darkMode` | `boolean` | Render the component in dark mode. | `false` | + +## Special Case + +### Aria Labels + +Either `label` or `aria-labelledby` or `aria-label` must be provided, or there will be a console error. This is to ensure that screenreaders have a description for what the DatePicker does. diff --git a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts index 273cda29a9..db4064906e 100644 --- a/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts +++ b/packages/date-picker/src/shared/types/BaseDatePickerProps.types.ts @@ -56,16 +56,20 @@ export type BaseDatePickerProps = { /** * The size of the input + * + * @default 'default' */ size?: Size; /** * Whether to show an error message + * + * @default 'none' */ state?: DatePickerState; /** - * A message to show in red underneath the input when state is Error + * A message to show in red underneath the input when state is `Error` */ errorMessage?: string; From d548aca9623204121fe5d5d1b46e5eeb6fdb70e9 Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Thu, 11 Jan 2024 13:04:19 -0500 Subject: [PATCH 345/351] update README --- packages/date-picker/README.md | 42 +++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/packages/date-picker/README.md b/packages/date-picker/README.md index b79b6c888c..d2e2b352d5 100644 --- a/packages/date-picker/README.md +++ b/packages/date-picker/README.md @@ -22,12 +22,14 @@ npm install @leafygreen-ui/date-picker ```js import { DatePicker } from '@leafygreen-ui/date-picker'; -import { setToUTCMidnight } from '@leafygreen-ui/date-utils'; + +const [date, setDate] = useState(); {}} + value={date} + max={new Date("2026-12-26")} + onDateChange={setDate} locale="iso8601" timeZone="utc" />; @@ -57,6 +59,40 @@ import { setToUTCMidnight } from '@leafygreen-ui/date-utils'; | `autoComplete` | `'off'` \| `'on'` \| `'bday'` | Whether the input should autofill | `off` | | `darkMode` | `boolean` | Render the component in dark mode. | `false` | +## 🔎 Glossary + +### Date format + +The pattern in which a string stores date (& time) information. E.g. `“YYYY-DD-MM”`, `“MM/DD/YYYY”`, `“YYYY-MM-DDTHH:mm:ss.sssZ”` + +### Wire format (or Data format) + +The format of the date string passed into the component. This will typically be [ISO-8601](https://www.iso.org/iso-8601-date-and-time-format.html), but could be any format accepted by the [Date constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date). + +### Presentation format + +The format in which the date is presented to the user. By default, the HTML date input element presents this in the format of the user’s Locale (as defined in browser or OS settings). + +### Locale + +Language, script, & region information. Can also include [other data](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale). + +### Time Zone + +A string representing a user’s local time zone (e.g. “America/New_York”) or UTC offset. Valid time zones are defined by IANA, and [listed on Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). A UTC offset can be [provided in a DateTime string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format). + +### UTC offset + +The offset of a time zone vs UTC. E.g. The UTC offset for `“America/New_York”` is -5:00, (or -4:00 depending on daylight savings). + +### Wire time zone (or Data time zone) + +The time zone information contained in the date string/object passed into the component. + +### Presentation time zone + +The time zone relative to which we present date information to the user. Can result in a different day than the wire time zone. E.g. `“2023-08-08T00:00:00Z”` (Aug. 8/2023 at midnight UTC) => `“2023-08-07T20:00:00-04:00”` (Aug. 7 at 8pm EDT) + ## Special Case ### Aria Labels From b36e61607d60fbb60a7ced45cd05da79cf604a89 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 11 Jan 2024 14:30:20 -0500 Subject: [PATCH 346/351] Date Picker x React 17 (#2163) * LG-3697: Expose value type for useLeafyGreenTable (#2127) * expose value type for useLeafyGreenTable * changeset * Remove unused code * change header value * Squashed commit of the following: commit 5a5565942ef4b6a585edadc82ef2e7a695801a0a Merge: 9578ad8a0 c080ec01a Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu Jan 11 11:12:50 2024 -0500 Merge branch 'main' into adam/React17-renderHook commit 9578ad8a0d49d60cdccb34bb99f1213057abdb00 Author: Adam Thompson Date: Thu Jan 11 10:43:18 2024 -0500 Update hooks.spec.tsx commit c080ec01a0f76d18ad513a5fc41696dd7f91e3b1 Author: Sean Park Date: Wed Jan 10 23:16:32 2024 -0500 LG-3697: Expose value type for useLeafyGreenTable (#2127) * expose value type for useLeafyGreenTable * changeset * Remove unused code * change header value commit 44f32b6fedf4d651ad55f4fc9a736c927bb7b7c8 Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed Jan 10 17:14:50 2024 -0500 Update config.ts commit 030772e5008e705fc66fe220d2153728c401d802 Author: Adam Thompson Date: Wed Jan 10 15:43:51 2024 -0500 adds docs to Exists commit f7fdbfda8f00c5ecfc132a399ebba2f72696c196 Author: Adam Thompson Date: Wed Jan 10 15:42:57 2024 -0500 moves globToRegex commit 55c328392307f10ae6df9381569f239d7ba5147e Author: Adam Thompson Date: Wed Jan 10 15:33:41 2024 -0500 adds comment for @ts-expect-error commit ee41597c00da439962d04cd45d1c31fc72285026 Author: Adam Thompson Date: Wed Jan 10 12:32:55 2024 -0500 update testing-lib build script commit f11ddfee4985acbfbb6a8d708e0aae559bc3e296 Author: Adam Thompson Date: Wed Jan 10 12:04:59 2024 -0500 updates externalDependencies commit 9b6023b78e0d69d3026bba8e5aeee72096b34e7b Author: Adam Thompson Date: Wed Jan 10 11:49:18 2024 -0500 updates dependencies commit f921f8e649a00f1664ca5d6f8793e75b1672348b Author: Adam Thompson Date: Wed Jan 10 11:30:41 2024 -0500 adds changesets commit f25d2e8191161f914c20b4a86a915493daf26009 Author: Adam Thompson Date: Wed Jan 10 11:18:03 2024 -0500 updates hooks & toast tests commit 232d059f4cc0aa611a2935cfd6edb8c15fa6d005 Author: Adam Thompson Date: Wed Jan 10 11:17:52 2024 -0500 Creates `waitForState ` commit 085cad242bc401c3549b0311f9fe7cc4c9a3e323 Author: Adam Thompson Date: Tue Jan 9 16:00:33 2024 -0500 update types in RTLOverrides commit 021551d3ae383aab797d7c7b92c9d04b639c8793 Author: Adam Thompson Date: Tue Jan 9 15:27:02 2024 -0500 remove `act` warnings commit 9e6e2a95a725e37f7ab1b93f613f923d9fc17b44 Author: Adam Thompson Date: Tue Jan 9 15:21:59 2024 -0500 Updates return value of `useToast.updateToast` commit 77007356b0c657e41caa5f2f4bf483fb367d8f54 Author: Adam Thompson Date: Tue Jan 9 15:06:33 2024 -0500 update usage in `hooks` commit 3ce1187bb3f1383deb46a8e0eccdf3a7c842bd82 Author: Adam Thompson Date: Tue Jan 9 14:07:32 2024 -0500 export `act` from @lg-tl commit 5292c246bbb72af39ad0b5252d5812c56c26f08f Author: Adam Thompson Date: Tue Jan 9 13:53:21 2024 -0500 update usage in toast commit 488ad00ee2bc22330a8712466e80a44f076d758d Author: Adam Thompson Date: Tue Jan 9 13:53:15 2024 -0500 update renderHook imports in basic packages commit b46ad881984016a3ce29718a26044f2ebe12fb4b Author: Adam Thompson Date: Tue Jan 9 13:25:08 2024 -0500 adds renderHook override to testing-lib * update dp tests * useControlledValue properly sets isControlled * Update hooks.spec.tsx --------- Co-authored-by: Sean Park --- .changeset/bright-walls-glow.md | 6 ++ .changeset/fast-bananas-watch.md | 5 + .changeset/healthy-needles-learn.md | 8 ++ .changeset/khaki-lies-carry.md | 5 + .changeset/real-tomatoes-sneeze.md | 5 + .changeset/strange-scissors-prove.md | 5 + packages/a11y/src/A11y.spec.tsx | 3 +- .../context/SharedDatePickerContext.spec.tsx | 24 ++--- .../useControlledValue.spec.tsx | 17 ++- .../useControlledValue/useControlledValue.ts | 8 +- .../useDateSegments/useDateSegments.spec.ts | 3 +- packages/hooks/src/hooks.spec.tsx | 36 ++++--- .../useDynamicRefs/useDynamicRefs.spec.tsx | 29 +++-- .../PopoverContext/PopoverContext.spec.tsx | 3 +- .../TableHeaderCheckbox.tsx | 4 +- .../useLeafyGreenTable/TableRowCheckbox.tsx | 6 +- .../useLeafyGreenTable/useLeafyGreenTable.tsx | 30 +++--- .../useLeafyGreenTable.types.ts | 13 ++- packages/testing-lib/package.json | 13 ++- packages/testing-lib/src/RTLOverrides.ts | 36 +++++++ packages/testing-lib/src/index.ts | 3 + packages/testing-lib/src/waitForState.ts | 19 ++++ packages/testing-lib/tsconfig.json | 1 - .../ToastReducer/useToastReducer.spec.ts | 3 +- .../ToastReducer/useToastReducer.ts | 17 ++- .../ToastContext/useToast/useToast.spec.tsx | 101 ++++++++++-------- tools/test/package.json | 1 + tools/validate/src/dependencies/config.ts | 20 ++-- .../src/dependencies/utils/globToRegex.ts | 13 +++ .../validate/src/dependencies/utils/index.ts | 4 +- .../validateListedDependencies.ts | 12 ++- yarn.lock | 2 +- 32 files changed, 314 insertions(+), 141 deletions(-) create mode 100644 .changeset/bright-walls-glow.md create mode 100644 .changeset/fast-bananas-watch.md create mode 100644 .changeset/healthy-needles-learn.md create mode 100644 .changeset/khaki-lies-carry.md create mode 100644 .changeset/real-tomatoes-sneeze.md create mode 100644 .changeset/strange-scissors-prove.md create mode 100644 packages/testing-lib/src/RTLOverrides.ts create mode 100644 packages/testing-lib/src/waitForState.ts create mode 100644 tools/validate/src/dependencies/utils/globToRegex.ts diff --git a/.changeset/bright-walls-glow.md b/.changeset/bright-walls-glow.md new file mode 100644 index 0000000000..6c87c10c91 --- /dev/null +++ b/.changeset/bright-walls-glow.md @@ -0,0 +1,6 @@ +--- +'@lg-tools/validate': patch +--- + +- Adds `'@leafygreen-ui/testing-lib'` to list of external dependencies. +- Updates handling of external dependencies with glob patterns diff --git a/.changeset/fast-bananas-watch.md b/.changeset/fast-bananas-watch.md new file mode 100644 index 0000000000..17e7b97cb5 --- /dev/null +++ b/.changeset/fast-bananas-watch.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/toast': patch +--- + +Updates `updateToast` function to consistently return the new toast object diff --git a/.changeset/healthy-needles-learn.md b/.changeset/healthy-needles-learn.md new file mode 100644 index 0000000000..ffc6f8cf3d --- /dev/null +++ b/.changeset/healthy-needles-learn.md @@ -0,0 +1,8 @@ +--- +'@leafygreen-ui/leafygreen-provider': patch +'@leafygreen-ui/hooks': patch +'@leafygreen-ui/toast': patch +'@leafygreen-ui/a11y': patch +--- + +Updates test to import `renderHook` from `@leafygreen-ui/testing-lib` diff --git a/.changeset/khaki-lies-carry.md b/.changeset/khaki-lies-carry.md new file mode 100644 index 0000000000..cdb701c7cd --- /dev/null +++ b/.changeset/khaki-lies-carry.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/testing-lib': minor +--- + +Exports `waitForState`, a wrapper around `act` that returns the result of the state update callback diff --git a/.changeset/real-tomatoes-sneeze.md b/.changeset/real-tomatoes-sneeze.md new file mode 100644 index 0000000000..a0c3f8114c --- /dev/null +++ b/.changeset/real-tomatoes-sneeze.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/table': minor +--- + +Table now exposes an optional second generic type for useLeafyGreenTable that controls the type of the value diff --git a/.changeset/strange-scissors-prove.md b/.changeset/strange-scissors-prove.md new file mode 100644 index 0000000000..5621823002 --- /dev/null +++ b/.changeset/strange-scissors-prove.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/testing-lib': minor +--- + +Exports overrides for `renderHook` and `act` that will work in both a React 17 and React 18 test environment diff --git a/packages/a11y/src/A11y.spec.tsx b/packages/a11y/src/A11y.spec.tsx index 410cf6afa3..020ec39814 100644 --- a/packages/a11y/src/A11y.spec.tsx +++ b/packages/a11y/src/A11y.spec.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; import { axe } from 'jest-axe'; +import { renderHook } from '@leafygreen-ui/testing-lib'; + import { AriaLabelProps, AriaLabelPropsWithLabel } from './AriaLabelProps'; import { prefersReducedMotion, diff --git a/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx b/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx index 732d553314..6ea620c425 100644 --- a/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx +++ b/packages/date-picker/src/shared/context/SharedDatePickerContext.spec.tsx @@ -1,9 +1,9 @@ -import React, { PropsWithChildren } from 'react'; +import React from 'react'; import { act, waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { consoleOnce } from '@leafygreen-ui/lib'; +import { renderHook } from '@leafygreen-ui/testing-lib'; import { MAX_DATE, MIN_DATE } from '../constants'; @@ -17,16 +17,16 @@ import { const renderSharedDatePickerProvider = ( props?: Partial, ) => { - const { result, rerender } = renderHook< - PropsWithChildren<{}>, - SharedDatePickerContextProps - >(useSharedDatePickerContext, { - wrapper: ({ children }) => ( - - {children} - - ), - }); + const { result, rerender } = renderHook( + useSharedDatePickerContext, + { + wrapper: ({ children }) => ( + + {children} + + ), + }, + ); return { result, rerender }; }; diff --git a/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx index a1cec55d7e..c380814afa 100644 --- a/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx +++ b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.spec.tsx @@ -1,16 +1,21 @@ import React from 'react'; import { ChangeEventHandler } from 'react'; import { render } from '@testing-library/react'; -import { renderHook, RenderHookResult } from '@testing-library/react-hooks'; +import { RenderHookResult } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { act, renderHook } from '@leafygreen-ui/testing-lib'; + import { useControlledValue } from './useControlledValue'; const errorSpy = jest.spyOn(console, 'error'); const renderUseControlledValueHook = ( ...[valueProp, callback, initial]: Parameters> -): RenderHookResult>> => { +): RenderHookResult< + ReturnType>, + typeof valueProp +> => { const result = renderHook(v => useControlledValue(v, callback, initial), { initialProps: valueProp, }); @@ -18,7 +23,7 @@ const renderUseControlledValueHook = ( return { ...result }; }; -describe('packages/hooks/useControlledValue', () => { +describe('packages/date-picker/hooks/useControlledValue', () => { beforeEach(() => { errorSpy.mockImplementation(() => {}); }); @@ -109,7 +114,7 @@ describe('packages/hooks/useControlledValue', () => { test('setting value to undefined should keep the component controlled', () => { const { rerender, result } = renderUseControlledValueHook('apple'); expect(result.current.isControlled).toBe(true); - rerender(undefined); + act(() => rerender(undefined)); expect(result.current.isControlled).toBe(true); }); @@ -144,8 +149,10 @@ describe('packages/hooks/useControlledValue', () => { }); test('setValue updates the value', () => { - const { result } = renderUseControlledValueHook(undefined); + const { result, rerender } = + renderUseControlledValueHook(undefined); result.current.setValue('banana'); + rerender(); expect(result.current.value).toBe('banana'); }); }); diff --git a/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts index ffad96bf35..db1e3fe143 100644 --- a/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts +++ b/packages/date-picker/src/shared/hooks/useControlledValue/useControlledValue.ts @@ -37,9 +37,11 @@ export const useControlledValue = ( // If the value prop changes from undefined to something defined, // then isControlled is set to true, // and will remain true for the life of the component - const isControlled: boolean = useMemo(() => { - return isControlled || !isUndefined(valueProp); - }, [valueProp]); + const [isControlled, setControlled] = useState(!isUndefined(valueProp)); + useEffect(() => { + setControlled(isControlled || !isUndefined(valueProp)); + }, [isControlled, valueProp]); + const wasControlled = usePrevious(isControlled); useEffect(() => { diff --git a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts index 7a4c2e6d25..2ab50ece93 100644 --- a/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts +++ b/packages/date-picker/src/shared/hooks/useDateSegments/useDateSegments.spec.ts @@ -1,6 +1,5 @@ -import { renderHook } from '@testing-library/react'; - import { DateType, Month, newUTC } from '@leafygreen-ui/date-utils'; +import { renderHook } from '@leafygreen-ui/testing-lib'; import { useDateSegments } from './useDateSegments'; import { OnUpdateCallback } from './useDateSegments.types'; diff --git a/packages/hooks/src/hooks.spec.tsx b/packages/hooks/src/hooks.spec.tsx index f9ec105871..1a15480309 100644 --- a/packages/hooks/src/hooks.spec.tsx +++ b/packages/hooks/src/hooks.spec.tsx @@ -1,4 +1,6 @@ -import { act, renderHook } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import { act, renderHook } from '@leafygreen-ui/testing-lib'; import { useEventListener, @@ -94,7 +96,7 @@ describe('packages/hooks', () => { describe.skip('useMutationObserver', () => {}); //eslint-disable-line jest/no-disabled-tests test('useViewportSize responds to updates in window size', async () => { - const { result, waitForNextUpdate } = renderHook(() => useViewportSize()); + const { result, rerender } = renderHook(() => useViewportSize()); const mutableWindow: { -readonly [K in keyof Window]: Window[K] } = window; const initialHeight = 360; @@ -104,10 +106,11 @@ describe('packages/hooks', () => { mutableWindow.innerWidth = initialWidth; window.dispatchEvent(new Event('resize')); - await act(waitForNextUpdate); - - expect(result?.current?.height).toBe(initialHeight); - expect(result?.current?.width).toBe(initialWidth); + rerender(); + await waitFor(() => { + expect(result?.current?.height).toBe(initialHeight); + expect(result?.current?.width).toBe(initialWidth); + }); const updateHeight = 768; const updateWidth = 1024; @@ -116,10 +119,10 @@ describe('packages/hooks', () => { mutableWindow.innerWidth = updateWidth; window.dispatchEvent(new Event('resize')); - await act(waitForNextUpdate); - - expect(result?.current?.height).toBe(updateHeight); - expect(result?.current?.width).toBe(updateWidth); + await waitFor(() => { + expect(result?.current?.height).toBe(updateHeight); + expect(result?.current?.width).toBe(updateWidth); + }); }); describe('usePoller', () => { @@ -249,10 +252,10 @@ describe('packages/hooks', () => { expect(pollHandler).toHaveBeenCalledTimes(0); }); - test('when document is not visible', () => { + test('when document is not visible', async () => { const pollHandler = jest.fn(); - renderHook(() => usePoller(pollHandler)); + const { rerender } = renderHook(() => usePoller(pollHandler)); expect(pollHandler).toHaveBeenCalledTimes(1); @@ -260,15 +263,18 @@ describe('packages/hooks', () => { act(() => { document.dispatchEvent(new Event('visibilitychange')); }); - jest.advanceTimersByTime(30e3); expect(pollHandler).toHaveBeenCalledTimes(1); mutableDocument.visibilityState = 'visible'; + act(() => { document.dispatchEvent(new Event('visibilitychange')); }); + jest.advanceTimersByTime(30e3); + + rerender(pollHandler); // immediate triggers the pollHandler expect(pollHandler).toHaveBeenCalledTimes(2); @@ -308,11 +314,11 @@ describe('packages/hooks', () => { rerender(2020); expect(result.current).toEqual(42); - rerender(); + rerender(123); expect(result.current).toEqual(2020); rerender(); - expect(result.current).toEqual(2020); + expect(result.current).toEqual(123); }); }); diff --git a/packages/hooks/src/useDynamicRefs/useDynamicRefs.spec.tsx b/packages/hooks/src/useDynamicRefs/useDynamicRefs.spec.tsx index 19c22a3b65..90a878abb3 100644 --- a/packages/hooks/src/useDynamicRefs/useDynamicRefs.spec.tsx +++ b/packages/hooks/src/useDynamicRefs/useDynamicRefs.spec.tsx @@ -1,6 +1,6 @@ -import { renderHook } from '@testing-library/react-hooks'; - +// import { renderHook } from '@testing-library/react-hooks'; import { consoleOnce } from '@leafygreen-ui/lib'; +import { renderHook } from '@leafygreen-ui/testing-lib'; import { DynamicRefGetter, useDynamicRefs } from '.'; @@ -11,11 +11,13 @@ describe('packages/hooks/useDynamicRefs', () => { }); test('returns identical getter when rerendered ', () => { + const props = { prefix: 'A' }; const { result, rerender } = renderHook(v => useDynamicRefs(v), { - initialProps: { prefix: 'A' }, + initialProps: props, }); - rerender(); - expect(result.all[0]).toBe(result.all[1]); + const initialValue = result.current; + rerender(props); + expect(result.current).toStrictEqual(initialValue); }); test('returns unique getters when called with the same prefix', () => { @@ -33,11 +35,14 @@ describe('packages/hooks/useDynamicRefs', () => { test('returns unique getters when re-rendered with a different prefix', () => { // This is an edge-case, but this is the behavior we want if it happens + const props = { prefix: 'A' }; const { result, rerender } = renderHook(v => useDynamicRefs(v), { - initialProps: { prefix: 'A' }, + initialProps: props, }); - rerender({ prefix: 'B' }); - expect(result.all[0]).not.toBe(result.all[1]); + const initialValue = result.current; + const newProps = { prefix: 'B' }; + rerender(newProps); + expect(result.current).not.toBe(initialValue); }); describe('ref getter function', () => { @@ -66,18 +71,20 @@ describe('packages/hooks/useDynamicRefs', () => { }); test('returns identical refs when called with the same key', () => { - const { result } = renderHook(() => useDynamicRefs({ prefix: 'A' })); + const props = { prefix: 'A' }; + const { result } = renderHook(() => useDynamicRefs(props)); const ref1 = result.current('key'); const ref2 = result.current('key'); expect(ref1).toBe(ref2); }); test('returns identical refs when rerendered', () => { + const props = { prefix: 'A' }; const { result, rerender } = renderHook(v => useDynamicRefs(v), { - initialProps: { prefix: 'A' }, + initialProps: props, }); const ref1 = result.current('key'); - rerender(); + rerender(props); const ref2 = result.current('key'); expect(ref1).toBe(ref2); }); diff --git a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx index 1f53ec961c..00b3d8dd18 100644 --- a/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx +++ b/packages/leafygreen-provider/src/PopoverContext/PopoverContext.spec.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react'; import { act, fireEvent, render, waitFor } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; + +import { renderHook } from '@leafygreen-ui/testing-lib'; import { PopoverProvider, type PopoverState, usePopoverContext } from '.'; diff --git a/packages/table/src/useLeafyGreenTable/TableHeaderCheckbox.tsx b/packages/table/src/useLeafyGreenTable/TableHeaderCheckbox.tsx index 8efe2654cb..7d8c957c40 100644 --- a/packages/table/src/useLeafyGreenTable/TableHeaderCheckbox.tsx +++ b/packages/table/src/useLeafyGreenTable/TableHeaderCheckbox.tsx @@ -10,10 +10,10 @@ import { useRowContext } from '../Row/RowContext'; import { disabledTableRowCheckStyles } from './useLeafyGreenTable.styles'; import { LGRowData, LGTableDataType } from '.'; -export const TableHeaderCheckbox = ({ +export const TableHeaderCheckbox = ({ table, }: { - table: Table>; + table: Table>; }) => { const { theme } = useDarkMode(); const { disabled: rowIsDisabled } = useRowContext(); diff --git a/packages/table/src/useLeafyGreenTable/TableRowCheckbox.tsx b/packages/table/src/useLeafyGreenTable/TableRowCheckbox.tsx index d0557ed2a4..c16551ca12 100644 --- a/packages/table/src/useLeafyGreenTable/TableRowCheckbox.tsx +++ b/packages/table/src/useLeafyGreenTable/TableRowCheckbox.tsx @@ -10,12 +10,12 @@ import { useRowContext } from '../Row/RowContext'; import { disabledTableRowCheckStyles } from './useLeafyGreenTable.styles'; import { LGRowData, LGTableDataType } from '.'; -export const TableRowCheckbox = ({ +export const TableRowCheckbox = ({ row, table, }: { - table: Table>; - row: Row>; + table: Table>; + row: Row>; }) => { const { theme } = useDarkMode(); const { disabled: rowIsDisabled } = useRowContext(); diff --git a/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.tsx b/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.tsx index b1ebcc62d4..7070e7f751 100644 --- a/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.tsx +++ b/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.tsx @@ -15,17 +15,7 @@ import { LeafyGreenTable, LGColumnDef, LGTableDataType } from '.'; const CHECKBOX_WIDTH = 14; -/** - * A `ColumnDef` object injected into `useReactTable`'s `columns` option when the user is using selectable rows. - */ -const baseSelectColumnConfig: LGColumnDef = { - id: 'select', - size: CHECKBOX_WIDTH, - header: TableHeaderCheckbox, - cell: TableRowCheckbox, -}; - -function useLeafyGreenTable({ +function useLeafyGreenTable({ containerRef, data, columns: columnsProp, @@ -34,7 +24,17 @@ function useLeafyGreenTable({ useVirtualScrolling = false, allowSelectAll = true, ...rest -}: LeafyGreenTableOptions): LeafyGreenTable { +}: LeafyGreenTableOptions): LeafyGreenTable { + /** + * A `ColumnDef` object injected into `useReactTable`'s `columns` option when the user is using selectable rows. + */ + const baseSelectColumnConfig: LGColumnDef = { + id: 'select', + size: CHECKBOX_WIDTH, + header: TableHeaderCheckbox, + cell: TableRowCheckbox, + }; + const hasSortableColumns = React.useMemo( () => columnsProp.some(propCol => !!propCol.enableSorting), [columnsProp], @@ -42,15 +42,15 @@ function useLeafyGreenTable({ const selectColumnConfig = allowSelectAll ? baseSelectColumnConfig : omit(baseSelectColumnConfig, 'header'); - const columns = React.useMemo>>( + const columns = React.useMemo>>( () => [ - ...(hasSelectableRows ? [selectColumnConfig as LGColumnDef] : []), + ...(hasSelectableRows ? [selectColumnConfig as LGColumnDef] : []), ...columnsProp.map(propColumn => { return { ...propColumn, align: propColumn.align ?? 'left', enableSorting: propColumn.enableSorting ?? false, - } as LGColumnDef; + } as LGColumnDef; }), ], [columnsProp, hasSelectableRows, selectColumnConfig], diff --git a/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.types.ts b/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.types.ts index 0556a31eaa..f8cef2f587 100644 --- a/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.types.ts +++ b/packages/table/src/useLeafyGreenTable/useLeafyGreenTable.types.ts @@ -31,17 +31,22 @@ export type LeafyGreenTableCell = Cell< export interface LeafyGreenTableRow extends Row> {} -export type LGColumnDef = ColumnDef> & { +export type LGColumnDef< + T extends LGRowData, + V extends unknown = unknown, +> = ColumnDef, V> & { align?: HTMLElementProps<'td'>['align']; }; /** LeafyGreen extension of `useReactTable` {@link TableOptions}*/ -export interface LeafyGreenTableOptions - extends Omit>, 'getCoreRowModel'> { +export interface LeafyGreenTableOptions< + T extends LGRowData, + V extends unknown = unknown, +> extends Omit>, 'getCoreRowModel'> { containerRef: RefObject; hasSelectableRows?: boolean; useVirtualScrolling?: boolean; - columns: Array>; + columns: Array>; withPagination?: boolean; allowSelectAll?: boolean; } diff --git a/packages/testing-lib/package.json b/packages/testing-lib/package.json index e2568db3a4..e4f59a42bb 100644 --- a/packages/testing-lib/package.json +++ b/packages/testing-lib/package.json @@ -16,13 +16,18 @@ "@testing-library/user-event": "13.5.0", "lodash": "^4.17.21" }, + "devDependencies": { + "@lg-tools/build": "0.3.1" + }, "peerDependencies": { - "@lg-tools/test": "0.3.2" + "@testing-library/react": "^12.0.0 || ^13.1.0 || ^14.0.0" + }, + "optionalDependencies": { + "@testing-library/react-hooks": ">=3.7.0" }, "scripts": { - "build": "lg build-package", - "tsc": "lg build-ts", - "docs": "lg build-tsdoc" + "build": "lg-internal-build-package", + "tsc": "tsc --build tsconfig.json" }, "license": "Apache-2.0", "publishConfig": { diff --git a/packages/testing-lib/src/RTLOverrides.ts b/packages/testing-lib/src/RTLOverrides.ts new file mode 100644 index 0000000000..2562f60e0a --- /dev/null +++ b/packages/testing-lib/src/RTLOverrides.ts @@ -0,0 +1,36 @@ +import * as RTL from '@testing-library/react'; + +/** + * Utility type that returns `X.Y` if it exists, otherwise defaults to fallback type `Z`, or `any` + */ +export type Exists< + X, + Y extends keyof X | string, + Z = unknown, +> = Y extends keyof X ? X[Y] : Z; + +/** + * Re-exports `renderHook` from `"@testing-library/react"` if it exists, + * or from `"@testing-library/react-hooks"` + * + * (used when running in a React 17 test environment) + */ +export const renderHook: Exists = + (RTL as any).renderHook ?? + (() => { + const RHTL = require('@testing-library/react-hooks'); + return RHTL.renderHook; + })(); + +/** + * Re-exports `act` from `"@testing-library/react"` if it exists, + * or from `"@testing-library/react-hooks"` + * + * (used when running in a React 17 test environment) + */ +export const act: Exists = + RTL.act ?? + (() => { + const RHTL = require('@testing-library/react-hooks'); + return RHTL.act; + })(); diff --git a/packages/testing-lib/src/index.ts b/packages/testing-lib/src/index.ts index dc9301d6b5..6d8ae4de56 100644 --- a/packages/testing-lib/src/index.ts +++ b/packages/testing-lib/src/index.ts @@ -1,6 +1,9 @@ import * as Context from './context'; import * as jest from './jest'; import * as JestDOM from './jest-dom'; +export { act, renderHook } from './RTLOverrides'; +export { waitForState } from './waitForState'; + export { Context, jest, JestDOM }; export { eventContainingTargetValue } from './eventContainingTargetValue'; diff --git a/packages/testing-lib/src/waitForState.ts b/packages/testing-lib/src/waitForState.ts new file mode 100644 index 0000000000..b7f0b67c8c --- /dev/null +++ b/packages/testing-lib/src/waitForState.ts @@ -0,0 +1,19 @@ +import { act } from './RTLOverrides'; + +/** + * Wrapper around `act`. + * + * Awaits an `act` call, + * and returns the value of the state update callback + */ +export const waitForState = async ( + callback: () => T, +): Promise => { + let val: T; + await act(() => { + val = callback(); + }); + + // @ts-expect-error - val is returned before TS sees it as being defined + return val; +}; diff --git a/packages/testing-lib/tsconfig.json b/packages/testing-lib/tsconfig.json index 9f2d965a63..edb1c03f02 100644 --- a/packages/testing-lib/tsconfig.json +++ b/packages/testing-lib/tsconfig.json @@ -6,7 +6,6 @@ "rootDir": "src", "baseUrl": ".", "paths": { - "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], "@leafygreen-ui/*": ["../*/src"] } }, diff --git a/packages/toast/src/ToastContext/ToastReducer/useToastReducer.spec.ts b/packages/toast/src/ToastContext/ToastReducer/useToastReducer.spec.ts index a8d8e631f0..b79aa3f886 100644 --- a/packages/toast/src/ToastContext/ToastReducer/useToastReducer.spec.ts +++ b/packages/toast/src/ToastContext/ToastReducer/useToastReducer.spec.ts @@ -1,6 +1,7 @@ import { act } from 'react-dom/test-utils'; import { cleanup } from '@testing-library/react'; -import { renderHook } from '@testing-library/react-hooks'; + +import { renderHook } from '@leafygreen-ui/testing-lib'; import { Variant } from '../../Toast.types'; import { ToastId, ToastStack } from '../ToastContext.types'; diff --git a/packages/toast/src/ToastContext/ToastReducer/useToastReducer.ts b/packages/toast/src/ToastContext/ToastReducer/useToastReducer.ts index 6cf56148dc..501c87657a 100644 --- a/packages/toast/src/ToastContext/ToastReducer/useToastReducer.ts +++ b/packages/toast/src/ToastContext/ToastReducer/useToastReducer.ts @@ -113,16 +113,25 @@ export const useToastReducer = (initialValue?: ToastStack) => { const updateToast: ToastContextProps['updateToast'] = useCallback( (id: ToastId, props: Partial) => { - dispatch({ + const action: ToastReducerAction = { type: ToastReducerActionType.Update, payload: { id, props, }, - }); - return getToast(id); + }; + + dispatch(action); + + // `getToast` will return the previous toast value, + // so we need to apply the state change manually + // in order to return the updated value + const { stack: newStack } = toastReducer({ stack }, action); + const updatedToast = newStack.get(id); + + return updatedToast; }, - [getToast], + [stack], ); const clearStack: ToastContextProps['clearStack'] = useCallback(() => { diff --git a/packages/toast/src/ToastContext/useToast/useToast.spec.tsx b/packages/toast/src/ToastContext/useToast/useToast.spec.tsx index 78b606f7b0..e3da1e5c4b 100644 --- a/packages/toast/src/ToastContext/useToast/useToast.spec.tsx +++ b/packages/toast/src/ToastContext/useToast/useToast.spec.tsx @@ -1,14 +1,11 @@ import React, { PropsWithChildren, useEffect } from 'react'; import { render } from '@testing-library/react'; -import { - cleanup, - renderHook, - RenderHookResult, -} from '@testing-library/react-hooks'; +import { cleanup } from '@testing-library/react-hooks'; + +import { renderHook, waitForState } from '@leafygreen-ui/testing-lib'; import { ToastProps, Variant } from '../../Toast.types'; import { ToastContext } from '../ToastContext'; -import { ToastContextProps } from '../ToastContext.types'; import { useToastReducer } from '../ToastReducer'; import { makeToast, @@ -56,63 +53,83 @@ describe('packages/toast/useToast', () => { }); describe('returned functions return correct values', () => { - let current: RenderHookResult< - unknown, - ToastContextProps - >['result']['current']; - - beforeEach(() => { - const { result } = renderHook(useToast, { wrapper: ToastProviderMock }); - current = result.current; - }); - - test('pushToast => ToastId', () => { - const { pushToast } = current; - const toastId = pushToast({ title: 'test' }); + test('pushToast => ToastId', async () => { + const { result } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast } = result.current; + const toastId = await waitForState(() => pushToast({ title: 'test' })); expect(toastId).toEqual(expect.stringContaining('toast-')); }); - test('getToast => ToastProps', () => { - const { pushToast, getToast } = current; - const toastId = pushToast({ title: 'test' }); + test('getToast => ToastProps', async () => { + const { result, rerender } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast, getToast } = result.current; + + const toastId = await waitForState(() => pushToast({ title: 'test' })); + rerender(); expect(getToast(toastId)).toEqual( expect.objectContaining({ title: 'test' }), ); }); - test('updateToast => ToastProps', () => { - const { pushToast, updateToast } = current; - const toastId = pushToast({ - title: 'test', - variant: Variant.Progress, - progress: 0, + test('updateToast => ToastProps', async () => { + const { result, rerender } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast, updateToast } = result.current; + const toastId = await waitForState(() => + pushToast({ + title: 'test', + variant: Variant.Progress, + progress: 0, + }), + ); + const updatedToast = await waitForState(() => { + rerender(); + return updateToast(toastId, { + progress: 0.5, + }); }); - expect(updateToast(toastId, { progress: 0.5 })).toEqual( + expect(updatedToast).toEqual( expect.objectContaining({ progress: 0.5 }), ); }); - test('popToast => ToastProps', () => { - const { pushToast, popToast } = current; - const toastId = pushToast({ title: 'test' }); - expect(popToast(toastId)).toEqual( - expect.objectContaining({ title: 'test' }), - ); + test('popToast => ToastProps', async () => { + const { result, rerender } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast, popToast } = result.current; + const toastId = await waitForState(() => pushToast({ title: 'test' })); + rerender(); + const poppedToast = await waitForState(() => popToast(toastId)); + expect(poppedToast).toEqual(expect.objectContaining({ title: 'test' })); }); - test('getStack => ToastStack (Map)', () => { - const { pushToast, getStack } = current; - pushToast({ title: 'test' }); + test('getStack => ToastStack (Map)', async () => { + const { result, rerender } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast, getStack } = result.current; + await waitForState(() => pushToast({ title: 'test' })); + rerender(); expect(getStack()).toBeDefined(); expect(getStack()?.size).toEqual(1); }); - test('clearStack => void', () => { - const { pushToast, clearStack, getStack } = current; - pushToast({ title: 'test' }); - expect(clearStack()).toBeUndefined(); + test('clearStack => void', async () => { + const { result } = renderHook(useToast, { + wrapper: ToastProviderMock, + }); + const { pushToast, clearStack, getStack } = result.current; + await waitForState(() => pushToast({ title: 'test' })); + const clearedStack = await waitForState(() => clearStack()); + expect(clearedStack).toBeUndefined(); expect(getStack()?.size).toEqual(0); }); }); diff --git a/tools/test/package.json b/tools/test/package.json index 31e827c99e..2561068ccf 100644 --- a/tools/test/package.json +++ b/tools/test/package.json @@ -19,6 +19,7 @@ "@emotion/server": "11.11.0", "@lg-tools/build": "0.3.1", "@lg-tools/meta": "0.1.5", + "@leafygreen-ui/testing-lib": "^0.3.4", "@testing-library/dom": "9.3.1", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "14.0.0", diff --git a/tools/validate/src/dependencies/config.ts b/tools/validate/src/dependencies/config.ts index 5fc01cf7d5..421ff2ae26 100644 --- a/tools/validate/src/dependencies/config.ts +++ b/tools/validate/src/dependencies/config.ts @@ -38,34 +38,38 @@ export const ignoreFilePatterns: Array = [ ]; /** - * These dependencies will be ignored when listed in a package.json. * These are globally available dev dependencies. - * We don't want every component flagged for not having - * these packages explicitly declared in its package.json + * + * Packages that omit these dependencies will not be flagged for missing dependencies. + * + * Packages that list these dependencies will not be flagged for unused dependencies */ -export const ignoreDependencies = [ - '@leafygreen-ui/mongo-nav', +export const externalDependencies = [ '@babel/*', '@emotion/*', + '@leafygreen-ui/mongo-nav', + '@leafygreen-ui/testing-lib', '@rollup/*', '@storybook/*', '@svgr/*', '@testing-library/*', '@types/*', - '@typescript-*', + '@typescript-eslint/*', 'buffer', 'eslint*', - 'jest*', + 'jest', + 'jest-*', 'jest-axe', 'prettier*', 'prop-types', 'react-*', 'rollup*', 'storybook-*', + 'typescript', '*-loader', '*-lint*', ]; export const depcheckOptions: depcheck.Options = { - ignoreMatches: ignoreDependencies, + ignoreMatches: externalDependencies, }; diff --git a/tools/validate/src/dependencies/utils/globToRegex.ts b/tools/validate/src/dependencies/utils/globToRegex.ts new file mode 100644 index 0000000000..55c7c3359c --- /dev/null +++ b/tools/validate/src/dependencies/utils/globToRegex.ts @@ -0,0 +1,13 @@ +/** + * Returns a _very_ rough approximation of a regex pattern, + * given a glob pattern. + * + * e.g. `globToRegex('@leafygreen-ui/*') // /@leafygreen-ui\/.+/` + * + * @internal + */ +export const globToRegex = (globPattern: string): RegExp => { + const regexPattern = globPattern.replace('*', '.+').replace('/', '\\/'); + const regEx = new RegExp(regexPattern); + return regEx; +}; diff --git a/tools/validate/src/dependencies/utils/index.ts b/tools/validate/src/dependencies/utils/index.ts index 5230b90160..9df9de2655 100644 --- a/tools/validate/src/dependencies/utils/index.ts +++ b/tools/validate/src/dependencies/utils/index.ts @@ -7,7 +7,7 @@ import path from 'path'; import { devFilePatterns, - ignoreDependencies, + externalDependencies, ignoreFilePatterns, } from '../config'; @@ -106,7 +106,7 @@ export const isDependencyUsedInSourceFile = ( importedPackages: depcheck.Results['using'], ): boolean => { // consider a dependency used in a package file if its in `ignoreMatches` - const isIgnored = ignoreDependencies.includes(depName); + const isIgnored = externalDependencies.includes(depName); const usedInPackageFile = importedPackages?.[depName]?.some( // is used in at least one... // file that is not ignored diff --git a/tools/validate/src/dependencies/validateListedDependencies.ts b/tools/validate/src/dependencies/validateListedDependencies.ts index 9aa9797f33..3af60c4fe8 100644 --- a/tools/validate/src/dependencies/validateListedDependencies.ts +++ b/tools/validate/src/dependencies/validateListedDependencies.ts @@ -5,7 +5,8 @@ import path from 'path'; import { ValidateCommandOptions } from '../validate.types'; -import { DepCheckFunctionProps, ignoreDependencies } from './config'; +import { globToRegex } from './utils/globToRegex'; +import { DepCheckFunctionProps, externalDependencies } from './config'; import { isDependencyOnlyUsedInTestFile, sortDependenciesByUsage, @@ -38,16 +39,18 @@ export function validateListedDependencies( const listedButOnlyUsedAsDev = listedDependencies.filter( listedDepName => !importedPackagesInSourceFile.includes(listedDepName) && - !ignoreDependencies.includes(listedDepName), + !externalDependencies.some(glob => { + const regEx = globToRegex(glob); + return regEx.test(listedDepName); + }), ); - verbose && + if (listedButOnlyUsedAsDev.length && verbose) { console.log( `${chalk.blue( pkgName, )}: lists packages as dependency, but only uses them in test files`, ); - verbose && console.log( listedButOnlyUsedAsDev .map( @@ -60,6 +63,7 @@ export function validateListedDependencies( ) .join('\n'), ); + } return listedButOnlyUsedAsDev; } diff --git a/yarn.lock b/yarn.lock index 9af9789a06..8f1def6e4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4189,7 +4189,7 @@ lodash "^4.17.15" redent "^3.0.0" -"@testing-library/react-hooks@8.0.1": +"@testing-library/react-hooks@8.0.1", "@testing-library/react-hooks@>=3.7.0": version "8.0.1" resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-8.0.1.tgz#0924bbd5b55e0c0c0502d1754657ada66947ca12" integrity sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g== From a24d14a5e2407cbaa39290f8110750952e2ef19d Mon Sep 17 00:00:00 2001 From: Shaneeza Date: Thu, 11 Jan 2024 15:25:55 -0500 Subject: [PATCH 347/351] fix label in README --- packages/date-picker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/README.md b/packages/date-picker/README.md index d2e2b352d5..23e4a1de37 100644 --- a/packages/date-picker/README.md +++ b/packages/date-picker/README.md @@ -39,7 +39,7 @@ const [date, setDate] = useState(); | Prop | Type | Description | Default | | ------------------ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| `label` | `ReactNode` | Label shown above the number input. | | +| `label` | `ReactNode` | Label shown above the date picker. | | | `description` | `ReactNode` | A description for the date picker. It's recommended to set a meaningful time zone representation as the description. (e.g. "Coordinated Universal Time") | | | `locale` | `'iso8601'`\| `'string'` | Sets the _presentation format_ for the displayed date, and localizes month & weekday labels. Defaults to the user’s browser preference (if available), otherwise ISO-8601. | `iso8601` | | `timeZone` | `string` | A valid IANA timezone string, or UTC offset, used to calculate initial values. Defaults to the user’s browser settings. | From 87f1260df60be7d9227e71df7b4b86ecf72a8af7 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:03:39 -0500 Subject: [PATCH 348/351] Date picker [LG-3911] Responding to `null` and `"Invalid Date"` values (#2147) * add component tests for invalid date * move types * creates InvalidDate * update isValidDate * update checks in getISODateTZ & getISODate * update stories TS * updates getSegmentsFromDate * add test to ensure mocked dates are valid * updates some utils to accept DateType as param * update sameDay check in useDateSegment * updates Context * Update DatePickerInput.tsx * fixes menu TS * updates & tests useDateSegments * Add re-render tests to DateInputBox * Adds DateInputBox test for invalid date * adds invalidDate tests for DatePicker * adds DateInputBox tests * adds isInvalidDateObject * updates newDateFromSegments * Sets values to `Invalid Date` * run range validation only for valid dates * Update DatePickerInput.tsx * add value debugging to story * Displays Error message for invalid dates * update dateInputBox setValue signature * add more complex error tests * clear error message if value to validate is null * Update DatePickerInput.tsx * re-enable tests * Change story update fn signature to DateInputChangeEventHandler * fix ts * rm flaky tests * improve isNextFocusElementASegment check * fix ts --- .../date-picker/src/DatePicker.stories.tsx | 42 ++- .../src/DatePicker/DatePicker.spec.tsx | 102 ++++--- .../DatePickerContext/DatePickerContext.tsx | 50 ++-- .../DatePickerInput/DatePickerInput.tsx | 44 +-- .../DateInputBox/DateInputBox.spec.tsx | 251 ++++++++++++------ .../DateInputBox/DateInputBox.stories.tsx | 24 +- .../DateInput/DateInputBox/DateInputBox.tsx | 66 ++--- .../DateInputBox/DateInputBox.types.ts | 12 +- .../shared/utils/newDateFromSegments/index.ts | 27 +- .../newDateFromSegments.spec.ts | 106 +++++--- .../newDateFromSegments.ts | 26 ++ packages/date-utils/src/index.ts | 2 +- packages/date-utils/src/isValidDate/index.ts | 7 +- .../src/isValidDate/isValidDate.spec.ts | 47 +++- .../date-utils/src/isValidDate/isValidDate.ts | 37 ++- 15 files changed, 538 insertions(+), 305 deletions(-) create mode 100644 packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.ts diff --git a/packages/date-picker/src/DatePicker.stories.tsx b/packages/date-picker/src/DatePicker.stories.tsx index 06d7e01533..e6e5bdff42 100644 --- a/packages/date-picker/src/DatePicker.stories.tsx +++ b/packages/date-picker/src/DatePicker.stories.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useState } from 'react'; import { StoryFn } from '@storybook/react'; +import { isNull, isUndefined } from 'lodash'; import Button from '@leafygreen-ui/button'; import { @@ -11,10 +12,12 @@ import { testLocales, testTimeZoneLabels, } from '@leafygreen-ui/date-utils'; +import { css } from '@leafygreen-ui/emotion'; import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; import { StoryMetaType } from '@leafygreen-ui/lib'; import Modal from '@leafygreen-ui/modal'; import { Size } from '@leafygreen-ui/tokens'; +import { Overline } from '@leafygreen-ui/typography'; import { MAX_DATE, MIN_DATE } from './shared/constants'; import { @@ -97,21 +100,34 @@ export const LiveExample: StoryFn = props => { const [value, setValue] = useState(); return ( - { - // eslint-disable-next-line no-console - console.log('Storybook: onDateChange', { v }); - if (isValidDate(v)) { +

+ { + // eslint-disable-next-line no-console + console.log('Storybook: onDateChange', { v }); setValue(v); + }} + handleValidation={date => + // eslint-disable-next-line no-console + console.log('Storybook: handleValidation', { date }) } - }} - handleValidation={date => - // eslint-disable-next-line no-console - console.log('Storybook: handleValidation', { date }) - } - /> + /> +
+ Current value + + {isValidDate(value) + ? value.toISOString() + : isNull(value) || isUndefined(value) + ? String(value) + : value.toDateString()} + +
); }; diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx index a13deb026d..7cd1afade7 100644 --- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx +++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx @@ -1607,6 +1607,9 @@ describe('packages/date-picker', () => { 'lg-form_field-error_message', ); expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + '2021-02-29 is not a valid date', + ); }); }); @@ -1634,27 +1637,12 @@ describe('packages/date-picker', () => { 'lg-form_field-error_message', ); expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + '2020-02-31 is not a valid date', + ); }); }); - test('error state stays after menu is closed', async () => { - const result = renderDatePicker({ - value: newUTC(2020, Month.January, 31), - }); - const input = getRelevantInput(result); - userEvent.click(input); - userEvent.keyboard('{arrowup}'); - const { menuContainerEl } = - await result.findMenuElements(); - userEvent.click(result.container.parentElement!); - - await waitForElementToBeRemoved(menuContainerEl); - const errorElement = result.queryByTestId( - 'lg-form_field-error_message', - ); - expect(errorElement).toBeInTheDocument(); - }); - break; } @@ -1923,6 +1911,9 @@ describe('packages/date-picker', () => { 'lg-form_field-error_message', ); expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + '2019-02-29 is not a valid date', + ); }); }); @@ -1950,27 +1941,12 @@ describe('packages/date-picker', () => { 'lg-form_field-error_message', ); expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + '2020-02-31 is not a valid date', + ); }); }); - test('error state stays after menu is closed', async () => { - const result = renderDatePicker({ - value: newUTC(2020, Month.March, 31), - }); - const input = getRelevantInput(result); - userEvent.click(input); - userEvent.keyboard('{arrowdown}'); - const { menuContainerEl } = - await result.findMenuElements(); - userEvent.click(result.container.parentElement!); - - await waitForElementToBeRemoved(menuContainerEl); - const errorElement = result.queryByTestId( - 'lg-form_field-error_message', - ); - expect(errorElement).toBeInTheDocument(); - }); - break; } @@ -2844,7 +2820,6 @@ describe('packages/date-picker', () => { }); }); - // TODO: describe('if the value is not a valid date', () => { // E.g. Feb 31 2020 test('the input is rendered with the typed date', async () => { @@ -2873,6 +2848,9 @@ describe('packages/date-picker', () => { expect(inputContainer).toHaveAttribute('aria-invalid', 'true'); const errorElement = queryByTestId('lg-form_field-error_message'); expect(errorElement).toBeInTheDocument(); + expect(errorElement).toHaveTextContent( + '2020-02-31 is not a valid date', + ); }); }); @@ -3484,6 +3462,56 @@ describe('packages/date-picker', () => { }); }); + describe('Error messages', () => { + test('Updating the input to a still-invalid date updates the error message', () => { + const { yearInput, monthInput, dayInput, queryByTestId } = + renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '02'); + userEvent.type(dayInput, '31'); + userEvent.tab(); + let errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + '2020-02-31 is not a valid date', + ); + + userEvent.type(dayInput, '{backspace}0'); + userEvent.tab(); + errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + '2020-02-30 is not a valid date', + ); + + userEvent.type(dayInput, '{backspace}{backspace}'); + userEvent.tab(); + errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + '2020-02- is not a valid date', + ); + }); + + test('Clearing the input after an invalid date error message is displayed removes the message', () => { + const { yearInput, monthInput, dayInput, queryByTestId } = + renderDatePicker({}); + userEvent.type(yearInput, '2020'); + userEvent.type(monthInput, '02'); + userEvent.type(dayInput, '31'); + const errorElement = queryByTestId('lg-form_field-error_message'); + expect(errorElement).toHaveTextContent( + '2020-02-31 is not a valid date', + ); + + userEvent.type(dayInput, '{backspace}{backspace}'); + userEvent.type(monthInput, '{backspace}{backspace}'); + userEvent.type( + yearInput, + '{backspace}{backspace}{backspace}{backspace}', + ); + const errorElement2 = queryByTestId('lg-form_field-error_message'); + expect(errorElement2).not.toBeInTheDocument(); + }); + }); + // JSDOM does not support layout: https://github.com/testing-library/react-testing-library/issues/671 test.todo('page does not scroll when arrow keys are pressed'); }); diff --git a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx index 5028c07dfb..06e1cf2948 100644 --- a/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerContext/DatePickerContext.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useState, } from 'react'; +import { isNull } from 'lodash'; import { DateType, @@ -20,13 +21,7 @@ import { import { usePrevious } from '@leafygreen-ui/hooks'; import { useSharedDatePickerContext } from '../../shared/context'; -import { - doSegmentsFormValidDate, - getFormattedDateString, - getFormattedDateStringFromSegments, - getSegmentStateFromRefs, - isEverySegmentFilled, -} from '../../shared/utils'; +import { getFormattedDateString } from '../../shared/utils'; import { getInitialHighlight } from '../utils/getInitialHighlight'; import { @@ -122,36 +117,23 @@ export const DatePickerProvider = ({ */ const handleValidation = (val?: DateType): void => { // Set an internal error state if necessary - if (val && !isInRange(val)) { - if (isOnOrBefore(val, min)) { - setInternalErrorMessage( - `Date must be after ${getFormattedDateString(min, locale)}`, - ); + if (isValidDate(val)) { + if (isInRange(val)) { + clearInternalErrorMessage(); } else { - setInternalErrorMessage( - `Date must be before ${getFormattedDateString(max, locale)}`, - ); - } - } else { - // Wait for the inputs to update, then check they're valid - setTimeout(() => { - const segments = getSegmentStateFromRefs(refs.segmentRefs); - const areAllFilled = isEverySegmentFilled(segments); - const areSegmentsValidDate = doSegmentsFormValidDate(segments); - - // If the segments are valid, clear any error messages - if (areSegmentsValidDate) { - clearInternalErrorMessage(); - } else if (areAllFilled) { - // Show an error iff areAllFilled - const dateString = getFormattedDateStringFromSegments( - segments, - locale, + if (isOnOrBefore(val, min)) { + setInternalErrorMessage( + `Date must be after ${getFormattedDateString(min, locale)}`, + ); + } else { + setInternalErrorMessage( + `Date must be before ${getFormattedDateString(max, locale)}`, ); - // Setting the error message here is likely redundant (handled by DateInputBox) - setInternalErrorMessage(`${dateString} is not a valid date`); } - }); + } + } else if (isNull(val)) { + // This could still be an error, but it's not defined internally + clearInternalErrorMessage(); } _handleValidation?.(val); diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index 9d2b71fd4e..a883b12c28 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -5,14 +5,17 @@ import React, { KeyboardEventHandler, MouseEventHandler, } from 'react'; +import { isNull } from 'lodash'; +import { DateInputChangeEventHandler } from 'src/shared/components/DateInput/DateInputBox/DateInputBox.types'; -import { DateType, isSameUTCDay } from '@leafygreen-ui/date-utils'; +import { isInvalidDateObject, isSameUTCDay } from '@leafygreen-ui/date-utils'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; import { DateInputSegmentChangeEventHandler } from '../../shared/components/DateInput/DateInputSegment'; import { useSharedDatePickerContext } from '../../shared/context'; import { + getFormattedDateStringFromSegments, getRelativeSegmentRef, isElementInputSegment, } from '../../shared/utils'; @@ -31,8 +34,13 @@ export const DatePickerInput = forwardRef( }: DatePickerInputProps, fwdRef, ) => { - const { formatParts, disabled, isDirty, setIsDirty } = - useSharedDatePickerContext(); + const { + formatParts, + disabled, + locale, + setIsDirty, + setInternalErrorMessage, + } = useSharedDatePickerContext(); const { refs: { segmentRefs, calendarButtonRef }, value, @@ -43,10 +51,18 @@ export const DatePickerInput = forwardRef( } = useDatePickerContext(); /** Called when the input's Date value has changed */ - const handleInputValueChange = (inputVal?: DateType) => { - if (!isSameUTCDay(inputVal, value)) { - handleValidation?.(inputVal); - setValue(inputVal || null); + const handleInputValueChange: DateInputChangeEventHandler = ({ + value: newVal, + segments, + }) => { + if (!isSameUTCDay(newVal, value)) { + handleValidation?.(newVal); + setValue(newVal); + } + + if (!isNull(newVal) && isInvalidDateObject(newVal)) { + const dateString = getFormattedDateStringFromSegments(segments, locale); + setInternalErrorMessage(`${dateString} is not a valid date`); } }; @@ -170,13 +186,11 @@ export const DatePickerInput = forwardRef( */ const handleInputBlur: FocusEventHandler = e => { const nextFocus = e.relatedTarget as HTMLInputElement; + const isNextFocusElementASegment = Object.values(segmentRefs) + .map(ref => ref.current) + .includes(nextFocus); - // If the next focus is _not_ on a segment - if ( - !Object.values(segmentRefs) - .map(ref => ref.current) - .includes(nextFocus) - ) { + if (!isNextFocusElementASegment) { setIsDirty(true); handleValidation?.(value); } @@ -190,10 +204,6 @@ export const DatePickerInput = forwardRef( segmentChangeEvent => { const { segment } = segmentChangeEvent; - if (isDirty) { - handleValidation?.(value); - } - /** * Fire a simulated `change` event */ diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx index 8e16735c09..157f27b7d4 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.spec.tsx @@ -6,7 +6,6 @@ import userEvent from '@testing-library/user-event'; import { Month, newUTC } from '@leafygreen-ui/date-utils'; import { - defaultSharedDatePickerContext, SharedDatePickerProvider, SharedDatePickerProviderProps, } from '../../../context'; @@ -29,10 +28,7 @@ const renderDateInputBox = ( newProps?: Omit, ) => { result.rerender( - + , ); @@ -192,95 +188,184 @@ describe('packages/date-picker/shared/date-input-box', () => { }); describe('Typing', () => { - test('updates the rendered segment value', () => { - const { dayInput } = renderDateInputBox(undefined, testContext); - userEvent.type(dayInput, '26'); - expect(dayInput.value).toBe('26'); - }); + describe('single segment', () => { + test('updates the rendered segment value', () => { + const { dayInput } = renderDateInputBox(undefined, testContext); + userEvent.type(dayInput, '26'); + expect(dayInput.value).toBe('26'); + }); - test('segment value is not immediately formatted', () => { - const { dayInput } = renderDateInputBox(undefined, testContext); - userEvent.type(dayInput, '2'); - expect(dayInput.value).toBe('2'); - }); + test('segment value is not immediately formatted', () => { + const { dayInput } = renderDateInputBox(undefined, testContext); + userEvent.type(dayInput, '2'); + expect(dayInput.value).toBe('2'); + }); - test('value is formatted on segment blur', () => { - const { dayInput } = renderDateInputBox(undefined, testContext); - userEvent.type(dayInput, '2'); - userEvent.tab(); - expect(dayInput.value).toBe('02'); - }); + test('value is formatted on segment blur', () => { + const { dayInput } = renderDateInputBox(undefined, testContext); + userEvent.type(dayInput, '2'); + userEvent.tab(); + expect(dayInput.value).toBe('02'); + }); - test('backspace deletes characters', () => { - const { dayInput, yearInput } = renderDateInputBox( - { value: newUTC(1993, Month.December, 26) }, - testContext, - ); - userEvent.type(dayInput, '{backspace}'); - expect(dayInput.value).toBe('2'); - userEvent.type(yearInput, '{backspace}'); - expect(yearInput.value).toBe('199'); - }); + test('backspace deletes characters', () => { + const { dayInput, yearInput } = renderDateInputBox( + { value: null }, + testContext, + ); + userEvent.type(dayInput, '21'); + userEvent.type(dayInput, '{backspace}'); + expect(dayInput.value).toBe('2'); + + userEvent.type(yearInput, '1993'); + userEvent.type(yearInput, '{backspace}'); + expect(yearInput.value).toBe('199'); + }); - test('segment change handler is called when typing into a segment', () => { - const { yearInput } = renderDateInputBox( - { - value: null, - onSegmentChange, - }, - testContext, - ); - userEvent.type(yearInput, '1993'); + test('segment change handler is called when typing into a segment', () => { + const { yearInput } = renderDateInputBox( + { onSegmentChange }, + testContext, + ); + userEvent.type(yearInput, '1993'); - expect(onSegmentChange).toHaveBeenCalledWith( - expect.objectContaining({ value: '1993' }), - ); - }); + expect(onSegmentChange).toHaveBeenCalledWith( + expect.objectContaining({ value: '1993' }), + ); + }); - test('value setter is not called when typing into a segment', () => { - const setValue = jest.fn(); - const { dayInput } = renderDateInputBox( - { - value: null, - setValue, - }, - testContext, - ); + test('value setter is not called when typing into a segment', () => { + const setValue = jest.fn(); + const { dayInput } = renderDateInputBox({ setValue }, testContext); + + userEvent.type(dayInput, '26'); + }); + + test('segment change handler is called when deleting from a single segment', () => { + const { dayInput } = renderDateInputBox( + { onSegmentChange }, + testContext, + ); + userEvent.type(dayInput, '21'); + userEvent.type(dayInput, '{backspace}'); + expect(onSegmentChange).toHaveBeenCalledWith( + expect.objectContaining({ value: '2' }), + ); + }); - userEvent.type(dayInput, '26'); - expect(setValue).not.toHaveBeenCalled(); + test('value setter is not called when deleting from a single segment', () => { + const setValue = jest.fn(); + + const { dayInput } = renderDateInputBox({ setValue }, testContext); + userEvent.type(dayInput, '21'); + userEvent.type(dayInput, '{backspace}'); + expect(setValue).not.toHaveBeenCalled(); + }); }); - test('value setter is not called when an ambiguous date is entered', () => { - const setValue = jest.fn(); - const { dayInput, monthInput, yearInput } = renderDateInputBox( - { - value: null, - setValue, - }, - testContext, - ); - userEvent.type(yearInput, '1993'); - userEvent.type(monthInput, '12'); - userEvent.type(dayInput, '2'); - expect(setValue).not.toHaveBeenCalled(); + describe('with no initial value', () => { + test('value setter is not called when an ambiguous date is entered', () => { + const setValue = jest.fn(); + const { dayInput, monthInput, yearInput } = renderDateInputBox( + { + value: null, + setValue, + }, + testContext, + ); + userEvent.type(yearInput, '1993'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '2'); + expect(setValue).not.toHaveBeenCalled(); + }); + + test('value setter is called when an explicit date is entered', () => { + const setValue = jest.fn(); + const { dayInput, monthInput, yearInput } = renderDateInputBox( + { + value: null, + setValue, + }, + testContext, + ); + userEvent.type(yearInput, '1993'); + userEvent.type(monthInput, '12'); + userEvent.type(dayInput, '26'); + expect(setValue).toHaveBeenCalledWith( + expect.objectContaining(newUTC(1993, Month.December, 26)), + ); + }); }); - test('value setter is only called when an explicit date is entered', () => { - const setValue = jest.fn(); - const { dayInput, monthInput, yearInput } = renderDateInputBox( - { - value: null, - setValue, - }, - testContext, - ); - userEvent.type(yearInput, '1993'); - userEvent.type(monthInput, '12'); - userEvent.type(dayInput, '26'); - expect(setValue).toHaveBeenCalledWith( - expect.objectContaining(newUTC(1993, Month.December, 26)), - ); + describe('with an initial value', () => { + test('value setter is called when a new date is typed', () => { + const setValue = jest.fn(); + const { dayInput } = renderDateInputBox( + { + value: newUTC(1993, Month.December, 26), + setValue, + }, + testContext, + ); + userEvent.type(dayInput, '{backspace}5'); + expect(setValue).toHaveBeenCalledWith( + expect.objectContaining(newUTC(1993, Month.December, 25)), + ); + expect(dayInput).toHaveValue('25'); + }); + + test('value setter is _not_ called when new input is ambiguous', () => { + const setValue = jest.fn(); + const { dayInput } = renderDateInputBox( + { + value: newUTC(1993, Month.December, 26), + setValue, + }, + testContext, + ); + userEvent.type(dayInput, '{backspace}'); + expect(setValue).not.toHaveBeenCalled(); + expect(dayInput).toHaveValue('2'); + }); + + test('value setter is called when the input is cleared', () => { + const setValue = jest.fn(); + const { dayInput, monthInput, yearInput } = renderDateInputBox( + { + value: newUTC(1993, Month.December, 26), + setValue, + }, + testContext, + ); + userEvent.type(dayInput, '{backspace}{backspace}'); + userEvent.type(monthInput, '{backspace}{backspace}'); + userEvent.type( + yearInput, + '{backspace}{backspace}{backspace}{backspace}', + ); + expect(setValue).toHaveBeenCalledWith(expect.objectContaining(null)); + expect(dayInput).toHaveValue(''); + expect(monthInput).toHaveValue(''); + expect(yearInput).toHaveValue(''); + }); + + test('value setter is called when new date is invalid', () => { + const setValue = jest.fn(); + const { yearInput, monthInput, dayInput } = renderDateInputBox( + { + value: newUTC(1993, Month.December, 26), + setValue, + }, + testContext, + ); + + userEvent.type(monthInput, '{backspace}{backspace}'); + // TODO: with InvalidDate + expect(setValue).toHaveBeenCalled(); + expect(dayInput).toHaveValue('26'); + expect(monthInput).toHaveValue(''); + expect(yearInput).toHaveValue('1993'); + }); }); }); diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx index adf60460b5..b18f073813 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.stories.tsx @@ -22,6 +22,7 @@ import { } from '../../../testutils'; import { DateInputBox } from './DateInputBox'; +import { DateInputChangeEventHandler } from './DateInputBox.types'; const testDate = newUTC(1993, Month.December, 26); @@ -78,16 +79,25 @@ export const Basic: StoryFn = props => { } }, [props.value]); - const updateDate = (date: DateType) => { - setDate(date); + const updateDate: DateInputChangeEventHandler = ({ value }) => { + setDate(value); }; return ( - +
+ + + {isValidDate(date) + ? date.toISOString() + : date + ? 'Invalid' + : 'undefined'} + +
); }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx index 7a55005d68..883fa43f63 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.tsx @@ -1,6 +1,12 @@ -import React, { FocusEventHandler } from 'react'; +import React, { FocusEventHandler, useEffect } from 'react'; +import { isNull } from 'lodash'; import isEqual from 'lodash/isEqual'; +import { + isDateObject, + isInvalidDateObject, + isValidDate, +} from '@leafygreen-ui/date-utils'; import { cx } from '@leafygreen-ui/emotion'; import { useForwardedRef } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; @@ -15,9 +21,6 @@ import { isDateSegment, } from '../../../types'; import { - doesSomeSegmentExist, - doSegmentsFormValidDate, - getFormattedDateStringFromSegments, getMaxSegmentValue, getMinSegmentValue, getRelativeSegment, @@ -63,15 +66,8 @@ export const DateInputBox = React.forwardRef( }: DateInputBoxProps, fwdRef, ) => { - const { - formatParts, - disabled, - min, - max, - locale, - setIsDirty, - setInternalErrorMessage, - } = useSharedDatePickerContext(); + const { isDirty, formatParts, disabled, min, max, setIsDirty } = + useSharedDatePickerContext(); const { theme } = useDarkMode(); const containerRef = useForwardedRef(fwdRef, null); @@ -86,6 +82,13 @@ export const DateInputBox = React.forwardRef( return formattedValue; }; + /** if the value is a `Date` the component is dirty */ + useEffect(() => { + if (isDateObject(value) && !isDirty) { + setIsDirty(true); + } + }, [isDirty, setIsDirty, value]); + /** * When a segment is updated, * trigger a `change` event for the segment, and @@ -98,30 +101,19 @@ export const DateInputBox = React.forwardRef( const hasAnySegmentChanged = !isEqual(newSegments, prevSegments); if (hasAnySegmentChanged) { - const areAllEmpty = !doesSomeSegmentExist(newSegments); - const areAllFilled = isEverySegmentFilled(newSegments); - - if (areAllEmpty) { - // if no segment exists, set the external value to null - setValue?.(null); - } else if (areAllFilled) { - const areAllExplicit = isEverySegmentValueExplicit(newSegments); - const utcDate = newDateFromSegments(newSegments); - const isValidDate = doSegmentsFormValidDate(newSegments); - - if (areAllExplicit && !!utcDate) { - // Update the value iff all segments create a valid date. - setValue?.(utcDate); - } else if (!isValidDate) { - const dateString = getFormattedDateStringFromSegments( - newSegments, - locale, - ); - // This error state will be removed by `handleValidation` once a value is set - setInternalErrorMessage(`${dateString} is not a valid date`); - } - // If all values are filled, set the input as dirty - setIsDirty(true); + const newDate = newDateFromSegments(newSegments); + + const shouldSetValue = + isNull(newDate) || + (isValidDate(newDate) && isEverySegmentValueExplicit(newSegments)) || + (isInvalidDateObject(newDate) && + (isDirty || isEverySegmentFilled(newSegments))); + + if (shouldSetValue) { + setValue?.({ + value: newDate, + segments: newSegments, + }); } } }; diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts index 5c1bcede61..4d131eb8d5 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/DateInputBox.types.ts @@ -2,8 +2,18 @@ import { DateType } from '@leafygreen-ui/date-utils'; import { HTMLElementProps } from '@leafygreen-ui/lib'; import { SegmentRefs } from '../../../hooks'; +import { DateSegmentsState } from '../../../types'; import { DateInputSegmentChangeEventHandler } from '../DateInputSegment/DateInputSegment.types'; +export interface DateInputChangeEvent { + value: DateType; + segments: DateSegmentsState; +} + +export type DateInputChangeEventHandler = ( + changeEvent: DateInputChangeEvent, +) => void; + export interface DateInputBoxProps extends Omit, 'onChange'> { /** @@ -15,7 +25,7 @@ export interface DateInputBoxProps * Value setter callback. * Date object is in UTC time */ - setValue?: (date: DateType) => void; + setValue?: DateInputChangeEventHandler; /** * Callback fired when any segment changes, but not necessarily a full value diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts index cc70fdcf9d..a7a38f2493 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/index.ts @@ -1,26 +1 @@ -import { newUTC } from '@leafygreen-ui/date-utils'; - -import { DateSegmentsState } from '../../types'; -import { isValidSegmentName } from '../isValidSegment'; -import { isValidValueForSegment } from '../isValidValueForSegment'; - -/** Constructs a date object in UTC from day, month, year segments */ -export const newDateFromSegments = ( - segments: DateSegmentsState, -): Date | undefined => { - const isEverySegmentValid = Object.entries(segments).every( - ([key, value]) => - isValidSegmentName(key) && isValidValueForSegment(key, value), - ); - - if (isEverySegmentValid) { - const { day, month, year } = segments; - const newDate = newUTC(Number(year), Number(month) - 1, Number(day)); - // If day > daysInMonth, then the month will roll-over - const isCorrectMonth = newDate.getUTCMonth() === Number(month) - 1; - - if (isCorrectMonth) { - return newDate; - } - } -}; +export { newDateFromSegments } from './newDateFromSegments'; diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts index a07f7d7f72..89bc4bfe8d 100644 --- a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.spec.ts @@ -1,59 +1,85 @@ +import { isInvalidDateObject, Month, newUTC } from '@leafygreen-ui/date-utils'; + import { newDateFromSegments } from '.'; -describe('packages/date=picker/utils/newDateFromSegments', () => { - test('returns the date in UTC', () => { - const newDate = newDateFromSegments({ - day: '1', - month: '1', - year: '2023', +describe('packages/date-picker/utils/newDateFromSegments', () => { + describe('valid dates', () => { + test('returns the date in UTC', () => { + const newDate = newDateFromSegments({ + day: '01', + month: '01', + year: '2023', + }); + expect(newDate).toEqual(newUTC(2023, Month.January, 1)); }); - expect(newDate?.toISOString()).toEqual('2023-01-01T00:00:00.000Z'); - }); - test('returns a date outside the default range', () => { - const newDate = newDateFromSegments({ - day: '1', - month: '1', - year: '2100', + test('returns a date outside the default range', () => { + const newDate = newDateFromSegments({ + day: '01', + month: '01', + year: '2100', + }); + expect(newDate).toEqual(newUTC(2100, Month.January, 1)); + }); + + test('returns a date if day segment is truncated', () => { + const newDate = newDateFromSegments({ + day: '1', + month: '01', + year: '2023', + }); + expect(newDate).toEqual(newUTC(2023, Month.January, 1)); }); - expect(newDate?.toISOString()).toEqual('2100-01-01T00:00:00.000Z'); - }); - test('returns undefined if year is truncated (2-digits)', () => { - const newDate = newDateFromSegments({ - day: '1', - month: '1', - year: '20', + test('returns a date if month segment is truncated', () => { + const newDate = newDateFromSegments({ + day: '01', + month: '1', + year: '2023', + }); + expect(newDate).toEqual(newUTC(2023, Month.January, 1)); }); - expect(newDate).toBeUndefined(); }); - test('returns undefined if year is truncated (3-digits)', () => { - const newDate = newDateFromSegments({ - day: '1', - month: '1', - year: '199', + describe('null', () => { + test('returns `null` if all segments are empty', () => { + const newDate = newDateFromSegments({ + day: '', + month: '', + year: '', + }); + expect(newDate).toBeNull(); }); - expect(newDate).toBeUndefined(); }); - test('returns undefined if month/day combo is invalid', () => { - const newDate = newDateFromSegments({ - day: '31', - month: '02', - year: '2024', + describe('Invalid', () => { + test('returns "Invalid Date" if year segment is truncated', () => { + const newDate = newDateFromSegments({ + day: '01', + month: '01', + year: '20', + }); + expect(isInvalidDateObject(newDate)).toBe(true); }); - expect(newDate).toBeUndefined(); - }); + test('returns "Invalid Date" if month/day combo is invalid', () => { + const newDate = newDateFromSegments({ + day: '31', + month: '02', + year: '2024', + }); - test('returns undefined if any segment is empty', () => { - const newDate = newDateFromSegments({ - day: '', - month: '1', - year: '2023', + expect(isInvalidDateObject(newDate)).toBe(true); }); - expect(newDate).toBeUndefined(); + test('returns "Invalid Date" if any segment is empty', () => { + const newDate = newDateFromSegments({ + day: '', + month: '1', + year: '2023', + }); + + expect(isInvalidDateObject(newDate)).toBe(true); + }); }); }); diff --git a/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.ts b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.ts new file mode 100644 index 0000000000..70847799b5 --- /dev/null +++ b/packages/date-picker/src/shared/utils/newDateFromSegments/newDateFromSegments.ts @@ -0,0 +1,26 @@ +import { DateType, newUTC } from '@leafygreen-ui/date-utils'; + +import { DateSegmentsState } from '../../types'; +import { doesSomeSegmentExist } from '../doesSomeSegmentExist'; +import { isEverySegmentFilled } from '../isEverySegmentFilled'; +import { isEverySegmentValid } from '../isEverySegmentValid'; + +/** + * Constructs a date object in UTC from day, month, year segments + */ +export const newDateFromSegments = (segments: DateSegmentsState): DateType => { + if (isEverySegmentFilled(segments) && isEverySegmentValid(segments)) { + const { day, month, year } = segments; + const newDate = newUTC(Number(year), Number(month) - 1, Number(day)); + // If day > daysInMonth, then the month will roll-over + const isCorrectMonth = newDate.getUTCMonth() === Number(month) - 1; + + if (isCorrectMonth) { + return newDate; + } + } else if (!doesSomeSegmentExist(segments)) { + return null; + } + + return new Date('invalid'); +}; diff --git a/packages/date-utils/src/index.ts b/packages/date-utils/src/index.ts index 6f79645133..de8b401315 100644 --- a/packages/date-utils/src/index.ts +++ b/packages/date-utils/src/index.ts @@ -25,7 +25,7 @@ export { isSameUTCDay } from './isSameUTCDay'; export { isSameUTCMonth } from './isSameUTCMonth'; export { isSameUTCRange } from './isSameUTCRange'; export { isTodayTZ } from './isTodayTZ'; -export { isValidDate } from './isValidDate'; +export { isDateObject, isInvalidDateObject, isValidDate } from './isValidDate'; export { isValidLocale } from './isValidLocale'; export { maxDate } from './maxDate'; export { minDate } from './minDate'; diff --git a/packages/date-utils/src/isValidDate/index.ts b/packages/date-utils/src/isValidDate/index.ts index 5afda48f1d..7944165c74 100644 --- a/packages/date-utils/src/isValidDate/index.ts +++ b/packages/date-utils/src/isValidDate/index.ts @@ -1 +1,6 @@ -export { isValidDate, isValidDateString } from './isValidDate'; +export { + isDateObject, + isInvalidDateObject, + isValidDate, + isValidDateString, +} from './isValidDate'; diff --git a/packages/date-utils/src/isValidDate/isValidDate.spec.ts b/packages/date-utils/src/isValidDate/isValidDate.spec.ts index 801e4afdb9..490276bd13 100644 --- a/packages/date-utils/src/isValidDate/isValidDate.spec.ts +++ b/packages/date-utils/src/isValidDate/isValidDate.spec.ts @@ -1,6 +1,12 @@ +import { DateType } from '../../dist'; import { mockTimeZone } from '../testing/mockTimeZone'; -import { isValidDate, isValidDateString } from '.'; +import { + isDateObject, + isInvalidDateObject, + isValidDate, + isValidDateString, +} from '.'; describe('packages/date-utils/isValidDate', () => { test('accepts Date objects', () => { @@ -27,6 +33,30 @@ describe('packages/date-utils/isValidDate', () => { }); }); +describe('packages/date-utils/isDateObject', () => { + test('valid date', () => { + expect(isDateObject(new Date())).toBe(true); + }); + test('invalid date object', () => { + expect(isDateObject(new Date('invalid'))).toBe(true); + }); + test('null', () => { + expect(isDateObject(null)).toBe(false); + }); + test('undefined', () => { + expect(isDateObject(undefined)).toBe(false); + }); + test('string', () => { + expect(isDateObject('string')).toBe(false); + }); + test('number', () => { + expect(isDateObject(5)).toBe(false); + }); + test('empty object', () => { + expect(isDateObject({})).toBe(false); + }); +}); + describe('packages/date-utils/isValidDateString', () => { test('us format is valid', () => { expect(isValidDateString('12/26/1993')).toBeTruthy(); @@ -40,3 +70,18 @@ describe('packages/date-utils/isValidDateString', () => { expect(isValidDateString(undefined)).toBeFalsy(); }); }); + +describe('packages/date-utils/isInvalidDateObject', () => { + test('valid date', () => { + expect(isInvalidDateObject(new Date())).toBe(false); + }); + test('null', () => { + expect(isInvalidDateObject(null)).toBe(false); + }); + test('empty object', () => { + expect(isInvalidDateObject({} as DateType)).toBe(false); + }); + test('invalid Date', () => { + expect(isInvalidDateObject(new Date('invalid'))).toBe(true); + }); +}); diff --git a/packages/date-utils/src/isValidDate/isValidDate.ts b/packages/date-utils/src/isValidDate/isValidDate.ts index 274b204310..718e2b176e 100644 --- a/packages/date-utils/src/isValidDate/isValidDate.ts +++ b/packages/date-utils/src/isValidDate/isValidDate.ts @@ -3,6 +3,7 @@ import isNull from 'lodash/isNull'; import isUndefined from 'lodash/isUndefined'; import { DateType } from '../types'; +import { InvalidDate } from '../types/InvalidDate'; /** * An extension of `date-fns` {@link isValid} @@ -12,14 +13,10 @@ export const isValidDate = (date?: DateType): date is Date => { // Enumerating all cases to ensure test coverage if (isUndefined(date)) return false; if (isNull(date)) return false; - if (date.constructor.name !== 'Date') return false; + if (!isDateObject(date)) return false; + if (isInvalidDateObject(date)) return false; - try { - date?.toISOString(); - return isValid(date); - } catch (error) { - return false; - } + return isValid(date); }; /** @@ -32,3 +29,29 @@ export const isValidDateString = (str?: any): str is string => { !isUndefined(str) && typeof str === 'string' && !isNaN(Date.parse(str)) ); }; + +/** Whether the given object is a `Date` object */ +export const isDateObject = (date: any): date is Date | InvalidDate => { + return ( + !isNull(date) && + !isUndefined(date) && + typeof date === 'object' && + date.constructor.name == 'Date' && + typeof date.toISOString === 'function' + ); +}; + +/** + * Returns whether a given object is a Date object that will print `"Invalid Date"` + */ +export const isInvalidDateObject = (date: DateType): date is InvalidDate => { + if (isNull(date)) return false; + if (!isDateObject(date)) return false; + + try { + date.toISOString(); + return false; + } catch { + return true; + } +}; From 19e601fe421802ae0f71ef46488393688dbe17af Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 11 Jan 2024 17:25:43 -0500 Subject: [PATCH 349/351] fix DateInputChangeEventHandler import --- .../src/DatePicker/DatePickerInput/DatePickerInput.tsx | 7 +++++-- .../src/shared/components/DateInput/DateInputBox/index.ts | 5 ++++- .../date-picker/src/shared/components/DateInput/index.ts | 6 +++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index a883b12c28..8f24c10f5f 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -6,12 +6,15 @@ import React, { MouseEventHandler, } from 'react'; import { isNull } from 'lodash'; -import { DateInputChangeEventHandler } from 'src/shared/components/DateInput/DateInputBox/DateInputBox.types'; import { isInvalidDateObject, isSameUTCDay } from '@leafygreen-ui/date-utils'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; -import { DateFormField, DateInputBox } from '../../shared/components/DateInput'; +import { + DateFormField, + DateInputBox, + DateInputChangeEventHandler, +} from '../../shared/components/DateInput'; import { DateInputSegmentChangeEventHandler } from '../../shared/components/DateInput/DateInputSegment'; import { useSharedDatePickerContext } from '../../shared/context'; import { diff --git a/packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts b/packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts index 6334727ac3..d027909952 100644 --- a/packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts +++ b/packages/date-picker/src/shared/components/DateInput/DateInputBox/index.ts @@ -1,2 +1,5 @@ export { DateInputBox } from './DateInputBox'; -export type { DateInputBoxProps } from './DateInputBox.types'; +export type { + DateInputBoxProps, + DateInputChangeEventHandler, +} from './DateInputBox.types'; diff --git a/packages/date-picker/src/shared/components/DateInput/index.ts b/packages/date-picker/src/shared/components/DateInput/index.ts index 74de023c3c..8ba05e3866 100644 --- a/packages/date-picker/src/shared/components/DateInput/index.ts +++ b/packages/date-picker/src/shared/components/DateInput/index.ts @@ -1,4 +1,8 @@ export { CalendarButton } from './CalendarButton'; export { DateFormField } from './DateFormField'; -export { DateInputBox, type DateInputBoxProps } from './DateInputBox'; +export { + DateInputBox, + type DateInputBoxProps, + type DateInputChangeEventHandler, +} from './DateInputBox'; export { DateInputSegment } from './DateInputSegment'; From 28a733cc4562d25dcdb0618c4b46ca1b0c379022 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 11 Jan 2024 17:32:15 -0500 Subject: [PATCH 350/351] updates changeset version --- .changeset/eighty-kings-mix.md | 4 ++-- .changeset/smart-steaks-care.md | 4 ++-- packages/date-picker/package.json | 2 +- packages/date-utils/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.changeset/eighty-kings-mix.md b/.changeset/eighty-kings-mix.md index 64910cab2c..56b8c543ab 100644 --- a/.changeset/eighty-kings-mix.md +++ b/.changeset/eighty-kings-mix.md @@ -1,5 +1,5 @@ --- -'@leafygreen-ui/date-picker': major +'@leafygreen-ui/date-picker': minor --- -Inital release of `date-picker`. Use DatePicker to allow users to input a date +Initial pre-release of `date-picker`. Use DatePicker to allow users to input a date diff --git a/.changeset/smart-steaks-care.md b/.changeset/smart-steaks-care.md index 0f43ab654c..5489ec9ad3 100644 --- a/.changeset/smart-steaks-care.md +++ b/.changeset/smart-steaks-care.md @@ -1,5 +1,5 @@ --- -'@leafygreen-ui/date-utils': major +'@leafygreen-ui/date-utils': minor --- -Initial release of `date-utils`. DateUtils contains utility functions for managing and manipulating JS Date objects +Initial pre-release of `date-utils`. DateUtils contains utility functions for managing and manipulating JS Date objects diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index b53935c609..d3bdc374f1 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -1,6 +1,6 @@ { "name": "@leafygreen-ui/date-picker", - "version": "0.1.0", + "version": "0.0.1", "description": "LeafyGreen UI Kit Date Picker", "license": "Apache-2.0", "main": "./dist/index.js", diff --git a/packages/date-utils/package.json b/packages/date-utils/package.json index ef531592c4..5fa9496b68 100644 --- a/packages/date-utils/package.json +++ b/packages/date-utils/package.json @@ -1,7 +1,7 @@ { "name": "@leafygreen-ui/date-utils", - "version": "0.1.0", + "version": "0.0.1", "description": "LeafyGreen UI Kit Date Utils", "main": "./dist/index.js", "module": "./dist/esm/index.js", From ae4b58d7fe3ffddf045d1fd29251372cfaa7cb2e Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Thu, 11 Jan 2024 17:33:46 -0500 Subject: [PATCH 351/351] Update package.json --- packages/date-picker/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/date-picker/package.json b/packages/date-picker/package.json index d3bdc374f1..e6dc619952 100644 --- a/packages/date-picker/package.json +++ b/packages/date-picker/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@leafygreen-ui/a11y": "^1.4.11", - "@leafygreen-ui/date-utils": "^0.1.0", + "@leafygreen-ui/date-utils": "^0.0.1", "@leafygreen-ui/emotion": "^4.0.7", "@leafygreen-ui/form-field": "^0.2.0", "@leafygreen-ui/hooks": "^8.0.0",