@indico-data/design-system 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/build/generated/iconNames.ts +3 -0
  2. package/build/generated/iconTypes.ts +5 -0
  3. package/lib/components/forms/date/datePicker/DatePicker.d.ts +1 -2
  4. package/lib/components/forms/date/datePicker/__tests__/DatePicker.test.d.ts +1 -0
  5. package/lib/components/forms/date/datePicker/contants.d.ts +1 -1
  6. package/lib/components/forms/date/datePicker/index.d.ts +1 -0
  7. package/lib/components/forms/date/iconTriggerDatePicker/IconTriggerDatePicker.d.ts +3 -3
  8. package/lib/components/forms/date/iconTriggerDatePicker/__tests__/IconTriggerDatePicker.test.d.ts +1 -0
  9. package/lib/components/forms/date/inputDatePicker/SingleInputDatePicker.d.ts +5 -6
  10. package/lib/components/forms/date/inputDatePicker/__tests__/SingleInputDatePicker.test.d.ts +1 -0
  11. package/lib/components/forms/date/inputDatePicker/helpers.d.ts +1 -0
  12. package/lib/components/forms/date/inputDateRangePicker/InputDateRangePicker.d.ts +5 -6
  13. package/lib/components/forms/date/inputDateRangePicker/InputDateRangePicker.stories.d.ts +1 -1
  14. package/lib/components/forms/date/inputDateRangePicker/__tests__/InputDateRangePicker.test.d.ts +1 -0
  15. package/lib/components/icons/constants.d.ts +1 -0
  16. package/lib/components/index.d.ts +1 -0
  17. package/lib/index.d.ts +8 -7
  18. package/lib/index.esm.js +48 -47
  19. package/lib/index.esm.js.map +1 -1
  20. package/lib/index.js +48 -47
  21. package/lib/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/generateIconTypes.js +26 -0
  24. package/src/assets/indicons/api-doc.svg +3 -0
  25. package/src/assets/indicons/application-squares.svg +3 -0
  26. package/src/assets/indicons/arrow-dots.svg +3 -0
  27. package/src/assets/indicons/arrow-down.svg +9 -0
  28. package/src/assets/indicons/arrow-up-circle.svg +3 -0
  29. package/src/assets/indicons/arrows-cursor.svg +3 -0
  30. package/src/assets/indicons/bookmark-saved.svg +3 -0
  31. package/src/assets/indicons/check-circle.svg +3 -0
  32. package/src/assets/indicons/chevron-down.svg +3 -0
  33. package/src/assets/indicons/chevron-left.svg +3 -0
  34. package/src/assets/indicons/chevron-right.svg +3 -0
  35. package/src/assets/indicons/chevron-up.svg +3 -0
  36. package/src/assets/indicons/choose-library.svg +11 -0
  37. package/src/assets/indicons/circle-help.svg +3 -0
  38. package/src/assets/indicons/classification-document.svg +3 -0
  39. package/src/assets/indicons/classification-image.svg +3 -0
  40. package/src/assets/indicons/classification-page.svg +3 -0
  41. package/src/assets/indicons/classify-and-unbundle.svg +3 -0
  42. package/src/assets/indicons/coffee-1.svg +3 -0
  43. package/src/assets/indicons/coffee-2.svg +3 -0
  44. package/src/assets/indicons/coffee-fail.svg +3 -0
  45. package/src/assets/indicons/company-name.svg +3 -0
  46. package/src/assets/indicons/confidence-bar-1.svg +3 -0
  47. package/src/assets/indicons/confidence-bar-2.svg +3 -0
  48. package/src/assets/indicons/confidence-bar-3.svg +3 -0
  49. package/src/assets/indicons/confidence-bar-4.svg +3 -0
  50. package/src/assets/indicons/confidence-bars.svg +3 -0
  51. package/src/assets/indicons/data-and-access.svg +3 -0
  52. package/src/assets/indicons/elmos-fire.svg +3 -0
  53. package/src/assets/indicons/exclamation-circle-stroke.svg +13 -0
  54. package/src/assets/indicons/filter-outline.svg +7 -0
  55. package/src/assets/indicons/find-documents.svg +10 -0
  56. package/src/assets/indicons/gen-ai.svg +3 -0
  57. package/src/assets/indicons/grid-view.svg +3 -0
  58. package/src/assets/indicons/help-solid.svg +3 -0
  59. package/src/assets/indicons/highlight-outline.svg +10 -0
  60. package/src/assets/indicons/indico-logo-white.svg +3 -0
  61. package/src/assets/indicons/indico-o-white.svg +3 -0
  62. package/src/assets/indicons/indico-o.svg +3 -0
  63. package/src/assets/indicons/layout-complex.svg +3 -0
  64. package/src/assets/indicons/layout-simple.svg +3 -0
  65. package/src/assets/indicons/libraries-app.svg +15 -0
  66. package/src/assets/indicons/list-view.svg +3 -0
  67. package/src/assets/indicons/model-import.svg +3 -0
  68. package/src/assets/indicons/model-starter.svg +3 -0
  69. package/src/assets/indicons/no-collections.svg +10 -0
  70. package/src/assets/indicons/no-format.svg +3 -0
  71. package/src/assets/indicons/no-libraries.svg +11 -0
  72. package/src/assets/indicons/object-detection.svg +11 -0
  73. package/src/assets/indicons/page-thumbnail.svg +3 -0
  74. package/src/assets/indicons/pay-as-you-go.svg +3 -0
  75. package/src/assets/indicons/preview-view.svg +3 -0
  76. package/src/assets/indicons/radio-button.svg +5 -0
  77. package/src/assets/indicons/search-insights.svg +3 -0
  78. package/src/assets/indicons/search-thin.svg +3 -0
  79. package/src/assets/indicons/sort-down.svg +3 -0
  80. package/src/assets/indicons/square-plus.svg +3 -0
  81. package/src/assets/indicons/step-forward.svg +3 -0
  82. package/src/assets/indicons/surround-outline.svg +12 -0
  83. package/src/assets/indicons/thumbs-down.svg +3 -0
  84. package/src/assets/indicons/thumbs-up.svg +3 -0
  85. package/src/assets/indicons/warning-stroke.svg +17 -0
  86. package/src/assets/indicons/x-close.svg +3 -0
  87. package/src/assets/indicons/zoom-in.svg +10 -0
  88. package/src/assets/indicons/zoom-out.svg +9 -0
  89. package/src/components/floatUI/{FloatUI.test.tsx → __tests__/FloatUI.test.tsx} +3 -3
  90. package/src/components/forms/date/datePicker/DatePicker.tsx +2 -2
  91. package/src/components/forms/date/datePicker/__tests__/DatePicker.test.tsx +55 -0
  92. package/src/components/forms/date/datePicker/contants.ts +1 -1
  93. package/src/components/forms/date/datePicker/index.ts +1 -0
  94. package/src/components/forms/date/iconTriggerDatePicker/IconTriggerDatePicker.mdx +1 -1
  95. package/src/components/forms/date/iconTriggerDatePicker/IconTriggerDatePicker.stories.tsx +0 -10
  96. package/src/components/forms/date/iconTriggerDatePicker/IconTriggerDatePicker.tsx +9 -12
  97. package/src/components/forms/date/iconTriggerDatePicker/__tests__/IconTriggerDatePicker.test.tsx +47 -127
  98. package/src/components/forms/date/inputDatePicker/SingleInputDatePicker.stories.tsx +17 -3
  99. package/src/components/forms/date/inputDatePicker/SingleInputDatePicker.tsx +31 -30
  100. package/src/components/forms/date/inputDatePicker/__tests__/SingleInputDatePicker.test.tsx +118 -0
  101. package/src/components/forms/date/inputDatePicker/helpers.ts +3 -0
  102. package/src/components/forms/date/inputDateRangePicker/InputDateRangePicker.mdx +9 -3
  103. package/src/components/forms/date/inputDateRangePicker/InputDateRangePicker.stories.tsx +41 -44
  104. package/src/components/forms/date/inputDateRangePicker/InputDateRangePicker.tsx +83 -49
  105. package/src/components/forms/date/inputDateRangePicker/__tests__/InputDateRangePicker.test.tsx +96 -0
  106. package/src/components/icons/constants.ts +47 -0
  107. package/src/components/index.ts +1 -0
  108. /package/lib/components/floatUI/{FloatUI.test.d.ts → __tests__/FloatUI.test.d.ts} +0 -0
