@transferwise/components 46.156.0 → 46.157.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.
- package/build/Calendar/Calendar.constants.js +10 -0
- package/build/Calendar/Calendar.constants.js.map +1 -0
- package/build/Calendar/Calendar.constants.mjs +8 -0
- package/build/Calendar/Calendar.constants.mjs.map +1 -0
- package/build/Calendar/Calendar.js +121 -17
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.messages.js +12 -0
- package/build/Calendar/Calendar.messages.js.map +1 -1
- package/build/Calendar/Calendar.messages.mjs +12 -0
- package/build/Calendar/Calendar.messages.mjs.map +1 -1
- package/build/Calendar/Calendar.mjs +122 -18
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.js +5 -1
- package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.mjs +5 -1
- package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.js +20 -2
- package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.mjs +20 -2
- package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +60 -19
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +60 -19
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.js +6 -0
- package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs +6 -0
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +8 -3
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +8 -3
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
- package/build/Calendar/TableLink/TableLink.js +95 -0
- package/build/Calendar/TableLink/TableLink.js.map +1 -0
- package/build/Calendar/TableLink/TableLink.mjs +91 -0
- package/build/Calendar/TableLink/TableLink.mjs.map +1 -0
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +8 -3
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +8 -3
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.js +7 -0
- package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.mjs +7 -0
- package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
- package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.js +24 -0
- package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.js.map +1 -0
- package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.mjs +22 -0
- package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.mjs.map +1 -0
- package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +33 -0
- package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +1 -0
- package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +31 -0
- package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +1 -0
- package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.js +40 -0
- package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.js.map +1 -0
- package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.mjs +38 -0
- package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.mjs.map +1 -0
- package/build/DateLookup/DateLookup.js +20 -9
- package/build/DateLookup/DateLookup.js.map +1 -1
- package/build/DateLookup/DateLookup.mjs +20 -9
- package/build/DateLookup/DateLookup.mjs.map +1 -1
- package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.js +3 -5
- package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.js.map +1 -1
- package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.mjs +3 -5
- package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.mjs.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
- package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
- package/build/Typeahead/Typeahead.js +53 -27
- package/build/Typeahead/Typeahead.js.map +1 -1
- package/build/Typeahead/Typeahead.messages.js +3 -0
- package/build/Typeahead/Typeahead.messages.js.map +1 -1
- package/build/Typeahead/Typeahead.messages.mjs +3 -0
- package/build/Typeahead/Typeahead.messages.mjs.map +1 -1
- package/build/Typeahead/Typeahead.mjs +53 -27
- package/build/Typeahead/Typeahead.mjs.map +1 -1
- package/build/Typeahead/TypeaheadInput/TypeaheadInput.js +2 -2
- package/build/Typeahead/TypeaheadInput/TypeaheadInput.js.map +1 -1
- package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs +2 -2
- package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs.map +1 -1
- package/build/Typeahead/TypeaheadOption/TypeaheadOption.js +3 -1
- package/build/Typeahead/TypeaheadOption/TypeaheadOption.js.map +1 -1
- package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs +3 -1
- package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs.map +1 -1
- package/build/i18n/en.json +5 -0
- package/build/i18n/en.json.js +5 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +5 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +2 -2
- package/build/index.mjs +1 -1
- package/build/main.css +133 -43
- package/build/styles/Calendar/Calendar.css +112 -37
- package/build/styles/Calendar/TableLink/TableLink.css +115 -0
- package/build/styles/Typeahead/Typeahead.css +4 -0
- package/build/styles/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
- package/build/styles/main.css +133 -43
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/Calendar/Calendar.constants.d.ts +2 -0
- package/build/types/Calendar/Calendar.constants.d.ts.map +1 -0
- package/build/types/Calendar/Calendar.d.ts +16 -3
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.messages.d.ts +20 -0
- package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.types.d.ts +32 -0
- package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
- package/build/types/Calendar/DateHeader/DateHeader.d.ts +2 -0
- package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +7 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +6 -0
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +1 -0
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +2 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +5 -1
- package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
- package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +2 -1
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +1 -0
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
- package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts +8 -0
- package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts.map +1 -0
- package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
- package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
- package/build/types/Calendar/_helpers/index.d.ts +4 -0
- package/build/types/Calendar/_helpers/index.d.ts.map +1 -0
- package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts +3 -0
- package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts.map +1 -0
- package/build/types/Calendar/index.d.ts +1 -0
- package/build/types/Calendar/index.d.ts.map +1 -1
- package/build/types/DateLookup/DateLookup.d.ts +24 -5
- package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/DateLookup/index.d.ts +1 -0
- package/build/types/DateLookup/index.d.ts.map +1 -1
- package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts +6 -4
- package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts.map +1 -1
- package/build/types/IconButton/IconButton.d.ts +1 -1
- package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts +1 -1
- package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -1
- package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
- package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
- package/build/types/Title/Title.d.ts +2 -2
- package/build/types/Typeahead/Typeahead.d.ts.map +1 -1
- package/build/types/Typeahead/Typeahead.messages.d.ts +5 -0
- package/build/types/Typeahead/Typeahead.messages.d.ts.map +1 -1
- package/build/types/Typeahead/TypeaheadInput/TypeaheadInput.d.ts.map +1 -1
- package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts +1 -0
- package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts.map +1 -1
- package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
- package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
- package/build/types/index.d.ts +3 -2
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +10 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Calendar/Calendar.constants.ts +5 -0
- package/src/Calendar/Calendar.css +112 -37
- package/src/Calendar/Calendar.disabledDates.test.tsx +610 -0
- package/src/Calendar/Calendar.less +5 -35
- package/src/Calendar/Calendar.messages.ts +21 -0
- package/src/Calendar/Calendar.test.tsx +342 -32
- package/src/Calendar/Calendar.tsx +169 -19
- package/src/Calendar/Calendar.types.ts +35 -0
- package/src/Calendar/DateHeader/DateHeader.tsx +6 -0
- package/src/Calendar/DayCalendar/DayCalendar.tsx +39 -1
- package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +67 -27
- package/src/Calendar/MonthCalendar/MonthCalendar.tsx +8 -0
- package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +15 -3
- package/src/Calendar/TableLink/TableLink.css +115 -0
- package/src/Calendar/TableLink/TableLink.less +68 -0
- package/src/Calendar/TableLink/TableLink.tsx +117 -0
- package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +15 -3
- package/src/Calendar/YearCalendar/YearCalendar.tsx +9 -0
- package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.test.ts +90 -0
- package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.ts +25 -0
- package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
- package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
- package/src/Calendar/_helpers/index.ts +3 -0
- package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.test.ts +169 -0
- package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.ts +59 -0
- package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +118 -0
- package/src/Calendar/_stories/Calendar.story.tsx +144 -4
- package/src/Calendar/_stories/Calendar.test.story.tsx +114 -0
- package/src/Calendar/index.ts +7 -0
- package/src/DateLookup/DateLookup.disabledDates.test.tsx +312 -0
- package/src/DateLookup/DateLookup.test.tsx +34 -37
- package/src/DateLookup/DateLookup.tsx +37 -13
- package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +108 -0
- package/src/DateLookup/_stories/DateLookup.story.tsx +126 -2
- package/src/DateLookup/index.ts +6 -0
- package/src/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.tsx +12 -13
- package/src/Inputs/SelectInput/SelectInput.test.tsx +37 -0
- package/src/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
- package/src/Typeahead/Typeahead.css +4 -0
- package/src/Typeahead/Typeahead.less +2 -0
- package/src/Typeahead/Typeahead.messages.ts +5 -0
- package/src/Typeahead/Typeahead.test.tsx +80 -3
- package/src/Typeahead/Typeahead.tsx +50 -27
- package/src/Typeahead/TypeaheadInput/TypeaheadInput.tsx +2 -6
- package/src/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
- package/src/Typeahead/TypeaheadOption/TypeaheadOption.less +9 -0
- package/src/Typeahead/TypeaheadOption/TypeaheadOption.tsx +4 -2
- package/src/i18n/en.json +4 -0
- package/src/index.ts +3 -2
- package/src/main.css +133 -43
- package/build/Calendar/tableLink/TableLink.js +0 -61
- package/build/Calendar/tableLink/TableLink.js.map +0 -1
- package/build/Calendar/tableLink/TableLink.mjs +0 -57
- package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
- package/build/types/Calendar/tableLink/TableLink.d.ts.map +0 -1
- package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
- package/src/Calendar/tableLink/TableLink.tsx +0 -77
- /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
- /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Forms/DateLookup/Accessibility" />
|
|
4
|
+
|
|
5
|
+
# Accessibility
|
|
6
|
+
|
|
7
|
+
DateLookup wraps the Calendar component inside a dropdown panel. It provides full keyboard accessibility for opening, navigating, selecting, and dismissing dates.
|
|
8
|
+
|
|
9
|
+
## Keyboard Support
|
|
10
|
+
|
|
11
|
+
<table>
|
|
12
|
+
<thead>
|
|
13
|
+
<tr>
|
|
14
|
+
<th>Key</th>
|
|
15
|
+
<th>Context</th>
|
|
16
|
+
<th>Action</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<tr>
|
|
21
|
+
<td>
|
|
22
|
+
<code>ArrowLeft/Right/Up/Down</code>
|
|
23
|
+
</td>
|
|
24
|
+
<td>Trigger focused, panel closed</td>
|
|
25
|
+
<td>Opens the panel</td>
|
|
26
|
+
</tr>
|
|
27
|
+
<tr>
|
|
28
|
+
<td>
|
|
29
|
+
<code>ArrowLeft/Right/Up/Down</code>
|
|
30
|
+
</td>
|
|
31
|
+
<td>Panel open</td>
|
|
32
|
+
<td>Navigates between dates (see Calendar)</td>
|
|
33
|
+
</tr>
|
|
34
|
+
<tr>
|
|
35
|
+
<td>
|
|
36
|
+
<code>Enter</code> / <code>Space</code>
|
|
37
|
+
</td>
|
|
38
|
+
<td>Panel open, enabled date focused</td>
|
|
39
|
+
<td>Selects date, closes panel</td>
|
|
40
|
+
</tr>
|
|
41
|
+
<tr>
|
|
42
|
+
<td>
|
|
43
|
+
<code>Enter</code> / <code>Space</code>
|
|
44
|
+
</td>
|
|
45
|
+
<td>Panel open, disabled date focused</td>
|
|
46
|
+
<td>No action (panel stays open)</td>
|
|
47
|
+
</tr>
|
|
48
|
+
<tr>
|
|
49
|
+
<td>
|
|
50
|
+
<code>Escape</code>
|
|
51
|
+
</td>
|
|
52
|
+
<td>Panel open</td>
|
|
53
|
+
<td>Reverts to original value, closes panel</td>
|
|
54
|
+
</tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
|
|
58
|
+
<br />
|
|
59
|
+
|
|
60
|
+
### Click Behaviour
|
|
61
|
+
|
|
62
|
+
- **Clicking an enabled date**: selects it, closes the panel, fires `onChange`
|
|
63
|
+
- **Clicking a disabled date**: does not select, panel stays open, focus remains in the calendar grid
|
|
64
|
+
|
|
65
|
+
## Screen Reader Announcements
|
|
66
|
+
|
|
67
|
+
DateLookup relies on the Calendar's built-in accessibility features for date announcements. See [Calendar
|
|
68
|
+
Accessibility](?path=/docs/forms-calendar-accessibility--docs) for details on how dates are labelled and described.
|
|
69
|
+
|
|
70
|
+
### Trigger Button
|
|
71
|
+
|
|
72
|
+
The trigger button announces the currently selected date (or placeholder text when no date is selected). It uses `aria-controls` to associate with the panel.
|
|
73
|
+
|
|
74
|
+
### Panel
|
|
75
|
+
|
|
76
|
+
The dropdown panel is rendered with `role="dialog"` and is connected to the trigger via `aria-controls`. When opened, focus moves into the calendar grid.
|
|
77
|
+
|
|
78
|
+
## Disabled Dates (via `disabledRules`)
|
|
79
|
+
|
|
80
|
+
When `disabledRules` is provided, disabled dates remain focusable and keyboard-navigable:
|
|
81
|
+
|
|
82
|
+
- `aria-disabled="true"` marks the cell as unavailable
|
|
83
|
+
- "not available" is appended to the aria-label
|
|
84
|
+
- `aria-describedby` links to the InlinePrompt reason text
|
|
85
|
+
- Keyboard navigation reaches disabled dates (they are not skipped)
|
|
86
|
+
- Clicking or pressing Enter on a disabled date does **not** close the panel
|
|
87
|
+
|
|
88
|
+
## Out-of-Range Dates (via `min`/`max`)
|
|
89
|
+
|
|
90
|
+
Dates outside `min`/`max` boundaries use `aria-disabled="true"` (not the native HTML `disabled` attribute). They are rendered as interactive buttons in the grid and can be clicked; the `aria-label` appends "not available". Arrow key navigation clamps at the boundary, so keyboard users cannot navigate into out-of-range cells.
|
|
91
|
+
|
|
92
|
+
This distinction is intentional: `min`/`max` defines a hard boundary, while `disabledRules` marks dates that exist but are unavailable (users can discover them and understand why via the InlinePrompt).
|
|
93
|
+
|
|
94
|
+
### InlinePrompt
|
|
95
|
+
|
|
96
|
+
An InlinePrompt renders below the day grid inside the panel when the visible month contains disabled dates. It displays the configured `disabledRules.reason` text.
|
|
97
|
+
|
|
98
|
+
The prompt is linked to disabled cells via `aria-describedby`, so screen readers announce the reason when a disabled
|
|
99
|
+
date receives focus. See [Calendar Accessibility](?path=/docs/forms-calendar-accessibility--docs) for full details on
|
|
100
|
+
the prompt's announcement behaviour.
|
|
101
|
+
|
|
102
|
+
## Consumer Callbacks
|
|
103
|
+
|
|
104
|
+
The `onDateSelect` callback fires with `{ date, isEnabled }` whenever the user navigates to or clicks on any date. Consumers can use this to:
|
|
105
|
+
|
|
106
|
+
- Display a custom message explaining why a specific date is disabled
|
|
107
|
+
- Track which dates users attempt to select
|
|
108
|
+
- Provide alternative actions for disabled dates
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { StoryObj } from '@storybook/react-webpack5';
|
|
3
|
+
import { fn } from 'storybook/test';
|
|
4
|
+
import { action } from 'storybook/actions';
|
|
3
5
|
import {
|
|
4
6
|
createSandboxStory,
|
|
5
7
|
globalScope,
|
|
6
8
|
} from '../../../.storybook/components/sandbox/SandboxEditor';
|
|
9
|
+
import { storySourceWithoutNoise } from '../../../.storybook/helpers';
|
|
7
10
|
import { Size } from '../../common';
|
|
8
|
-
import DateLookup, { type DateLookupProps } from '..';
|
|
9
11
|
import Field from '../../Field';
|
|
12
|
+
import DateLookup, { type DateLookupProps } from '..';
|
|
10
13
|
|
|
11
14
|
export default {
|
|
12
15
|
component: DateLookup,
|
|
13
16
|
title: 'Forms/DateLookup',
|
|
17
|
+
args: {
|
|
18
|
+
onFocus: fn(),
|
|
19
|
+
onBlur: fn(),
|
|
20
|
+
},
|
|
14
21
|
argTypes: {
|
|
15
22
|
size: { control: 'select', options: [Size.SMALL, Size.MEDIUM, Size.LARGE] },
|
|
16
23
|
disabled: { control: 'boolean' },
|
|
@@ -34,7 +41,7 @@ thePast.setUTCDate(epoch.getUTCDate() - 10);
|
|
|
34
41
|
|
|
35
42
|
export const Sandbox = createSandboxStory({
|
|
36
43
|
code: `const App = () => {
|
|
37
|
-
const [value, setValue] = React.useState(new Date(
|
|
44
|
+
const [value, setValue] = React.useState(new Date(2024, 5, 14));
|
|
38
45
|
|
|
39
46
|
return (
|
|
40
47
|
<DateLookup
|
|
@@ -44,6 +51,11 @@ export const Sandbox = createSandboxStory({
|
|
|
44
51
|
placeholder="Choose date"
|
|
45
52
|
clearable
|
|
46
53
|
value={value}
|
|
54
|
+
disabledRules={{
|
|
55
|
+
weekdays: ['sat', 'sun'],
|
|
56
|
+
dates: [new Date(2024, 5, 19)],
|
|
57
|
+
reason: 'Weekends and holidays are unavailable.',
|
|
58
|
+
}}
|
|
47
59
|
onChange={(date) => setValue(date)}
|
|
48
60
|
/>
|
|
49
61
|
);
|
|
@@ -120,3 +132,115 @@ export const RightAligned: Story = {
|
|
|
120
132
|
);
|
|
121
133
|
},
|
|
122
134
|
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Use `disabledRules` to disable specific weekdays, individual dates, or date ranges.
|
|
138
|
+
* Disabled dates remain focusable and keyboard-navigable — clicking or focusing one
|
|
139
|
+
* keeps the panel open, fires `onDateSelect` with `isEnabled: false`, and shows a
|
|
140
|
+
* negative inline prompt with the `reason` text. `min`/`max` out-of-range dates show
|
|
141
|
+
* a built-in boundary prompt (e.g. "Please select a date between 9 December 2024 and
|
|
142
|
+
* 22 January 2025.").
|
|
143
|
+
*
|
|
144
|
+
* The `reason` field is **required for accessibility**: it renders as an InlinePrompt
|
|
145
|
+
* below the grid and is linked to every disabled cell via `aria-describedby`, so screen
|
|
146
|
+
* reader users understand why a date is unavailable. For per-date reason customisation
|
|
147
|
+
* see [DisabledDatesWithDynamicReason](#disabled-dates-with-dynamic-reason).
|
|
148
|
+
*/
|
|
149
|
+
export const DisabledDates: Story = storySourceWithoutNoise<DateLookupProps>({
|
|
150
|
+
args: {
|
|
151
|
+
size: Size.MEDIUM,
|
|
152
|
+
label: 'Select a date',
|
|
153
|
+
placeholder: 'Choose date',
|
|
154
|
+
monthFormat: 'long',
|
|
155
|
+
min: new Date(2024, 11, 9),
|
|
156
|
+
max: new Date(2025, 0, 22),
|
|
157
|
+
},
|
|
158
|
+
render: function Render(args: DateLookupProps) {
|
|
159
|
+
const min = new Date(2024, 11, 9);
|
|
160
|
+
const max = new Date(2025, 0, 22);
|
|
161
|
+
const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
|
|
162
|
+
const [value, setValue] = useState<Date | null>(null);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<DateLookup
|
|
166
|
+
{...args}
|
|
167
|
+
min={args.min ? new Date(args.min) : undefined}
|
|
168
|
+
max={args.max ? new Date(args.max) : undefined}
|
|
169
|
+
value={value}
|
|
170
|
+
disabledRules={{
|
|
171
|
+
weekdays: ['sat', 'sun'],
|
|
172
|
+
dates: bankHolidays,
|
|
173
|
+
ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
|
|
174
|
+
reason: 'Some dates are unavailable for this service.',
|
|
175
|
+
}}
|
|
176
|
+
onChange={(date) => {
|
|
177
|
+
action('onChange')(date);
|
|
178
|
+
setValue(date);
|
|
179
|
+
}}
|
|
180
|
+
onDateSelect={({ date, isEnabled }) => {
|
|
181
|
+
action('onDateSelect')({ date, isEnabled });
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* When different disabled dates warrant different explanations, drive the `reason`
|
|
190
|
+
* dynamically from `onDateSelect`. The `isEnabled` flag tells you whether the focused
|
|
191
|
+
* date is selectable; from there you can look up the specific reason and update state.
|
|
192
|
+
*
|
|
193
|
+
* `min`/`max` out-of-range dates are handled automatically — the component shows a
|
|
194
|
+
* built-in prompt with the boundary dates, so `determineReason` only needs to cover
|
|
195
|
+
* rule-disabled dates.
|
|
196
|
+
*/
|
|
197
|
+
export const DisabledDatesWithDynamicReason: Story = storySourceWithoutNoise<DateLookupProps>({
|
|
198
|
+
args: {
|
|
199
|
+
size: Size.MEDIUM,
|
|
200
|
+
label: 'Select a date',
|
|
201
|
+
placeholder: 'Choose date',
|
|
202
|
+
monthFormat: 'long',
|
|
203
|
+
min: new Date(2024, 11, 9),
|
|
204
|
+
max: new Date(2025, 0, 22),
|
|
205
|
+
},
|
|
206
|
+
render: function Render(args: DateLookupProps) {
|
|
207
|
+
const DEFAULT_REASON = 'Some dates are unavailable for this service.';
|
|
208
|
+
const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
|
|
209
|
+
const [value, setValue] = useState<Date | null>(null);
|
|
210
|
+
const [reason, setReason] = useState(DEFAULT_REASON);
|
|
211
|
+
|
|
212
|
+
function determineReason(date: Date, isEnabled: boolean): string {
|
|
213
|
+
if (isEnabled) return DEFAULT_REASON;
|
|
214
|
+
if (bankHolidays.some((holiday) => holiday.toDateString() === date.toDateString())) {
|
|
215
|
+
return 'This date is a bank holiday.';
|
|
216
|
+
}
|
|
217
|
+
if (date.getDay() === 0 || date.getDay() === 6) {
|
|
218
|
+
return 'We do not operate on weekends.';
|
|
219
|
+
}
|
|
220
|
+
return 'This date is unavailable due to scheduled maintenance.';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<DateLookup
|
|
225
|
+
{...args}
|
|
226
|
+
min={args.min ? new Date(args.min) : undefined}
|
|
227
|
+
max={args.max ? new Date(args.max) : undefined}
|
|
228
|
+
value={value}
|
|
229
|
+
disabledRules={{
|
|
230
|
+
weekdays: ['sat', 'sun'],
|
|
231
|
+
dates: bankHolidays,
|
|
232
|
+
ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
|
|
233
|
+
reason,
|
|
234
|
+
}}
|
|
235
|
+
onChange={(date) => {
|
|
236
|
+
action('onChange')(date);
|
|
237
|
+
setValue(date);
|
|
238
|
+
}}
|
|
239
|
+
onDateSelect={({ date, isEnabled }) => {
|
|
240
|
+
action('onDateSelect')({ date, isEnabled });
|
|
241
|
+
setReason(determineReason(date, isEnabled));
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
});
|
package/src/DateLookup/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AvatarLayoutProps } from '../../AvatarLayout';
|
|
2
2
|
import Button from '../../Button';
|
|
3
|
+
import type { NewButtonProps } from '../../Button';
|
|
3
4
|
import {
|
|
4
5
|
SelectInput,
|
|
5
6
|
SelectInputOptionContent,
|
|
@@ -8,13 +9,7 @@ import {
|
|
|
8
9
|
import type { CurrencyType, ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
|
|
9
10
|
import { ChevronDown } from '@transferwise/icons';
|
|
10
11
|
import { Flag } from '@wise/art';
|
|
11
|
-
import {
|
|
12
|
-
type ButtonHTMLAttributes,
|
|
13
|
-
forwardRef,
|
|
14
|
-
type MouseEventHandler,
|
|
15
|
-
useMemo,
|
|
16
|
-
useState,
|
|
17
|
-
} from 'react';
|
|
12
|
+
import { forwardRef, type MouseEventHandler, useMemo, useState } from 'react';
|
|
18
13
|
import { useIntl } from 'react-intl';
|
|
19
14
|
|
|
20
15
|
import messages from '../ExpressiveMoneyInput.messages';
|
|
@@ -44,6 +39,11 @@ export type CurrencySelectorProps = {
|
|
|
44
39
|
onSearchChange?: (payload: { query: string; resultCount: number }) => void;
|
|
45
40
|
} & Pick<ExpressiveMoneyInputProps, 'currency'>;
|
|
46
41
|
|
|
42
|
+
type ButtonInputProps = Omit<
|
|
43
|
+
Extract<NewButtonProps, { as?: 'button' }>,
|
|
44
|
+
'v2' | 'size' | 'priority' | 'className' | 'href' | 'target'
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
47
|
export const CurrencySelector = ({
|
|
48
48
|
id,
|
|
49
49
|
currency,
|
|
@@ -109,7 +109,6 @@ export const CurrencySelector = ({
|
|
|
109
109
|
renderTrigger={() => (
|
|
110
110
|
<SelectInputTriggerButton
|
|
111
111
|
as={ButtonInput}
|
|
112
|
-
// @ts-expect-error new (v2) ButtonProps
|
|
113
112
|
addonStart={{
|
|
114
113
|
type: 'avatar',
|
|
115
114
|
value: [
|
|
@@ -122,11 +121,11 @@ export const CurrencySelector = ({
|
|
|
122
121
|
}),
|
|
123
122
|
},
|
|
124
123
|
]
|
|
125
|
-
.filter(
|
|
124
|
+
.filter((avatar): avatar is NonNullable<typeof avatar> => avatar != null)
|
|
126
125
|
.filter((avatar) => !(avatar && Object.keys(avatar).length === 0)),
|
|
127
126
|
}}
|
|
128
127
|
addonEnd={disabled ? undefined : { type: 'icon', value: <ChevronDown /> }}
|
|
129
|
-
onClick={
|
|
128
|
+
onClick={handleTriggerClick}
|
|
130
129
|
>
|
|
131
130
|
<>
|
|
132
131
|
<span aria-hidden>{formatCurrencyCode(currency)}</span>
|
|
@@ -151,9 +150,9 @@ export const CurrencySelector = ({
|
|
|
151
150
|
);
|
|
152
151
|
};
|
|
153
152
|
|
|
154
|
-
export const ButtonInput = forwardRef(function ButtonInput(
|
|
155
|
-
{ children, ...rest }
|
|
156
|
-
ref
|
|
153
|
+
export const ButtonInput = forwardRef<HTMLButtonElement, ButtonInputProps>(function ButtonInput(
|
|
154
|
+
{ children, ...rest },
|
|
155
|
+
ref,
|
|
157
156
|
) {
|
|
158
157
|
return (
|
|
159
158
|
<Button
|
|
@@ -7,9 +7,12 @@ import { render, mockMatchMedia, mockResizeObserver } from '../../test-utils';
|
|
|
7
7
|
import {
|
|
8
8
|
SelectInput,
|
|
9
9
|
SelectInputOptionContent,
|
|
10
|
+
SelectInputTriggerButton,
|
|
10
11
|
type SelectInputOptionItem,
|
|
11
12
|
type SelectInputProps,
|
|
12
13
|
} from '.';
|
|
14
|
+
import Button from '../../Button';
|
|
15
|
+
import type { ButtonProps as ResolverButtonProps } from '../../Button/Button.resolver';
|
|
13
16
|
import Field from '../../Field';
|
|
14
17
|
|
|
15
18
|
mockMatchMedia();
|
|
@@ -337,6 +340,40 @@ describe('SelectInput', () => {
|
|
|
337
340
|
expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
|
|
338
341
|
});
|
|
339
342
|
|
|
343
|
+
it('renders addonStart and addonEnd when using trigger as Button', () => {
|
|
344
|
+
type V2ButtonProps = Extract<ResolverButtonProps, { v2: true }>;
|
|
345
|
+
|
|
346
|
+
render(
|
|
347
|
+
<SelectInput
|
|
348
|
+
items={[{ type: 'option', value: 'USD' }]}
|
|
349
|
+
renderTrigger={({ content, className, size }) => {
|
|
350
|
+
const triggerButtonProps: V2ButtonProps = {
|
|
351
|
+
v2: true,
|
|
352
|
+
size,
|
|
353
|
+
className,
|
|
354
|
+
addonStart: {
|
|
355
|
+
type: 'icon',
|
|
356
|
+
value: <span data-testid="select-trigger-addon-start">Start</span>,
|
|
357
|
+
},
|
|
358
|
+
addonEnd: {
|
|
359
|
+
type: 'icon',
|
|
360
|
+
value: <span data-testid="select-trigger-addon-end">End</span>,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<SelectInputTriggerButton as={Button} {...triggerButtonProps}>
|
|
366
|
+
{content}
|
|
367
|
+
</SelectInputTriggerButton>
|
|
368
|
+
);
|
|
369
|
+
}}
|
|
370
|
+
/>,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(screen.getByTestId('select-trigger-addon-start')).toBeInTheDocument();
|
|
374
|
+
expect(screen.getByTestId('select-trigger-addon-end')).toBeInTheDocument();
|
|
375
|
+
});
|
|
376
|
+
|
|
340
377
|
it('deduplicates search results across groups using compareValues as key', async () => {
|
|
341
378
|
interface Currency {
|
|
342
379
|
code: string;
|
|
@@ -5,7 +5,7 @@ import { PolymorphicWithOverrides } from '../../../common/PolymorphicWithOverrid
|
|
|
5
5
|
import { Merge } from '../../../utils';
|
|
6
6
|
import { SelectInputTriggerButtonPropsContext } from '../SelectInput.contexts';
|
|
7
7
|
|
|
8
|
-
export type SelectInputTriggerButtonElementType =
|
|
8
|
+
export type SelectInputTriggerButtonElementType = React.ElementType;
|
|
9
9
|
|
|
10
10
|
export type SelectInputTriggerButtonProps<
|
|
11
11
|
T extends SelectInputTriggerButtonElementType = 'button',
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
.typeahead .dropdown-menu {
|
|
8
8
|
max-width: 100%;
|
|
9
9
|
width: 100%;
|
|
10
|
+
scroll-padding-top: 8px;
|
|
11
|
+
scroll-padding-top: var(--size-8);
|
|
12
|
+
scroll-padding-bottom: 8px;
|
|
13
|
+
scroll-padding-bottom: var(--size-8);
|
|
10
14
|
}
|
|
11
15
|
.np-theme-personal--forest-green .typeahead .dropdown-menu,
|
|
12
16
|
.np-theme-personal--bright-green .typeahead .dropdown-menu,
|
|
@@ -6,4 +6,9 @@ export default defineMessages({
|
|
|
6
6
|
defaultMessage: 'Clear',
|
|
7
7
|
description: 'Description of clear button',
|
|
8
8
|
},
|
|
9
|
+
suggestionsLabel: {
|
|
10
|
+
id: 'neptune.Typeahead.suggestionsLabel',
|
|
11
|
+
defaultMessage: 'Suggestions',
|
|
12
|
+
description: 'Accessible label for the list of typeahead suggestions',
|
|
13
|
+
},
|
|
9
14
|
});
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import Field from '../Field';
|
|
2
|
-
import { mockMatchMedia, render, screen, fireEvent } from '../test-utils';
|
|
2
|
+
import { mockMatchMedia, render, screen, fireEvent, userEvent } from '../test-utils';
|
|
3
3
|
import { wait } from '../test-utils/wait';
|
|
4
4
|
import Typeahead from './Typeahead';
|
|
5
5
|
|
|
6
6
|
mockMatchMedia();
|
|
7
7
|
|
|
8
|
+
const scrollIntoView = jest.fn();
|
|
9
|
+
|
|
8
10
|
describe('Typeahead', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
HTMLElement.prototype.scrollIntoView = scrollIntoView;
|
|
13
|
+
scrollIntoView.mockClear();
|
|
14
|
+
});
|
|
15
|
+
|
|
9
16
|
it('supports `Field` for labeling', () => {
|
|
10
17
|
render(
|
|
11
18
|
<Field id="test" label="Tags">
|
|
@@ -22,7 +29,7 @@ describe('Typeahead', () => {
|
|
|
22
29
|
<Typeahead id="test" name="test" options={[]} onChange={() => {}} />
|
|
23
30
|
</Field>,
|
|
24
31
|
);
|
|
25
|
-
expect(screen.queryByRole('
|
|
32
|
+
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
|
|
26
33
|
});
|
|
27
34
|
it('does render a dropdown when only a footer is provided', () => {
|
|
28
35
|
render(
|
|
@@ -30,7 +37,7 @@ describe('Typeahead', () => {
|
|
|
30
37
|
<Typeahead id="test" name="test" options={[]} footer={<p>hello</p>} onChange={() => {}} />
|
|
31
38
|
</Field>,
|
|
32
39
|
);
|
|
33
|
-
expect(screen.getByRole('
|
|
40
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
34
41
|
});
|
|
35
42
|
});
|
|
36
43
|
|
|
@@ -74,6 +81,76 @@ describe('Typeahead', () => {
|
|
|
74
81
|
expect(onChange).toHaveBeenCalled();
|
|
75
82
|
});
|
|
76
83
|
|
|
84
|
+
it('scrolls options into view when navigating with arrow keys', async () => {
|
|
85
|
+
render(
|
|
86
|
+
<Typeahead
|
|
87
|
+
id="test"
|
|
88
|
+
name="test"
|
|
89
|
+
options={[{ label: 'Option 1' }, { label: 'Option 2' }]}
|
|
90
|
+
minQueryLength={0}
|
|
91
|
+
onChange={() => {}}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
const input = screen.getByRole('combobox');
|
|
95
|
+
|
|
96
|
+
await userEvent.click(input);
|
|
97
|
+
await userEvent.keyboard('{ArrowDown}{ArrowDown}');
|
|
98
|
+
|
|
99
|
+
expect(input).toHaveFocus();
|
|
100
|
+
expect(scrollIntoView).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(scrollIntoView).toHaveBeenNthCalledWith(1, { block: 'nearest' });
|
|
102
|
+
expect(scrollIntoView).toHaveBeenNthCalledWith(2, { block: 'nearest' });
|
|
103
|
+
expect(scrollIntoView.mock.contexts[0]).toBe(screen.getByText('Option 1').closest('li'));
|
|
104
|
+
expect(scrollIntoView.mock.contexts[1]).toBe(screen.getByText('Option 2').closest('li'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('uses one Tab stop for the suggestions', async () => {
|
|
108
|
+
render(
|
|
109
|
+
<>
|
|
110
|
+
<Typeahead
|
|
111
|
+
id="test"
|
|
112
|
+
name="test"
|
|
113
|
+
options={[{ label: 'Option 1' }, { label: 'Option 2' }]}
|
|
114
|
+
minQueryLength={0}
|
|
115
|
+
onChange={() => {}}
|
|
116
|
+
/>
|
|
117
|
+
<button type="button">Next control</button>
|
|
118
|
+
</>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const input = screen.getByRole('combobox');
|
|
122
|
+
await userEvent.click(input);
|
|
123
|
+
await userEvent.tab();
|
|
124
|
+
|
|
125
|
+
expect(screen.getByRole('listbox')).toHaveFocus();
|
|
126
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
127
|
+
expect(screen.getByRole('option', { name: 'Option 1' })).toHaveClass(
|
|
128
|
+
'tw-dropdown-item--focused',
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByRole('listbox')).toHaveFocus();
|
|
131
|
+
await userEvent.tab();
|
|
132
|
+
expect(screen.getByRole('button', { name: 'Next control' })).toHaveFocus();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('uses the active option style when hovering a suggestion', async () => {
|
|
136
|
+
render(
|
|
137
|
+
<Typeahead
|
|
138
|
+
id="test"
|
|
139
|
+
name="test"
|
|
140
|
+
options={[{ label: 'Option 1' }, { label: 'Option 2' }]}
|
|
141
|
+
minQueryLength={0}
|
|
142
|
+
onChange={() => {}}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
await userEvent.click(screen.getByRole('combobox'));
|
|
147
|
+
await userEvent.hover(screen.getByRole('option', { name: 'Option 2' }));
|
|
148
|
+
|
|
149
|
+
expect(screen.getByRole('option', { name: 'Option 2' })).toHaveClass(
|
|
150
|
+
'tw-dropdown-item--focused',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
77
154
|
it('shows clear button when there is a value', () => {
|
|
78
155
|
render(
|
|
79
156
|
<Typeahead
|