@transferwise/components 46.30.2 → 46.32.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/index.js +931 -523
- package/build/index.js.map +1 -1
- package/build/index.mjs +928 -523
- package/build/index.mjs.map +1 -1
- package/build/main.css +135 -0
- package/build/styles/carousel/Carousel.css +135 -0
- package/build/styles/main.css +135 -0
- package/build/types/carousel/Carousel.d.ts +26 -0
- package/build/types/carousel/Carousel.d.ts.map +1 -0
- package/build/types/carousel/index.d.ts +3 -0
- package/build/types/carousel/index.d.ts.map +1 -0
- package/build/types/common/card/Card.d.ts +2 -2
- package/build/types/common/card/Card.d.ts.map +1 -1
- package/build/types/dateInput/DateInput.d.ts +5 -4
- package/build/types/dateInput/DateInput.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts +11 -4
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/field/Field.d.ts +12 -0
- package/build/types/field/Field.d.ts.map +1 -0
- package/build/types/index.d.ts +6 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/Input.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/inputs/_common.d.ts +2 -2
- package/build/types/inputs/_common.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +24 -0
- package/build/types/inputs/contexts.d.ts.map +1 -0
- package/build/types/label/Label.d.ts +9 -0
- package/build/types/label/Label.d.ts.map +1 -0
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/promoCard/PromoCard.d.ts +16 -5
- package/build/types/promoCard/PromoCard.d.ts.map +1 -1
- package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
- package/build/types/switch/Switch.d.ts +6 -3
- package/build/types/switch/Switch.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/carousel/Carousel.css +135 -0
- package/src/carousel/Carousel.less +133 -0
- package/src/carousel/Carousel.spec.tsx +221 -0
- package/src/carousel/Carousel.story.tsx +63 -0
- package/src/carousel/Carousel.tsx +345 -0
- package/src/carousel/index.ts +3 -0
- package/src/common/card/Card.tsx +51 -43
- package/src/dateInput/DateInput.rtl.spec.tsx +17 -0
- package/src/dateInput/DateInput.tsx +28 -22
- package/src/dateLookup/DateLookup.keyboardEvents.spec.js +2 -2
- package/src/dateLookup/DateLookup.rtl.spec.tsx +21 -0
- package/src/dateLookup/DateLookup.state.spec.js +5 -5
- package/src/dateLookup/DateLookup.tests.story.tsx +4 -11
- package/src/dateLookup/DateLookup.tsx +24 -9
- package/src/dateLookup/DateLookup.view.spec.js +11 -11
- package/src/field/Field.spec.tsx +95 -0
- package/src/field/Field.story.tsx +59 -0
- package/src/field/Field.tsx +70 -0
- package/src/index.ts +6 -0
- package/src/inputs/Input.tsx +5 -3
- package/src/inputs/SelectInput.spec.tsx +10 -0
- package/src/inputs/SelectInput.tsx +9 -4
- package/src/inputs/TextArea.tsx +6 -3
- package/src/inputs/_ButtonInput.tsx +2 -2
- package/src/inputs/_common.ts +2 -2
- package/src/inputs/contexts.tsx +45 -0
- package/src/label/Label.spec.tsx +26 -0
- package/src/label/Label.story.tsx +37 -0
- package/src/label/Label.tsx +20 -0
- package/src/main.css +135 -0
- package/src/main.less +1 -0
- package/src/phoneNumberInput/PhoneNumberInput.story.tsx +16 -22
- package/src/phoneNumberInput/PhoneNumberInput.tsx +14 -2
- package/src/promoCard/PromoCard.story.tsx +2 -2
- package/src/promoCard/PromoCard.tsx +30 -9
- package/src/radioGroup/RadioGroup.rtl.spec.tsx +14 -0
- package/src/radioGroup/RadioGroup.story.tsx +26 -0
- package/src/radioGroup/RadioGroup.tsx +4 -1
- package/src/switch/Switch.spec.tsx +10 -0
- package/src/switch/Switch.tsx +22 -13
- package/src/utilities/logActionRequired.js +1 -1
package/src/common/card/Card.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
|
-
import { MouseEvent, ReactNode, useRef } from 'react';
|
|
2
|
+
import { MouseEvent, type ReactNode, forwardRef, useRef } from 'react';
|
|
3
3
|
|
|
4
4
|
import { CloseButton } from '../closeButton';
|
|
5
5
|
import { stopPropagation } from '../domHelpers';
|
|
@@ -48,48 +48,56 @@ export interface CardProps {
|
|
|
48
48
|
* <p>Hello World!</p>
|
|
49
49
|
* </Card>
|
|
50
50
|
*/
|
|
51
|
-
const Card
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
className,
|
|
55
|
+
children = null,
|
|
56
|
+
id,
|
|
57
|
+
isDisabled = false,
|
|
58
|
+
isSmall = false,
|
|
59
|
+
onDismiss,
|
|
60
|
+
testId,
|
|
61
|
+
...props
|
|
62
|
+
},
|
|
63
|
+
ref,
|
|
64
|
+
) => {
|
|
65
|
+
const closeButtonReference = useRef(null);
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
return (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={classNames(
|
|
71
|
+
'np-Card',
|
|
72
|
+
{
|
|
73
|
+
'np-Card--small': !!isSmall,
|
|
74
|
+
'is-disabled': !!isDisabled,
|
|
75
|
+
},
|
|
76
|
+
className,
|
|
77
|
+
)}
|
|
78
|
+
id={id}
|
|
79
|
+
data-testid={testId}
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
{onDismiss && (
|
|
83
|
+
<CloseButton
|
|
84
|
+
ref={closeButtonReference}
|
|
85
|
+
className="np-Card-closeButton"
|
|
86
|
+
size={isSmall ? 'sm' : 'md'}
|
|
87
|
+
isDisabled={isDisabled}
|
|
88
|
+
testId="close-button"
|
|
89
|
+
onClick={(e) => {
|
|
90
|
+
stopPropagation(e);
|
|
91
|
+
onDismiss();
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
Card.displayName = 'Card';
|
|
94
102
|
|
|
95
103
|
export default Card;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Field } from '../field/Field';
|
|
2
|
+
import { mockMatchMedia, mockResizeObserver, render, screen } from '../test-utils';
|
|
3
|
+
import DateInput from './DateInput';
|
|
4
|
+
|
|
5
|
+
mockMatchMedia();
|
|
6
|
+
mockResizeObserver();
|
|
7
|
+
|
|
8
|
+
describe('DateInput', () => {
|
|
9
|
+
it('supports `Field` for labeling', () => {
|
|
10
|
+
render(
|
|
11
|
+
<Field label="Date of birth">
|
|
12
|
+
<DateInput onChange={() => {}} />
|
|
13
|
+
</Field>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getAllByRole('group')[0]).toHaveAccessibleName(/^Date of birth/);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -2,22 +2,23 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { useIntl } from 'react-intl';
|
|
4
4
|
|
|
5
|
-
import { Input, SelectInput,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { Input, SelectInput, SelectInputOptionContent, SelectInputProps } from '..';
|
|
6
|
+
import { DateMode, MonthFormat, Size, SizeLarge, SizeMedium, SizeSmall } from '../common';
|
|
7
|
+
import { MDY, YMD, getMonthNames, isDateValid, isMonthAndYearFormat } from '../common/dateUtils';
|
|
8
|
+
import { useInputAttributes } from '../inputs/contexts';
|
|
9
9
|
import messages from './DateInput.messages';
|
|
10
10
|
import { convertToLocalMidnight } from './utils';
|
|
11
11
|
|
|
12
12
|
export interface DateInputProps {
|
|
13
|
+
/** @deprecated Use `Field` wrapper or the `aria-labelledby` attribute instead. */
|
|
13
14
|
'aria-label'?: string;
|
|
14
15
|
'aria-labelledby'?: string;
|
|
15
16
|
disabled?: boolean;
|
|
16
17
|
size?: SizeSmall | SizeMedium | SizeLarge;
|
|
17
18
|
value?: Date | string;
|
|
18
19
|
onChange: (value: string | null) => void;
|
|
19
|
-
onFocus?: React.FocusEventHandler<
|
|
20
|
-
onBlur?: React.FocusEventHandler<
|
|
20
|
+
onFocus?: React.FocusEventHandler<HTMLDivElement>;
|
|
21
|
+
onBlur?: React.FocusEventHandler<HTMLDivElement>;
|
|
21
22
|
dayLabel?: string;
|
|
22
23
|
dayAutoComplete?: string;
|
|
23
24
|
monthLabel?: string;
|
|
@@ -35,7 +36,7 @@ export interface DateInputProps {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const DateInput = ({
|
|
38
|
-
'aria-labelledby':
|
|
39
|
+
'aria-labelledby': ariaLabelledByProp,
|
|
39
40
|
'aria-label': ariaLabel,
|
|
40
41
|
disabled = false,
|
|
41
42
|
size = Size.MEDIUM,
|
|
@@ -51,9 +52,13 @@ const DateInput = ({
|
|
|
51
52
|
onFocus,
|
|
52
53
|
onBlur,
|
|
53
54
|
placeholders,
|
|
54
|
-
id,
|
|
55
|
+
id: idProp,
|
|
55
56
|
selectProps = {},
|
|
56
57
|
}: DateInputProps) => {
|
|
58
|
+
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
59
|
+
const id = idProp ?? inputAttributes.id;
|
|
60
|
+
const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes['aria-labelledby'];
|
|
61
|
+
|
|
57
62
|
const { locale, formatMessage } = useIntl();
|
|
58
63
|
|
|
59
64
|
const getDateObject = (): Date | undefined => {
|
|
@@ -95,9 +100,9 @@ const DateInput = ({
|
|
|
95
100
|
);
|
|
96
101
|
const monthNames = getMonthNames(locale, monthFormat);
|
|
97
102
|
|
|
98
|
-
dayLabel
|
|
99
|
-
monthLabel
|
|
100
|
-
yearLabel
|
|
103
|
+
dayLabel ||= formatMessage(messages.dayLabel);
|
|
104
|
+
monthLabel ||= formatMessage(messages.monthLabel);
|
|
105
|
+
yearLabel ||= formatMessage(messages.yearLabel);
|
|
101
106
|
placeholders = {
|
|
102
107
|
day: placeholders?.day || formatMessage(messages.dayPlaceholder),
|
|
103
108
|
month: placeholders?.month || formatMessage(messages.monthLabel),
|
|
@@ -298,14 +303,15 @@ const DateInput = ({
|
|
|
298
303
|
return (
|
|
299
304
|
<div
|
|
300
305
|
className="tw-date"
|
|
306
|
+
{...inputAttributes}
|
|
301
307
|
id={id}
|
|
302
308
|
aria-labelledby={ariaLabelledBy}
|
|
303
309
|
aria-label={ariaLabel}
|
|
304
310
|
role="group" // Add role attribute to indicate container for interactive elements
|
|
305
|
-
onFocus={(event
|
|
311
|
+
onFocus={(event) =>
|
|
306
312
|
shouldPropagateOnFocus(event) ? onFocus && onFocus(event) : event.stopPropagation()
|
|
307
313
|
}
|
|
308
|
-
onBlur={(event
|
|
314
|
+
onBlur={(event) =>
|
|
309
315
|
shouldPropagateOnBlur(event) ? onBlur && onBlur(event) : event.stopPropagation()
|
|
310
316
|
}
|
|
311
317
|
>
|
|
@@ -328,7 +334,8 @@ const DateInput = ({
|
|
|
328
334
|
{getYear()}
|
|
329
335
|
</>
|
|
330
336
|
);
|
|
331
|
-
}
|
|
337
|
+
}
|
|
338
|
+
if (yearFirst) {
|
|
332
339
|
return (
|
|
333
340
|
<>
|
|
334
341
|
{getYear()}
|
|
@@ -336,15 +343,14 @@ const DateInput = ({
|
|
|
336
343
|
{getDay()}
|
|
337
344
|
</>
|
|
338
345
|
);
|
|
339
|
-
} else {
|
|
340
|
-
return (
|
|
341
|
-
<>
|
|
342
|
-
{getDay()}
|
|
343
|
-
{getMonth()}
|
|
344
|
-
{getYear()}
|
|
345
|
-
</>
|
|
346
|
-
);
|
|
347
346
|
}
|
|
347
|
+
return (
|
|
348
|
+
<>
|
|
349
|
+
{getDay()}
|
|
350
|
+
{getMonth()}
|
|
351
|
+
{getYear()}
|
|
352
|
+
</>
|
|
353
|
+
);
|
|
348
354
|
})()}
|
|
349
355
|
</div>
|
|
350
356
|
</div>
|
|
@@ -4,12 +4,12 @@ import { mount } from 'enzyme';
|
|
|
4
4
|
import { fakeKeyDownEventForKey } from '../common/fakeEvents';
|
|
5
5
|
import { mockMatchMedia } from '../test-utils';
|
|
6
6
|
|
|
7
|
-
import DateLookup from '
|
|
7
|
+
import { DateLookupWithoutInputAttributes as DateLookup } from './DateLookup';
|
|
8
8
|
|
|
9
9
|
mockMatchMedia();
|
|
10
10
|
|
|
11
11
|
const defaultLocale = 'en-GB';
|
|
12
|
-
const formatMessage = (id) =>
|
|
12
|
+
const formatMessage = (id) => String(id);
|
|
13
13
|
jest.mock('react-intl', () => ({
|
|
14
14
|
injectIntl: (Component) =>
|
|
15
15
|
function (props) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Field } from '../field/Field';
|
|
2
|
+
import { mockMatchMedia, mockResizeObserver, render, screen } from '../test-utils';
|
|
3
|
+
import DateLookup from './DateLookup';
|
|
4
|
+
|
|
5
|
+
mockMatchMedia();
|
|
6
|
+
mockResizeObserver();
|
|
7
|
+
|
|
8
|
+
const now = new Date();
|
|
9
|
+
|
|
10
|
+
describe('DateLookup', () => {
|
|
11
|
+
it('supports `Field` for labeling', () => {
|
|
12
|
+
render(
|
|
13
|
+
<Field label="Date of birth">
|
|
14
|
+
<DateLookup value={now} onChange={() => {}} />
|
|
15
|
+
</Field>,
|
|
16
|
+
);
|
|
17
|
+
expect(screen.getByLabelText('Date of birth')).toHaveTextContent(
|
|
18
|
+
now.getUTCFullYear().toString(),
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { shallow } from 'enzyme';
|
|
2
2
|
import { useIntl } from 'react-intl';
|
|
3
3
|
|
|
4
|
-
import DateLookup from '
|
|
4
|
+
import { DateLookupWithoutInputAttributes as DateLookup } from './DateLookup';
|
|
5
5
|
|
|
6
6
|
jest.mock('react-intl');
|
|
7
7
|
|
|
@@ -38,7 +38,7 @@ describe('DateLookup state', () => {
|
|
|
38
38
|
it('updates selectedDate on props value change', () => {
|
|
39
39
|
const props = { value: new Date(2018, 11, 27) };
|
|
40
40
|
const newState = DateLookup.getDerivedStateFromProps(props, defaultState);
|
|
41
|
-
expect(
|
|
41
|
+
expect(Number(newState.selectedDate)).toBe(Number(new Date(2018, 11, 27)));
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
it('sets date values to midnight', () => {
|
|
@@ -48,9 +48,9 @@ describe('DateLookup state', () => {
|
|
|
48
48
|
max: new Date(2018, 11, 28, 0, 30),
|
|
49
49
|
};
|
|
50
50
|
const newState = DateLookup.getDerivedStateFromProps(props, defaultState);
|
|
51
|
-
expect(
|
|
52
|
-
expect(
|
|
53
|
-
expect(
|
|
51
|
+
expect(Number(newState.selectedDate)).toBe(Number(new Date(2018, 11, 27, 0, 0)));
|
|
52
|
+
expect(Number(newState.min)).toBe(Number(new Date(2018, 10, 27, 0, 0)));
|
|
53
|
+
expect(Number(newState.max)).toBe(Number(new Date(2018, 11, 28, 0, 0)));
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it('calls onChange with min when it is < min', () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Story } from '@storybook/react';
|
|
2
2
|
import { expect, userEvent, within } from '@storybook/test';
|
|
3
|
-
import { PlayFunctionContext } from '@storybook/types';
|
|
4
3
|
import { useState } from 'react';
|
|
5
4
|
|
|
6
5
|
import DateLookup from './DateLookup';
|
|
@@ -10,7 +9,7 @@ export default {
|
|
|
10
9
|
title: 'Forms/DateLookup/Tests',
|
|
11
10
|
};
|
|
12
11
|
|
|
13
|
-
const Template: Story<DateLookup> = () => {
|
|
12
|
+
const Template: Story<typeof DateLookup> = () => {
|
|
14
13
|
const [value, setValue] = useState<Date | null>(new Date(1987, 0, 10, 12, 0, 0));
|
|
15
14
|
|
|
16
15
|
return (
|
|
@@ -27,10 +26,7 @@ const Template: Story<DateLookup> = () => {
|
|
|
27
26
|
|
|
28
27
|
export const ClearSpace = Template.bind({});
|
|
29
28
|
|
|
30
|
-
ClearSpace.play = async ({
|
|
31
|
-
canvasElement,
|
|
32
|
-
step,
|
|
33
|
-
}: PlayFunctionContext<ReactRenderer, DateLookup>) => {
|
|
29
|
+
ClearSpace.play = async ({ canvasElement, step }) => {
|
|
34
30
|
const canvas = within(canvasElement);
|
|
35
31
|
|
|
36
32
|
await step('space can activate clear button', async () => {
|
|
@@ -49,10 +45,7 @@ ClearSpace.play = async ({
|
|
|
49
45
|
|
|
50
46
|
export const ClearEnter = Template.bind({});
|
|
51
47
|
|
|
52
|
-
ClearEnter.play = async ({
|
|
53
|
-
canvasElement,
|
|
54
|
-
step,
|
|
55
|
-
}: PlayFunctionContext<ReactRenderer, DateLookup>) => {
|
|
48
|
+
ClearEnter.play = async ({ canvasElement, step }) => {
|
|
56
49
|
const canvas = within(canvasElement);
|
|
57
50
|
|
|
58
51
|
await step('enter can activate clear button', async () => {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from '../common';
|
|
13
13
|
import { isWithinRange, moveToWithinRange } from '../common/dateUtils';
|
|
14
14
|
import ResponsivePanel from '../common/responsivePanel';
|
|
15
|
-
|
|
15
|
+
import { WithInputAttributesProps, withInputAttributes } from '../inputs/contexts';
|
|
16
16
|
import DateTrigger from './dateTrigger';
|
|
17
17
|
import DayCalendar from './dayCalendar';
|
|
18
18
|
import { getStartOfDay } from './getStartOfDay';
|
|
@@ -36,6 +36,8 @@ export interface DateLookupProps {
|
|
|
36
36
|
onBlur?: () => void;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
type DateLookupPropsWithInputAttributes = DateLookupProps & Partial<WithInputAttributesProps>;
|
|
40
|
+
|
|
39
41
|
interface DateLookupState {
|
|
40
42
|
selectedDate: Date | null;
|
|
41
43
|
originalDate: Date | null;
|
|
@@ -48,9 +50,9 @@ interface DateLookupState {
|
|
|
48
50
|
isMobile: boolean;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
class DateLookup extends PureComponent<
|
|
52
|
-
declare props:
|
|
53
|
-
Required<Pick<
|
|
53
|
+
class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateLookupState> {
|
|
54
|
+
declare props: DateLookupPropsWithInputAttributes &
|
|
55
|
+
Required<Pick<DateLookupPropsWithInputAttributes, keyof typeof DateLookup.defaultProps>>;
|
|
54
56
|
|
|
55
57
|
static defaultProps = {
|
|
56
58
|
value: null,
|
|
@@ -62,7 +64,7 @@ class DateLookup extends PureComponent<DateLookupProps, DateLookupState> {
|
|
|
62
64
|
monthFormat: MonthFormat.LONG,
|
|
63
65
|
disabled: false,
|
|
64
66
|
clearable: false,
|
|
65
|
-
} satisfies Partial<
|
|
67
|
+
} satisfies Partial<DateLookupPropsWithInputAttributes>;
|
|
66
68
|
|
|
67
69
|
element = createRef<HTMLDivElement>();
|
|
68
70
|
dropdown = createRef<HTMLDivElement>();
|
|
@@ -106,7 +108,7 @@ class DateLookup extends PureComponent<DateLookupProps, DateLookupState> {
|
|
|
106
108
|
return null;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
componentDidUpdate(previousProps:
|
|
111
|
+
componentDidUpdate(previousProps: DateLookupPropsWithInputAttributes) {
|
|
110
112
|
if (this.props.value?.getTime() !== previousProps.value?.getTime() && this.state.open) {
|
|
111
113
|
this.focusOn('.active');
|
|
112
114
|
}
|
|
@@ -301,19 +303,25 @@ class DateLookup extends PureComponent<DateLookupProps, DateLookupState> {
|
|
|
301
303
|
const { selectedDate, open } = this.state;
|
|
302
304
|
|
|
303
305
|
const {
|
|
306
|
+
inputAttributes,
|
|
307
|
+
id: idProp,
|
|
308
|
+
'aria-labelledby': ariaLabelledByProp,
|
|
304
309
|
size,
|
|
305
310
|
placeholder,
|
|
306
311
|
label,
|
|
307
|
-
'aria-labelledby': ariaLabelledBy,
|
|
308
312
|
monthFormat,
|
|
309
313
|
disabled,
|
|
310
314
|
clearable,
|
|
311
315
|
value,
|
|
312
316
|
} = this.props;
|
|
317
|
+
const id = idProp ?? inputAttributes?.id;
|
|
318
|
+
const ariaLabelledBy = ariaLabelledByProp ?? inputAttributes?.['aria-labelledby'];
|
|
319
|
+
|
|
313
320
|
return (
|
|
314
321
|
<div
|
|
315
322
|
ref={this.element}
|
|
316
|
-
|
|
323
|
+
{...inputAttributes}
|
|
324
|
+
id={id}
|
|
317
325
|
aria-labelledby={ariaLabelledBy}
|
|
318
326
|
className="input-group"
|
|
319
327
|
onKeyDown={this.handleKeyDown}
|
|
@@ -342,4 +350,11 @@ class DateLookup extends PureComponent<DateLookupProps, DateLookupState> {
|
|
|
342
350
|
}
|
|
343
351
|
}
|
|
344
352
|
|
|
345
|
-
export
|
|
353
|
+
export const DateLookupWithoutInputAttributes = DateLookup;
|
|
354
|
+
|
|
355
|
+
export default withInputAttributes(
|
|
356
|
+
DateLookup as React.ComponentType<DateLookupPropsWithInputAttributes>,
|
|
357
|
+
{
|
|
358
|
+
nonLabelable: true,
|
|
359
|
+
},
|
|
360
|
+
);
|
|
@@ -7,7 +7,7 @@ import DayCalendar from './dayCalendar';
|
|
|
7
7
|
import MonthCalendar from './monthCalendar';
|
|
8
8
|
import YearCalendar from './yearCalendar';
|
|
9
9
|
|
|
10
|
-
import DateLookup from '
|
|
10
|
+
import { DateLookupWithoutInputAttributes as DateLookup } from './DateLookup';
|
|
11
11
|
|
|
12
12
|
mockMatchMedia();
|
|
13
13
|
|
|
@@ -51,7 +51,7 @@ describe('DateLookup view', () => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
it('passes props forward to open button', () => {
|
|
54
|
-
expect(
|
|
54
|
+
expect(Number(dateTrigger().prop('selectedDate'))).toBe(Number(date));
|
|
55
55
|
expect(dateTrigger().prop('size')).toBe('lg');
|
|
56
56
|
expect(dateTrigger().prop('placeholder')).toBe('Asd..');
|
|
57
57
|
expect(dateTrigger().prop('label')).toBe('Date..');
|
|
@@ -76,9 +76,9 @@ describe('DateLookup view', () => {
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
it('passes props forward to day calendar', () => {
|
|
79
|
-
expect(
|
|
80
|
-
expect(
|
|
81
|
-
expect(
|
|
79
|
+
expect(Number(dayCalendar().prop('selectedDate'))).toBe(Number(date));
|
|
80
|
+
expect(Number(dayCalendar().prop('min'))).toBe(Number(min));
|
|
81
|
+
expect(Number(dayCalendar().prop('max'))).toBe(Number(max));
|
|
82
82
|
expect(dayCalendar().prop('viewMonth')).toBe(11);
|
|
83
83
|
expect(dayCalendar().prop('viewYear')).toBe(2018);
|
|
84
84
|
expect(dayCalendar().prop('monthFormat')).toBe('long');
|
|
@@ -103,9 +103,9 @@ describe('DateLookup view', () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
it('passes props forward to month calendar', () => {
|
|
106
|
-
expect(
|
|
107
|
-
expect(
|
|
108
|
-
expect(
|
|
106
|
+
expect(Number(monthCalendar().prop('selectedDate'))).toBe(Number(date));
|
|
107
|
+
expect(Number(monthCalendar().prop('min'))).toBe(Number(min));
|
|
108
|
+
expect(Number(monthCalendar().prop('max'))).toBe(Number(max));
|
|
109
109
|
expect(monthCalendar().prop('viewYear')).toBe(2018);
|
|
110
110
|
expect(monthCalendar().prop('placeholder')).toBe('Asd..');
|
|
111
111
|
});
|
|
@@ -129,9 +129,9 @@ describe('DateLookup view', () => {
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
it('passes props forward to year calendar', () => {
|
|
132
|
-
expect(
|
|
133
|
-
expect(
|
|
134
|
-
expect(
|
|
132
|
+
expect(Number(yearCalendar().prop('selectedDate'))).toBe(Number(date));
|
|
133
|
+
expect(Number(yearCalendar().prop('min'))).toBe(Number(min));
|
|
134
|
+
expect(Number(yearCalendar().prop('max'))).toBe(Number(max));
|
|
135
135
|
expect(yearCalendar().prop('viewYear')).toBe(2018);
|
|
136
136
|
expect(yearCalendar().prop('placeholder')).toBe('Asd..');
|
|
137
137
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import Info from '../info/Info';
|
|
2
|
+
import { Input } from '../inputs/Input';
|
|
3
|
+
import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
|
|
4
|
+
|
|
5
|
+
import { Field } from './Field';
|
|
6
|
+
|
|
7
|
+
mockMatchMedia();
|
|
8
|
+
|
|
9
|
+
describe('Field', () => {
|
|
10
|
+
it('should render label', () => {
|
|
11
|
+
render(
|
|
12
|
+
<Field label="Phone number">
|
|
13
|
+
<Input />
|
|
14
|
+
</Field>,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(screen.getByLabelText('Phone number')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('aria-describedby');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should render help text if provided', () => {
|
|
22
|
+
render(
|
|
23
|
+
<Field label="Phone number" hint="This is help text">
|
|
24
|
+
<Input />
|
|
25
|
+
</Field>,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const textbox = screen.getByRole('textbox', { description: 'This is help text' });
|
|
29
|
+
expect(textbox).toBeInTheDocument();
|
|
30
|
+
expect(textbox).not.toBeInvalid();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render error text if provided', () => {
|
|
34
|
+
render(
|
|
35
|
+
<Field label="Phone number" error="This is error text">
|
|
36
|
+
<Input />
|
|
37
|
+
</Field>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const textbox = screen.getByRole('textbox', { description: 'This is error text' });
|
|
41
|
+
expect(textbox).toBeInTheDocument();
|
|
42
|
+
expect(textbox).toBeInvalid();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should prefer error text over help text if both are provided', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Field label="Phone number" error="This is error text" hint="This is help text">
|
|
48
|
+
<Input />
|
|
49
|
+
</Field>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByRole('textbox', { description: 'This is error text' })).toBeInTheDocument();
|
|
53
|
+
expect(screen.queryByText('This is help text')).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('avoids triggering button within label inadvertently', async () => {
|
|
57
|
+
const handleClick = jest.fn();
|
|
58
|
+
|
|
59
|
+
render(
|
|
60
|
+
<Field
|
|
61
|
+
label={
|
|
62
|
+
<span>
|
|
63
|
+
Phone number{' '}
|
|
64
|
+
<Info content="Further information explained in detail." onClick={handleClick} />
|
|
65
|
+
</span>
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
<Input />
|
|
69
|
+
</Field>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const button = screen.getByRole('button');
|
|
73
|
+
button.addEventListener('click', handleClick);
|
|
74
|
+
|
|
75
|
+
const label = screen.getByText('Phone number');
|
|
76
|
+
userEvent.click(label);
|
|
77
|
+
|
|
78
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('allows nesting-based label association', async () => {
|
|
82
|
+
const handleClick = jest.fn();
|
|
83
|
+
|
|
84
|
+
render(
|
|
85
|
+
<Field id={null} label="Phone number">
|
|
86
|
+
<Input onClick={handleClick} />
|
|
87
|
+
</Field>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(screen.getByRole('textbox')).not.toHaveAttribute('id');
|
|
91
|
+
|
|
92
|
+
const label = screen.getByText('Phone number');
|
|
93
|
+
userEvent.click(label);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { Input } from '../inputs/Input';
|
|
4
|
+
import { Field } from './Field';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
component: Field,
|
|
8
|
+
title: 'Field',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Basic = () => {
|
|
12
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
13
|
+
return (
|
|
14
|
+
<Field label="Phone number">
|
|
15
|
+
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
16
|
+
</Field>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const WithErrorMessage = () => {
|
|
21
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
22
|
+
return (
|
|
23
|
+
<Field label="Phone number" error="This is a required field">
|
|
24
|
+
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
25
|
+
</Field>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const WithHelp = () => {
|
|
30
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
31
|
+
return (
|
|
32
|
+
<Field label="Phone number" hint="This is a helpful message">
|
|
33
|
+
<Input value={value} onChange={({ target }) => setValue(target.value)} />
|
|
34
|
+
</Field>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithHelpAndErrorOnBlur = () => {
|
|
39
|
+
const [value, setValue] = useState<string | undefined>('This is some text');
|
|
40
|
+
const [error, setError] = useState<string | undefined>(undefined);
|
|
41
|
+
return (
|
|
42
|
+
<Field label="Phone number" hint="Please include country code" error={error}>
|
|
43
|
+
<Input
|
|
44
|
+
value={value}
|
|
45
|
+
onChange={({ target }) => {
|
|
46
|
+
setValue(target.value);
|
|
47
|
+
setError(undefined);
|
|
48
|
+
}}
|
|
49
|
+
onBlur={() => {
|
|
50
|
+
if (!value) {
|
|
51
|
+
setError('This is a required field');
|
|
52
|
+
} else {
|
|
53
|
+
setError(undefined);
|
|
54
|
+
}
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</Field>
|
|
58
|
+
);
|
|
59
|
+
};
|