@@ -0,0 +1,55 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { DatePicker } from '../DatePicker';
3
+
4
+ describe('DatePicker', () => {
5
+ describe('Single Date Picker', () => {
6
+ it('Renders the default mode for date picker (single)', () => {
7
+ render(<DatePicker data-testid="date-picker" />);
8
+ expect(screen.getByTestId('date-picker')).toHaveAttribute('data-mode', 'single');
9
+ });
10
+
11
+ it('renders the date picker with a default of 2 months if no months are provided', () => {
12
+ render(<DatePicker data-testid="date-picker" />);
13
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(1);
14
+ });
15
+
16
+ it('renders the date picker with a custom number of months', () => {
17
+ render(<DatePicker numberOfMonths={3} data-testid="date-picker" />);
18
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(3);
19
+ });
20
+ });
21
+
22
+ describe('Range Date Picker', () => {
23
+ it('Renders the range mode for date picker', () => {
24
+ render(<DatePicker mode="range" data-testid="date-picker" />);
25
+ expect(screen.getByTestId('date-picker')).toHaveAttribute('data-mode', 'range');
26
+ });
27
+
28
+ it('renders the date range picker with a default of 2 months if no months are provided', () => {
29
+ render(<DatePicker mode="range" data-testid="date-picker" />);
30
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(2);
31
+ });
32
+
33
+ it('renders the date range picker with a custom number of months', () => {
34
+ render(<DatePicker mode="range" numberOfMonths={3} data-testid="date-picker" />);
35
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(3);
36
+ });
37
+ });
38
+
39
+ describe('Multiple Date Picker', () => {
40
+ it('Renders the multiple mode for date picker', () => {
41
+ render(<DatePicker mode="multiple" data-testid="date-picker" />);
42
+ expect(screen.getByTestId('date-picker')).toHaveAttribute('data-mode', 'multiple');
43
+ });
44
+
45
+ it('renders the date picker with a default of 2 months if no months are provided', () => {
46
+ render(<DatePicker mode="multiple" data-testid="date-picker" />);
47
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(1);
48
+ });
49
+
50
+ it('renders the date picker with a custom number of months', () => {
51
+ render(<DatePicker mode="multiple" numberOfMonths={3} data-testid="date-picker" />);
52
+ expect(document.getElementsByClassName('rdp-month')).toHaveLength(3);
53
+ });
54
+ });
55
+ });
@@ -4,7 +4,7 @@ export const getCommonProps = (props: CommonProps) => ({
4
4
  className: props.className,
5
5
  id: props.id,
6
6
  month: props.month,
7
- captionLayout: props.captionLayout,
7
+ captionLayout: props.captionLayout ?? 'dropdown',
8
8
  defaultMonth: props.defaultMonth,
9
9
  startMonth: props.startMonth,
10
10
  endMonth: props.endMonth,
@@ -0,0 +1 @@
1
+ export { DatePicker } from './DatePicker';
@@ -5,7 +5,7 @@ import { Row, Col } from '../../../grid/index';
5
5
  <Meta title="Forms/DatePicker/IconTriggerDatePicker" name="IconTriggerDatePicker" />
6
6
 
7
7
  # IconTriggerDatePicker
8
- The No Input Date Picker is to be used when we need to open the datepicker with a simple icon and dont care about the user typing in a value. It leverages our DatePicker component inside of a popper window.
8
+ The Icon Trigger Date Picker is to be used when we need to open the datepicker with a simple icon and dont care about the user typing in a value. It leverages our DatePicker component inside of a popper window.
9
9
  <Canvas of={IconTriggerDatePicker.Default} />
10
10
 
11
11
  ## Props
@@ -54,16 +54,6 @@ const meta: Meta<typeof IconTriggerDatePicker> = {
54
54
  },
55
55
  },
56
56
  },
57
- month: {
58
- control: 'date',
59
- description: 'The month to display.',
60
- table: {
61
- category: 'Props',
62
- type: {
63
- summary: 'Date',
64
- },
65
- },
66
- },
67
57
  triggerIcon: {
68
58
  control: 'text',
69
59
  description: 'The icon to use as a trigger.',
@@ -12,14 +12,14 @@ interface Props {
12
12
  isDisabled?: boolean;
13
13
  id: string;
14
14
  label?: string;
15
- onSelect?: (selected: Date | DateRange | Date[] | undefined) => void;
16
- month?: Date;
17
- selected?: Date | DateRange | Date[] | undefined;
15
+ onSelect?: (selected: Date | DateRange | undefined) => void;
16
+ selected?: Date | DateRange | undefined;
18
17
  triggerIcon: IconName;
19
18
  triggerIconSize: IconSizes;
20
19
  isOpen?: boolean;
21
20
  clearOnClose?: boolean;
22
21
  className?: string;
22
+ initialMonth?: Date;
23
23
  'data-testid'?: string;
24
24
  }
25
25
 
@@ -30,7 +30,6 @@ export const IconTriggerDatePicker = (props: Props) => {
30
30
  isDisabled,
31
31
  disableBeforeDate,
32
32
  disableAfterDate,
33
- month,
34
33
  id,
35
34
  label,
36
35
  onSelect,
@@ -40,27 +39,24 @@ export const IconTriggerDatePicker = (props: Props) => {
40
39
  mode,
41
40
  isOpen,
42
41
  clearOnClose,
42
+ initialMonth,
43
43
  ...rest
44
44
  } = props;
45
45
 
46
- const [localSelected, setLocalSelected] = useState<Date | DateRange | Date[] | undefined>(
47
- selected || undefined,
48
- );
46
+ const [localMonth, setLocalMonth] = useState<Date>(initialMonth ?? new Date());
49
47
 
50
48
  const handleSelect: OnSelectHandler<Date> = (date) => {
51
49
  if (!date) {
52
50
  onSelect && onSelect(undefined);
53
- setLocalSelected(undefined);
54
51
  } else {
55
52
  onSelect && onSelect(date);
56
- setLocalSelected(date);
57
53
  }
58
54
  };
59
55
 
60
56
  // clear selection if clear on close is true
61
57
  useEffect(() => {
62
58
  if (!isOpen && clearOnClose) {
63
- setLocalSelected(undefined);
59
+ onSelect && onSelect(undefined);
64
60
  }
65
61
  }, [isOpen, clearOnClose]);
66
62
 
@@ -77,11 +73,12 @@ export const IconTriggerDatePicker = (props: Props) => {
77
73
  <DatePicker
78
74
  isDisabled={isDisabled}
79
75
  mode={mode}
80
- month={month}
81
- selected={localSelected as Date | DateRange | undefined}
76
+ selected={selected}
82
77
  onSelect={handleSelect}
78
+ month={localMonth}
83
79
  startMonth={disableBeforeDate}
84
80
  endMonth={disableAfterDate}
81
+ onMonthChange={setLocalMonth}
85
82
  {...rest}
86
83
  />
87
84
  </FloatUI>
@@ -1,127 +1,47 @@
1
- it.todo('reenable these');
2
- // import React from 'react';
3
- // import { render, screen } from '@testing-library/react';
4
- // import { NoInputDatePicker } from '../NoInputDatePicker';
5
- // import { DateRange } from 'react-day-picker';
6
- // import userEvent from '@testing-library/user-event';
7
-
8
- // const today = new Date();
9
- // const mockOnChange = jest.fn();
10
- // let trigger: any;
11
-
12
- // describe('NoInputDatePicker', () => {
13
- // describe('single', () => {
14
- // beforeEach(() => {
15
- // render(
16
- // <NoInputDatePicker
17
- // id={'date-picker'}
18
- // label={'Pick a date'}
19
- // value={today}
20
- // isRangePicker={false}
21
- // triggerIcon="calendar"
22
- // triggerIconSize={[20]}
23
- // onChange={(date: Date | DateRange | undefined) => {
24
- // mockOnChange(date);
25
- // }}
26
- // />,
27
- // );
28
-
29
- // trigger = screen.getByTestId('datepicker-trigger-for-date-picker');
30
- // });
31
-
32
- // it('shows single date picker', async () => {
33
- // await userEvent.click(trigger);
34
- // expect(screen.getByTestId('datepicker-dialog')).toBeVisible();
35
- // expect(screen.getByTestId('single-datepicker')).toBeVisible();
36
- // });
37
-
38
- // it('opens the date picker when clicking on the trigger', async () => {
39
- // await userEvent.click(trigger);
40
- // const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
41
- // expect(datePickerDialogContainer).toBeVisible();
42
- // });
43
-
44
- // it('emits the right date picked for single', async () => {
45
- // await userEvent.click(trigger);
46
- // const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
47
- // expect(datePickerDialogContainer).toBeVisible();
48
-
49
- // const day28 = screen.getByText('28');
50
- // await userEvent.click(day28);
51
- // expect(mockOnChange).toHaveBeenCalledWith(
52
- // new Date(today.getFullYear(), today.getMonth(), 28),
53
- // );
54
- // });
55
-
56
- // it('closes date picker on single selection', async () => {
57
- // await userEvent.click(trigger);
58
- // const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
59
- // expect(datePickerDialogContainer).toBeVisible();
60
-
61
- // const day28 = screen.getByText('28');
62
- // await userEvent.click(day28);
63
- // expect(mockOnChange).toHaveBeenCalledWith(
64
- // new Date(today.getFullYear(), today.getMonth(), 28),
65
- // );
66
- // expect(datePickerDialogContainer).not.toBeVisible();
67
- // });
68
- // });
69
-
70
- // describe('range', () => {
71
- // beforeEach(() => {
72
- // render(
73
- // <NoInputDatePicker
74
- // id={'date-picker'}
75
- // label={'Pick a date'}
76
- // value={today}
77
- // isRangePicker={true}
78
- // triggerIcon="calendar"
79
- // triggerIconSize={[20]}
80
- // onChange={(date: Date | DateRange | undefined) => {
81
- // mockOnChange(date);
82
- // }}
83
- // />,
84
- // );
85
-
86
- // trigger = screen.getByTestId('datepicker-trigger-for-date-picker');
87
- // });
88
-
89
- // it('shows ranged date picker', async () => {
90
- // await userEvent.click(trigger);
91
- // expect(screen.getByTestId('datepicker-dialog')).toBeVisible();
92
- // expect(screen.getByTestId('range-datepicker')).toBeVisible();
93
- // });
94
-
95
- // it('emits the right date picked for range', async () => {
96
- // await userEvent.click(trigger);
97
- // const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
98
- // expect(datePickerDialogContainer).toBeVisible();
99
-
100
- // const day27 = screen.getByText('27');
101
- // await userEvent.click(day27);
102
- // const day28 = screen.getByText('28');
103
- // await userEvent.click(day28);
104
-
105
- // expect(mockOnChange).toHaveBeenCalledWith({
106
- // from: new Date(today.getFullYear(), today.getMonth(), 27),
107
- // to: new Date(today.getFullYear(), today.getMonth(), 28),
108
- // });
109
- // });
110
- // it('closes date picker on both ranged selection', async () => {
111
- // await userEvent.click(trigger);
112
- // const datePickerDialogContainer = screen.getByTestId('datepicker-dialog');
113
- // expect(datePickerDialogContainer).toBeVisible();
114
-
115
- // const day27 = screen.getByText('27');
116
- // await userEvent.click(day27);
117
- // const day28 = screen.getByText('28');
118
- // await userEvent.click(day28);
119
-
120
- // expect(mockOnChange).toHaveBeenCalledWith({
121
- // from: new Date(today.getFullYear(), today.getMonth(), 27),
122
- // to: new Date(today.getFullYear(), today.getMonth(), 28),
123
- // });
124
- // expect(datePickerDialogContainer).not.toBeVisible();
125
- // });
126
- // });
127
- // });
1
+ import { render, screen } from '@testing-library/react';
2
+ import { IconTriggerDatePicker } from '../IconTriggerDatePicker';
3
+ import userEvent from '@testing-library/user-event';
4
+
5
+ describe('DatePicker', () => {
6
+ it('The date picker opens when the icon is clicked', async () => {
7
+ render(
8
+ <IconTriggerDatePicker
9
+ ariaLabel={''}
10
+ id={'date-picker'}
11
+ triggerIcon={'checkbox'}
12
+ triggerIconSize={'xs'}
13
+ data-testid={'date-picker-testid'}
14
+ />,
15
+ );
16
+
17
+ const icon = screen.getByTestId('datepicker-trigger-for-date-picker');
18
+ expect(icon).toBeInTheDocument();
19
+ await userEvent.click(icon);
20
+ expect(screen.getByTestId('date-picker-testid')).toBeInTheDocument();
21
+ });
22
+
23
+ it('handles onSelect with the correct value', async () => {
24
+ const onSelect = jest.fn();
25
+ render(
26
+ <IconTriggerDatePicker
27
+ ariaLabel={''}
28
+ id={'date-picker'}
29
+ triggerIcon={'checkbox'}
30
+ triggerIconSize={'xs'}
31
+ data-testid={'date-picker-testid'}
32
+ onSelect={onSelect}
33
+ />,
34
+ );
35
+
36
+ const icon = screen.getByTestId('datepicker-trigger-for-date-picker');
37
+ expect(icon).toBeInTheDocument();
38
+ await userEvent.click(icon);
39
+ expect(screen.getByTestId('date-picker-testid')).toBeInTheDocument();
40
+ expect(onSelect).toHaveBeenCalledTimes(0);
41
+ const day = screen.getAllByRole('button', { name: /day/i })[0];
42
+ expect(day).toBeInTheDocument();
43
+ await userEvent.click(day);
44
+ // no need to be more specific as it will cause issues with pipelines having different time zones. This was verified manually at the time of writing
45
+ expect(onSelect).toHaveBeenCalledTimes(1);
46
+ });
47
+ });
@@ -1,5 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
- import { SingleInputDatePicker } from './SingleInputDatePicker';
2
+ import { SingleInputDatePicker, SingleInputDatePickerProps } from './SingleInputDatePicker';
3
3
  import { useArgs } from '@storybook/preview-api';
4
4
 
5
5
  const meta: Meta<typeof SingleInputDatePicker> = {
@@ -23,6 +23,18 @@ const meta: Meta<typeof SingleInputDatePicker> = {
23
23
  },
24
24
  },
25
25
  },
26
+ captionLayout: {
27
+ control: 'select',
28
+ options: ['dropdown', 'dropdown-months', 'dropdown-years', 'label'],
29
+ description:
30
+ 'The layout of the caption. Enables you to add or remove the dropdown navigation for months/years',
31
+ table: {
32
+ category: 'Props',
33
+ type: {
34
+ summary: 'dropdown | dropdown-months | dropdown-years | label',
35
+ },
36
+ },
37
+ },
26
38
  className: {
27
39
  control: false,
28
40
  description: 'Accepts a CSS class name',
@@ -163,7 +175,7 @@ const meta: Meta<typeof SingleInputDatePicker> = {
163
175
  },
164
176
  },
165
177
  },
166
- month: {
178
+ initialMonth: {
167
179
  control: 'date',
168
180
  description: 'The month to display.',
169
181
  table: {
@@ -196,9 +208,11 @@ export const SingleInput: Story = {
196
208
  clearOnClose: false,
197
209
  errorMessage: '',
198
210
  ariaLabel: 'Date Picker',
211
+ selected: new Date(),
199
212
  },
200
213
  render: (args) => {
201
- const [{ selected }, updateArgs] = useArgs();
214
+ const [{ selected }, updateArgs] = useArgs<SingleInputDatePickerProps>();
215
+
202
216
  const handleSelect = (date: Date | undefined) => {
203
217
  updateArgs({ selected: date });
204
218
  };
@@ -1,21 +1,22 @@
1
1
  import { useId, useState, useEffect } from 'react';
2
- import { format, isValid, parse } from 'date-fns';
2
+ import { isValid, parse } from 'date-fns';
3
3
  import { DatePicker } from '../datePicker/DatePicker';
4
4
  import { Input } from '../../input';
5
5
  import { IconName } from '@/types';
6
- import { DateRange } from 'react-day-picker';
7
6
  import { FloatUI } from '../../../floatUI';
7
+ import { formatDateAsString } from './helpers';
8
8
 
9
- interface SingleInputDatePickerProps {
9
+ export interface SingleInputDatePickerProps {
10
10
  ariaLabel: string;
11
11
  disableBeforeDate?: Date;
12
12
  disableAfterDate?: Date;
13
13
  isDisabled?: boolean;
14
+ captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
14
15
  id?: string;
15
16
  label?: string;
16
- onSelect?: (selected: Date | undefined) => void;
17
- month?: Date;
18
- selected?: Date | DateRange | Date[] | undefined;
17
+ onSelect: (selected: Date | undefined) => void;
18
+ initialMonth?: Date;
19
+ selected?: Date;
19
20
  isOpen?: boolean;
20
21
  clearOnClose?: boolean;
21
22
  className?: string;
@@ -33,10 +34,10 @@ export function SingleInputDatePicker(props: SingleInputDatePickerProps) {
33
34
  isDisabled,
34
35
  disableBeforeDate,
35
36
  disableAfterDate,
36
- month,
37
+ captionLayout,
38
+ initialMonth,
37
39
  id,
38
40
  label,
39
- onSelect,
40
41
  selected,
41
42
  isOpen,
42
43
  inputPlaceholder,
@@ -44,49 +45,48 @@ export function SingleInputDatePicker(props: SingleInputDatePickerProps) {
44
45
  inputIconName,
45
46
  isClearable,
46
47
  errorMessage,
48
+ onSelect,
47
49
  ...rest
48
50
  } = props;
49
51
 
50
52
  const inputId = useId();
51
53
 
52
- // Hold the month in state to control the calendar when the input changes
53
- const [localMonth, setLocalMonth] = useState(new Date());
54
-
55
- // Hold the selected date in state
56
- const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
54
+ // The text value is assumed to be unneeded by the consumer.
55
+ const [localTextValue, setLocalTextValue] = useState<string>(
56
+ selected ? formatDateAsString(selected) : '',
57
+ );
57
58
 
58
- // Hold the input value in state
59
- const [inputValue, setInputValue] = useState('');
59
+ const [localMonth, setLocalMonth] = useState<Date>(initialMonth ?? selected ?? new Date());
60
60
 
61
+ // When the day picker is selected, update the text value.
61
62
  const handleDayPickerSelect = (date: Date | undefined) => {
62
63
  if (!date) {
63
- setInputValue('');
64
- setSelectedDate(undefined);
64
+ setLocalTextValue('');
65
+ onSelect(undefined);
65
66
  } else {
66
- setSelectedDate(date);
67
- setLocalMonth(date);
68
- setInputValue(format(date, 'MM/dd/yyyy'));
67
+ setLocalTextValue(formatDateAsString(date));
68
+ onSelect(date);
69
69
  }
70
70
  };
71
71
 
72
+ // When the text input is changed, update the selected date.
72
73
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
73
- setInputValue(e.target.value); // keep the input value in sync
74
+ setLocalTextValue(e.target.value); // keep the input value in sync
74
75
 
75
76
  const parsedDate = parse(e.target.value, 'MM/dd/yyyy', new Date());
76
77
 
77
78
  if (isValid(parsedDate)) {
78
- setSelectedDate(parsedDate);
79
- setLocalMonth(parsedDate);
79
+ onSelect(parsedDate);
80
80
  } else {
81
- setSelectedDate(undefined);
81
+ onSelect(undefined);
82
82
  }
83
83
  };
84
84
 
85
85
  // clear selection if clear on close is true
86
86
  useEffect(() => {
87
87
  if (!isOpen && clearOnClose) {
88
- setSelectedDate(undefined);
89
- setInputValue('');
88
+ onSelect(undefined);
89
+ setLocalTextValue('');
90
90
  }
91
91
  }, [isOpen, clearOnClose]);
92
92
 
@@ -94,7 +94,7 @@ export function SingleInputDatePicker(props: SingleInputDatePickerProps) {
94
94
  <FloatUI isOpen={isOpen} ariaLabel={ariaLabel}>
95
95
  <Input
96
96
  id={inputId}
97
- value={inputValue}
97
+ value={localTextValue}
98
98
  placeholder={inputPlaceholder}
99
99
  isDisabled={isDisabled}
100
100
  iconName={inputIconName}
@@ -105,11 +105,12 @@ export function SingleInputDatePicker(props: SingleInputDatePickerProps) {
105
105
  name={'Date Picker'}
106
106
  />
107
107
  <DatePicker
108
- month={localMonth}
109
- onMonthChange={setLocalMonth}
108
+ captionLayout={captionLayout}
110
109
  mode="single"
111
- selected={selectedDate}
110
+ selected={selected}
112
111
  onSelect={handleDayPickerSelect}
112
+ month={localMonth}
113
+ onMonthChange={setLocalMonth}
113
114
  {...rest}
114
115
  />
115
116
  </FloatUI>
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { SingleInputDatePicker } from '../SingleInputDatePicker';
5
+ import { format, addDays } from 'date-fns';
6
+
7
+ const mockOnSelect = jest.fn();
8
+
9
+ describe('SingleInputDatePicker', () => {
10
+ const RealDate = Date;
11
+
12
+ beforeAll(() => {
13
+ global.Date = class extends RealDate {
14
+ constructor() {
15
+ super('2024-01-01T00:00:00Z');
16
+ }
17
+ } as any;
18
+ Object.assign(global.Date, RealDate);
19
+ });
20
+
21
+ afterAll(() => {
22
+ global.Date = RealDate;
23
+ });
24
+
25
+ beforeEach(() => {
26
+ jest.clearAllMocks();
27
+ });
28
+
29
+ it('opens the date picker and selects a date', async () => {
30
+ render(
31
+ <SingleInputDatePicker
32
+ ariaLabel="Single Input Date Picker"
33
+ data-testid="datepicker-testid"
34
+ captionLayout="label"
35
+ onSelect={mockOnSelect}
36
+ />,
37
+ );
38
+
39
+ const formInput = await screen.findByTestId('form-input-Date Picker');
40
+ expect(formInput).toBeInTheDocument();
41
+
42
+ await userEvent.click(formInput);
43
+
44
+ const datepicker = await screen.findByTestId('datepicker-testid');
45
+ expect(datepicker).toBeVisible();
46
+
47
+ const dayButtons = await within(datepicker).findAllByRole('button', { name: /\d+/ });
48
+ expect(dayButtons.length).toBeGreaterThan(0);
49
+
50
+ const lastDayButton = dayButtons[dayButtons.length - 1];
51
+ await userEvent.click(lastDayButton);
52
+
53
+ const expectedDate = new Date('2024-01-01T00:00:00Z');
54
+ const alternateDate = addDays(expectedDate, -1);
55
+
56
+ await waitFor(
57
+ () => {
58
+ const inputValue = (formInput as HTMLInputElement).value;
59
+ const formattedExpectedDate = format(expectedDate, 'MM/dd/yyyy');
60
+ const formattedAlternateDate = format(alternateDate, 'MM/dd/yyyy');
61
+
62
+ expect(inputValue === formattedExpectedDate || inputValue === formattedAlternateDate).toBe(
63
+ true,
64
+ );
65
+ },
66
+ { timeout: 2000 },
67
+ );
68
+ });
69
+
70
+ it('updates the selected value in the dropdown to match user input', async () => {
71
+ render(
72
+ <SingleInputDatePicker
73
+ ariaLabel="Single Input Date Picker"
74
+ captionLayout="label"
75
+ data-testid="datepicker-testid"
76
+ onSelect={mockOnSelect}
77
+ />,
78
+ );
79
+
80
+ const formInput = await screen.findByTestId('form-input-Date Picker');
81
+ expect(formInput).toBeInTheDocument();
82
+
83
+ await userEvent.click(formInput);
84
+ await userEvent.clear(formInput);
85
+ await userEvent.type(formInput, '12/31/2023');
86
+
87
+ await waitFor(() => {
88
+ expect(formInput).toHaveValue('12/31/2023');
89
+ });
90
+
91
+ await userEvent.click(formInput);
92
+
93
+ const datepicker = await screen.findByTestId('datepicker-testid');
94
+ expect(datepicker).toBeVisible();
95
+
96
+ const dayButtons = await within(datepicker).findAllByRole('button', { name: /\d+/ });
97
+ expect(dayButtons.length).toBeGreaterThan(0);
98
+
99
+ const lastDayButton = dayButtons[dayButtons.length - 1];
100
+ await userEvent.click(lastDayButton);
101
+
102
+ const expectedDate = new Date('2024-01-01T00:00:00Z');
103
+ const alternateDate = addDays(expectedDate, -1);
104
+
105
+ await waitFor(
106
+ () => {
107
+ const inputValue = (formInput as HTMLInputElement).value;
108
+ const formattedExpectedDate = format(expectedDate, 'MM/dd/yyyy');
109
+ const formattedAlternateDate = format(alternateDate, 'MM/dd/yyyy');
110
+ // date hack to make sure pipeline timezone does not break test.
111
+ expect(inputValue === formattedExpectedDate || inputValue === formattedAlternateDate).toBe(
112
+ true,
113
+ );
114
+ },
115
+ { timeout: 2000 },
116
+ );
117
+ });
118
+ });
@@ -0,0 +1,3 @@
1
+ import { format } from 'date-fns';
2
+
3
+ export const formatDateAsString = (date: Date) => format(date, 'MM/dd/yyyy');
@@ -7,12 +7,18 @@ import { Row, Col } from '../../../grid/index';
7
7
  # Input Date Range Picker
8
8
  The Input Date Range Picker is a visual representation of an input field that allows you to enter a date value to the input or select a date from the date picker. It leverages our DatePicker component inside of a FloatingUI window.
9
9
 
10
- <Canvas of={InputDateRangePicker.SingleInput} />
10
+ <Canvas of={InputDateRangePicker.RangeInput} />
11
11
 
12
12
  ## Props
13
13
  In addition to the props listed below, it also accepts all props listed [here](story=?path=/docs/forms-datepicker--datepicker)
14
- <Controls of={InputDateRangePicker.SingleInput} />
14
+ <Controls of={InputDateRangePicker.RangeInput} />
15
15
 
16
+ # Notes
17
+ It is expected to show the same date in both input fields if only one date has been selected. This represents a start and end date on the same date. A usecase for this behavior would be a separate time on that same date. Expect the `to` input to update once a range has been selected (or the `from` input has been updated) to reflect the selected range.
16
18
 
17
19
  ## ToDo
18
- - Create single component for all dropdown date pickers that accept an react element as the trigger, such as an icon, button, component, input, etc.
20
+ - Clear on closed property
21
+ - Clear all selections (we need to think this one through, my initial thought is this clear would be handled by the parent component, not the date picker itself.)
22
+ - Better error handling and react-hook-form support
23
+ - Decide whether we want one icon prop or two.
24
+ - Decide if we want individual labels for each input or a single label for the range.