@lumx/react 3.6.6 → 3.6.7-alpha.1
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/index.d.ts +15 -0
- package/index.js +100 -18
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/date-picker/DatePicker.test.tsx +3 -0
- package/src/components/date-picker/DatePicker.tsx +4 -7
- package/src/components/date-picker/DatePickerControlled.test.tsx +46 -2
- package/src/components/date-picker/DatePickerControlled.tsx +80 -5
- package/src/components/date-picker/DatePickerField.test.tsx +3 -0
- package/src/components/message/Message.stories.tsx +16 -0
- package/src/components/message/Message.test.tsx +25 -3
- package/src/components/message/Message.tsx +26 -3
- package/src/components/popover/Popover.tsx +6 -2
- package/src/stories/generated/Message/Demos.stories.tsx +1 -0
- package/src/utils/date/getYearDisplayName.test.ts +20 -0
- package/src/utils/date/getYearDisplayName.ts +12 -0
package/package.json
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"@juggle/resize-observer": "^3.2.0",
|
|
10
|
-
"@lumx/core": "^3.6.
|
|
11
|
-
"@lumx/icons": "^3.6.
|
|
10
|
+
"@lumx/core": "^3.6.7-alpha.1",
|
|
11
|
+
"@lumx/icons": "^3.6.7-alpha.1",
|
|
12
12
|
"@popperjs/core": "^2.5.4",
|
|
13
13
|
"body-scroll-lock": "^3.1.5",
|
|
14
14
|
"classnames": "^2.3.2",
|
|
@@ -112,5 +112,5 @@
|
|
|
112
112
|
"build:storybook": "storybook build"
|
|
113
113
|
},
|
|
114
114
|
"sideEffects": false,
|
|
115
|
-
"version": "3.6.
|
|
115
|
+
"version": "3.6.7-alpha.1"
|
|
116
116
|
}
|
|
@@ -10,6 +10,9 @@ import { CLASSNAME } from './constants';
|
|
|
10
10
|
|
|
11
11
|
const mockedDate = new Date(1487721600000);
|
|
12
12
|
Date.now = jest.fn(() => mockedDate.valueOf());
|
|
13
|
+
jest.mock('@lumx/react/utils/date/getYearDisplayName', () => ({
|
|
14
|
+
getYearDisplayName: () => 'année',
|
|
15
|
+
}));
|
|
13
16
|
|
|
14
17
|
const setup = (propsOverride: Partial<DatePickerProps> = {}) => {
|
|
15
18
|
const props: DatePickerProps = {
|
|
@@ -23,18 +23,14 @@ export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((pro
|
|
|
23
23
|
referenceDate = new Date();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const [
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const setNextMonth = () => setMonthOffset(monthOffset + 1);
|
|
26
|
+
const [selectedMonth, setSelectedMonth] = useState(referenceDate);
|
|
27
|
+
const setPrevMonth = () => setSelectedMonth((current) => addMonthResetDay(current, -1));
|
|
28
|
+
const setNextMonth = () => setSelectedMonth((current) => addMonthResetDay(current, +1));
|
|
30
29
|
|
|
31
30
|
const onDatePickerChange = (newDate?: Date) => {
|
|
32
31
|
onChange(newDate);
|
|
33
|
-
setMonthOffset(0);
|
|
34
32
|
};
|
|
35
33
|
|
|
36
|
-
const selectedMonth = addMonthResetDay(referenceDate, monthOffset);
|
|
37
|
-
|
|
38
34
|
return (
|
|
39
35
|
<DatePickerControlled
|
|
40
36
|
ref={ref}
|
|
@@ -46,6 +42,7 @@ export const DatePicker: Comp<DatePickerProps, HTMLDivElement> = forwardRef((pro
|
|
|
46
42
|
onNextMonthChange={setNextMonth}
|
|
47
43
|
selectedMonth={selectedMonth}
|
|
48
44
|
onChange={onDatePickerChange}
|
|
45
|
+
onMonthChange={setSelectedMonth}
|
|
49
46
|
/>
|
|
50
47
|
);
|
|
51
48
|
});
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
4
|
import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
5
5
|
import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
6
6
|
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
7
8
|
import { DatePickerControlled, DatePickerControlledProps } from './DatePickerControlled';
|
|
8
9
|
import { CLASSNAME } from './constants';
|
|
9
10
|
|
|
10
11
|
const mockedDate = new Date(1487721600000);
|
|
11
12
|
Date.now = jest.fn(() => mockedDate.valueOf());
|
|
13
|
+
jest.mock('@lumx/react/utils/date/getYearDisplayName', () => ({
|
|
14
|
+
getYearDisplayName: () => 'année',
|
|
15
|
+
}));
|
|
12
16
|
|
|
13
17
|
type SetupProps = Partial<DatePickerControlledProps>;
|
|
14
18
|
|
|
@@ -22,6 +26,7 @@ const setup = (propsOverride: SetupProps = {}) => {
|
|
|
22
26
|
value: mockedDate,
|
|
23
27
|
nextButtonProps: { label: 'Next month' },
|
|
24
28
|
previousButtonProps: { label: 'Previous month' },
|
|
29
|
+
onMonthChange: jest.fn(),
|
|
25
30
|
...propsOverride,
|
|
26
31
|
};
|
|
27
32
|
render(<DatePickerControlled {...props} />);
|
|
@@ -29,18 +34,57 @@ const setup = (propsOverride: SetupProps = {}) => {
|
|
|
29
34
|
return { props, datePickerControlled };
|
|
30
35
|
};
|
|
31
36
|
|
|
37
|
+
const queries = {
|
|
38
|
+
getYear: () =>
|
|
39
|
+
screen.getByRole('spinbutton', {
|
|
40
|
+
name: /année/i,
|
|
41
|
+
}),
|
|
42
|
+
getAccessibleMonthYear: (container: HTMLElement) => getByClassName(container, 'visually-hidden'),
|
|
43
|
+
};
|
|
44
|
+
|
|
32
45
|
describe(`<${DatePickerControlled.displayName}>`, () => {
|
|
33
46
|
it('should render', () => {
|
|
34
47
|
const { datePickerControlled } = setup();
|
|
35
48
|
expect(datePickerControlled).toBeInTheDocument();
|
|
36
49
|
|
|
37
50
|
const month = queryByClassName(datePickerControlled, `${CLASSNAME}__month`);
|
|
38
|
-
expect(month).toHaveTextContent('février
|
|
51
|
+
expect(month).toHaveTextContent('février');
|
|
52
|
+
|
|
53
|
+
expect(queries.getYear()).toBeInTheDocument();
|
|
54
|
+
expect(queries.getYear()).toHaveAttribute('value', '2017');
|
|
55
|
+
expect(queries.getYear()).toHaveAttribute('aria-label', 'année');
|
|
56
|
+
|
|
57
|
+
const toolbar = getByClassName(document.body, 'lumx-toolbar__label');
|
|
58
|
+
expect(queries.getAccessibleMonthYear(toolbar)).toBeInTheDocument();
|
|
59
|
+
expect(queries.getAccessibleMonthYear(toolbar)).toHaveTextContent('février 2017');
|
|
39
60
|
|
|
40
61
|
const selected = queryByClassName(datePickerControlled, `${CLASSNAME}__month-day--is-selected`);
|
|
41
62
|
expect(selected).toBe(screen.queryByRole('button', { name: /22 février 2017/i }));
|
|
42
63
|
});
|
|
43
64
|
|
|
65
|
+
it('should validate year when pressing Enter', async () => {
|
|
66
|
+
const { datePickerControlled, props } = setup();
|
|
67
|
+
expect(datePickerControlled).toBeInTheDocument();
|
|
68
|
+
|
|
69
|
+
expect(queries.getYear()).toHaveAttribute('value', '2017');
|
|
70
|
+
|
|
71
|
+
await userEvent.type(queries.getYear(), '{Backspace}6{Enter}'); // set year to 2016
|
|
72
|
+
await waitFor(() => expect(props.onMonthChange).toHaveBeenCalledTimes(1));
|
|
73
|
+
expect(props.onMonthChange).toHaveBeenCalledWith(new Date(1454284800000)); // 2017 - 12 months = 2016
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should validate year when on Blur', async () => {
|
|
77
|
+
const { datePickerControlled, props } = setup();
|
|
78
|
+
expect(datePickerControlled).toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
expect(queries.getYear()).toHaveAttribute('value', '2017');
|
|
81
|
+
|
|
82
|
+
await userEvent.type(queries.getYear(), '{Backspace}5'); // change value to 2015
|
|
83
|
+
await userEvent.tab(); // set year to 2015
|
|
84
|
+
await waitFor(() => expect(props.onMonthChange).toHaveBeenCalledTimes(1));
|
|
85
|
+
expect(props.onMonthChange).toHaveBeenCalledWith(new Date(1422748800000)); // 2017 - 24 months = 2015
|
|
86
|
+
});
|
|
87
|
+
|
|
44
88
|
commonTestsSuiteRTL(setup, {
|
|
45
89
|
baseClassName: CLASSNAME,
|
|
46
90
|
forwardRef: 'datePickerControlled',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { forwardRef } from 'react';
|
|
1
|
+
import React, { KeyboardEventHandler, forwardRef } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
|
-
import { DatePickerProps, Emphasis, IconButton, Toolbar } from '@lumx/react';
|
|
3
|
+
import { DatePickerProps, Emphasis, FlexBox, IconButton, Text, TextField, Toolbar } from '@lumx/react';
|
|
4
4
|
import { mdiChevronLeft, mdiChevronRight } from '@lumx/icons';
|
|
5
5
|
import { Comp } from '@lumx/react/utils/type';
|
|
6
6
|
import { getMonthCalendar } from '@lumx/react/utils/date/getMonthCalendar';
|
|
@@ -9,6 +9,9 @@ import { getCurrentLocale } from '@lumx/react/utils/locale/getCurrentLocale';
|
|
|
9
9
|
import { parseLocale } from '@lumx/react/utils/locale/parseLocale';
|
|
10
10
|
import { Locale } from '@lumx/react/utils/locale/types';
|
|
11
11
|
import { usePreviousValue } from '@lumx/react/hooks/usePreviousValue';
|
|
12
|
+
import { getYearDisplayName } from '@lumx/react/utils/date/getYearDisplayName';
|
|
13
|
+
import { onEnterPressed } from '@lumx/react/utils/event';
|
|
14
|
+
import { addMonthResetDay } from '@lumx/react/utils/date/addMonthResetDay';
|
|
12
15
|
import { CLASSNAME } from './constants';
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -21,6 +24,8 @@ export interface DatePickerControlledProps extends DatePickerProps {
|
|
|
21
24
|
onPrevMonthChange(): void;
|
|
22
25
|
/** On next month change callback. */
|
|
23
26
|
onNextMonthChange(): void;
|
|
27
|
+
/** On month/year change callback. */
|
|
28
|
+
onMonthChange?: (newMonth: Date) => void;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/**
|
|
@@ -48,12 +53,43 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
|
|
|
48
53
|
selectedMonth,
|
|
49
54
|
todayOrSelectedDateRef,
|
|
50
55
|
value,
|
|
56
|
+
onMonthChange,
|
|
51
57
|
} = props;
|
|
52
58
|
const { weeks, weekDays } = React.useMemo(() => {
|
|
53
59
|
const localeObj = parseLocale(locale) as Locale;
|
|
54
60
|
return getMonthCalendar(localeObj, selectedMonth, minDate, maxDate);
|
|
55
61
|
}, [locale, minDate, maxDate, selectedMonth]);
|
|
56
62
|
|
|
63
|
+
const selectedYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric' }).slice(0, 4);
|
|
64
|
+
const [textFieldYearValue, setTextFieldYearValue] = React.useState(selectedYear);
|
|
65
|
+
const isYearValid = Number(textFieldYearValue) > 0 && Number(textFieldYearValue) <= 9999;
|
|
66
|
+
|
|
67
|
+
// Updates month offset when validating year. Adds or removes 12 months per year when updating year value.
|
|
68
|
+
const updateMonthOffset = React.useCallback(() => {
|
|
69
|
+
if (isYearValid) {
|
|
70
|
+
const yearNumber = selectedMonth.getFullYear();
|
|
71
|
+
const offset = (Number(textFieldYearValue) - yearNumber) * 12;
|
|
72
|
+
if (onMonthChange) {
|
|
73
|
+
onMonthChange(addMonthResetDay(selectedMonth, offset));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [isYearValid, selectedMonth, textFieldYearValue, onMonthChange]);
|
|
77
|
+
|
|
78
|
+
const monthYear = selectedMonth.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
|
|
79
|
+
|
|
80
|
+
// Year can only be validatd by pressing Enter key or on Blur. The below handles the press Enter key case
|
|
81
|
+
const handleKeyPress: KeyboardEventHandler = React.useMemo(() => onEnterPressed(updateMonthOffset), [
|
|
82
|
+
updateMonthOffset,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// Required to update year in the TextField when the user changes year by using prev next month arrows
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (Number(textFieldYearValue) !== selectedMonth.getFullYear()) {
|
|
88
|
+
setTextFieldYearValue(selectedMonth.getFullYear().toString());
|
|
89
|
+
}
|
|
90
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
+
}, [selectedMonth]);
|
|
92
|
+
|
|
57
93
|
const prevSelectedMonth = usePreviousValue(selectedMonth);
|
|
58
94
|
const monthHasChanged = prevSelectedMonth && !isSameDay(selectedMonth, prevSelectedMonth);
|
|
59
95
|
|
|
@@ -63,6 +99,8 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
|
|
|
63
99
|
if (monthHasChanged) setLabelAriaLive('polite');
|
|
64
100
|
}, [monthHasChanged]);
|
|
65
101
|
|
|
102
|
+
const label = getYearDisplayName(locale);
|
|
103
|
+
|
|
66
104
|
return (
|
|
67
105
|
<div ref={ref} className={`${CLASSNAME}`}>
|
|
68
106
|
<Toolbar
|
|
@@ -84,9 +122,46 @@ export const DatePickerControlled: Comp<DatePickerControlledProps, HTMLDivElemen
|
|
|
84
122
|
/>
|
|
85
123
|
}
|
|
86
124
|
label={
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
|
|
125
|
+
<>
|
|
126
|
+
<span aria-live={labelAriaLive} className={onMonthChange ? 'visually-hidden' : ''} dir="auto">
|
|
127
|
+
{monthYear}
|
|
128
|
+
</span>
|
|
129
|
+
{onMonthChange && (
|
|
130
|
+
<FlexBox
|
|
131
|
+
className={`${CLASSNAME}__month`}
|
|
132
|
+
orientation="horizontal"
|
|
133
|
+
hAlign="center"
|
|
134
|
+
gap="regular"
|
|
135
|
+
vAlign="center"
|
|
136
|
+
dir="auto"
|
|
137
|
+
>
|
|
138
|
+
{RegExp(`(.*)(${selectedYear})(.*)`)
|
|
139
|
+
.exec(monthYear)
|
|
140
|
+
?.slice(1)
|
|
141
|
+
.filter((part) => part !== '')
|
|
142
|
+
.map((part) =>
|
|
143
|
+
part === selectedYear ? (
|
|
144
|
+
<TextField
|
|
145
|
+
value={textFieldYearValue}
|
|
146
|
+
aria-label={label}
|
|
147
|
+
onChange={setTextFieldYearValue}
|
|
148
|
+
type="number"
|
|
149
|
+
max={9999}
|
|
150
|
+
min={0}
|
|
151
|
+
onBlur={updateMonthOffset}
|
|
152
|
+
onKeyPress={handleKeyPress}
|
|
153
|
+
key="year"
|
|
154
|
+
className={`${CLASSNAME}__year`}
|
|
155
|
+
/>
|
|
156
|
+
) : (
|
|
157
|
+
<Text as="p" key={part}>
|
|
158
|
+
{part}
|
|
159
|
+
</Text>
|
|
160
|
+
),
|
|
161
|
+
)}
|
|
162
|
+
</FlexBox>
|
|
163
|
+
)}
|
|
164
|
+
</>
|
|
90
165
|
}
|
|
91
166
|
/>
|
|
92
167
|
<div className={`${CLASSNAME}__calendar`}>
|
|
@@ -11,6 +11,9 @@ import { CLASSNAME } from './constants';
|
|
|
11
11
|
|
|
12
12
|
const mockedDate = new Date(1487721600000);
|
|
13
13
|
Date.now = jest.fn(() => mockedDate.valueOf());
|
|
14
|
+
jest.mock('@lumx/react/utils/date/getYearDisplayName', () => ({
|
|
15
|
+
getYearDisplayName: () => 'année',
|
|
16
|
+
}));
|
|
14
17
|
|
|
15
18
|
const setup = (propsOverride: Partial<DatePickerFieldProps> = {}) => {
|
|
16
19
|
const props: DatePickerFieldProps = {
|
|
@@ -5,6 +5,7 @@ import { loremIpsum } from '@lumx/react/stories/utils/lorem';
|
|
|
5
5
|
import { iconArgType } from '@lumx/react/stories/controls/icons';
|
|
6
6
|
import { withCombinations } from '@lumx/react/stories/decorators/withCombinations';
|
|
7
7
|
import { withUndefined } from '@lumx/react/stories/controls/withUndefined';
|
|
8
|
+
import { withNestedProps } from '../../stories/decorators/withNestedProps';
|
|
8
9
|
|
|
9
10
|
export default {
|
|
10
11
|
title: 'LumX components/message/Message',
|
|
@@ -54,3 +55,18 @@ export const CustomIcon = {
|
|
|
54
55
|
icon: mdiDelete,
|
|
55
56
|
},
|
|
56
57
|
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* With close button (has background and kind info)
|
|
61
|
+
*/
|
|
62
|
+
export const ClosableMessage = {
|
|
63
|
+
args: {
|
|
64
|
+
'closeButtonProps.label': 'Close',
|
|
65
|
+
hasBackground: true,
|
|
66
|
+
kind: 'info',
|
|
67
|
+
},
|
|
68
|
+
argTypes: {
|
|
69
|
+
'closeButtonProps.onClick': { action: true },
|
|
70
|
+
},
|
|
71
|
+
decorators: [withNestedProps()],
|
|
72
|
+
};
|
|
@@ -2,9 +2,11 @@ import { commonTestsSuiteRTL } from '@lumx/react/testing/utils';
|
|
|
2
2
|
import { Kind } from '@lumx/react';
|
|
3
3
|
|
|
4
4
|
import React from 'react';
|
|
5
|
-
import { render } from '@testing-library/react';
|
|
5
|
+
import { queryByRole, render } from '@testing-library/react';
|
|
6
6
|
import { getByClassName, queryByClassName } from '@lumx/react/testing/utils/queries';
|
|
7
7
|
import { mdiAbTesting } from '@lumx/icons';
|
|
8
|
+
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
8
10
|
import { Message, MessageProps } from './Message';
|
|
9
11
|
|
|
10
12
|
const CLASSNAME = Message.className as string;
|
|
@@ -19,7 +21,8 @@ const setup = (propsOverride: SetupProps = {}) => {
|
|
|
19
21
|
render(<Message {...props} />);
|
|
20
22
|
const message = getByClassName(document.body, CLASSNAME);
|
|
21
23
|
const icon = queryByClassName(message, `${CLASSNAME}__icon`);
|
|
22
|
-
|
|
24
|
+
const closeButton = queryByRole(message, 'button', { name: props.closeButtonProps?.label });
|
|
25
|
+
return { message, icon, closeButton, props };
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
describe(`<${Message.displayName}>`, () => {
|
|
@@ -44,9 +47,28 @@ describe(`<${Message.displayName}>`, () => {
|
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
it.each(Object.values(Kind))('should render kind %s', (kind) => {
|
|
47
|
-
const { message, icon } = setup({ kind });
|
|
50
|
+
const { message, icon, closeButton } = setup({ kind });
|
|
48
51
|
expect(message.className).toEqual(expect.stringMatching(/\blumx-message--color-\w+\b/));
|
|
49
52
|
expect(icon).toBeInTheDocument();
|
|
53
|
+
expect(closeButton).not.toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should render close button', async () => {
|
|
57
|
+
const onClick = jest.fn();
|
|
58
|
+
const { closeButton } = setup({
|
|
59
|
+
hasBackground: true,
|
|
60
|
+
kind: 'info',
|
|
61
|
+
closeButtonProps: {
|
|
62
|
+
label: 'Close',
|
|
63
|
+
onClick,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(closeButton).toBeInTheDocument();
|
|
68
|
+
|
|
69
|
+
await userEvent.click(closeButton as HTMLElement);
|
|
70
|
+
|
|
71
|
+
expect(onClick).toHaveBeenCalled();
|
|
50
72
|
});
|
|
51
73
|
});
|
|
52
74
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mdiAlert, mdiAlertCircle, mdiCheckCircle, mdiInformation } from '@lumx/icons';
|
|
2
|
-
import { ColorPalette, Icon, Kind, Size } from '@lumx/react';
|
|
1
|
+
import { mdiAlert, mdiAlertCircle, mdiCheckCircle, mdiClose, mdiInformation } from '@lumx/icons';
|
|
2
|
+
import { ColorPalette, Emphasis, Icon, IconButton, Kind, Size } from '@lumx/react';
|
|
3
3
|
import { Comp, GenericProps } from '@lumx/react/utils/type';
|
|
4
4
|
import { getRootClassName, handleBasicClasses } from '@lumx/react/utils/className';
|
|
5
5
|
import classNames from 'classnames';
|
|
@@ -17,6 +17,17 @@ export interface MessageProps extends GenericProps {
|
|
|
17
17
|
kind?: Kind;
|
|
18
18
|
/** Message custom icon SVG path. */
|
|
19
19
|
icon?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Displays a close button.
|
|
22
|
+
*
|
|
23
|
+
* NB: only available if `kind === 'info' && hasBackground === true`
|
|
24
|
+
*/
|
|
25
|
+
closeButtonProps?: {
|
|
26
|
+
/** The callback called when the button is clicked */
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
/** The label of the close button. */
|
|
29
|
+
label: string;
|
|
30
|
+
};
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
@@ -47,8 +58,10 @@ const CONFIG = {
|
|
|
47
58
|
* @return React element.
|
|
48
59
|
*/
|
|
49
60
|
export const Message: Comp<MessageProps, HTMLDivElement> = forwardRef((props, ref) => {
|
|
50
|
-
const { children, className, hasBackground, kind, icon: customIcon, ...forwardedProps } = props;
|
|
61
|
+
const { children, className, hasBackground, kind, icon: customIcon, closeButtonProps, ...forwardedProps } = props;
|
|
51
62
|
const { color, icon } = CONFIG[kind as Kind] || {};
|
|
63
|
+
const { onClick, label: closeButtonLabel } = closeButtonProps || {};
|
|
64
|
+
const isCloseButtonDisplayed = hasBackground && kind === 'info' && onClick && closeButtonLabel;
|
|
52
65
|
|
|
53
66
|
return (
|
|
54
67
|
<div
|
|
@@ -67,8 +80,18 @@ export const Message: Comp<MessageProps, HTMLDivElement> = forwardRef((props, re
|
|
|
67
80
|
<Icon className={`${CLASSNAME}__icon`} icon={customIcon || icon} size={Size.xs} color={color} />
|
|
68
81
|
)}
|
|
69
82
|
<div className={`${CLASSNAME}__text`}>{children}</div>
|
|
83
|
+
{isCloseButtonDisplayed && (
|
|
84
|
+
<IconButton
|
|
85
|
+
className={`${CLASSNAME}__close-button`}
|
|
86
|
+
icon={mdiClose}
|
|
87
|
+
onClick={onClick}
|
|
88
|
+
label={closeButtonLabel}
|
|
89
|
+
emphasis={Emphasis.low}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
70
92
|
</div>
|
|
71
93
|
);
|
|
72
94
|
});
|
|
95
|
+
|
|
73
96
|
Message.displayName = COMPONENT_NAME;
|
|
74
97
|
Message.className = CLASSNAME;
|
|
@@ -60,6 +60,8 @@ export interface PopoverProps extends GenericProps, HasTheme {
|
|
|
60
60
|
placement?: Placement;
|
|
61
61
|
/** Whether the popover should be rendered into a DOM node that exists outside the DOM hierarchy of the parent component. */
|
|
62
62
|
usePortal?: boolean;
|
|
63
|
+
/** The element in which the focus trap should be set. Default to popover. */
|
|
64
|
+
focusTrapZone?: RefObject<HTMLElement>;
|
|
63
65
|
/** Z-axis position. */
|
|
64
66
|
zIndex?: number;
|
|
65
67
|
/** On close callback (on click away or Escape pressed). */
|
|
@@ -115,6 +117,7 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
115
117
|
boundaryRef,
|
|
116
118
|
fitToAnchorWidth,
|
|
117
119
|
fitWithinViewportHeight,
|
|
120
|
+
focusTrapZone,
|
|
118
121
|
offset,
|
|
119
122
|
placement,
|
|
120
123
|
style,
|
|
@@ -146,12 +149,13 @@ const _InnerPopover: Comp<PopoverProps, HTMLDivElement> = forwardRef((props, ref
|
|
|
146
149
|
});
|
|
147
150
|
|
|
148
151
|
const unmountSentinel = useRestoreFocusOnClose({ focusAnchorOnClose, anchorRef, parentElement }, popperElement);
|
|
149
|
-
|
|
152
|
+
const focusZoneElement = focusTrapZone?.current || popoverRef?.current;
|
|
153
|
+
console.log('focusZone', focusZoneElement);
|
|
150
154
|
useCallbackOnEscape(onClose, isOpen && closeOnEscape);
|
|
151
155
|
|
|
152
156
|
/** Only set focus within if the focus trap is disabled as they interfere with one another. */
|
|
153
157
|
useFocus(focusElement?.current, !withFocusTrap && isOpen && isPositioned);
|
|
154
|
-
useFocusTrap(withFocusTrap && isOpen &&
|
|
158
|
+
useFocusTrap(withFocusTrap && isOpen && focusZoneElement, focusElement?.current);
|
|
155
159
|
|
|
156
160
|
const clickAwayRefs = useRef([popoverRef, anchorRef]);
|
|
157
161
|
const mergedRefs = useMergeRefs<HTMLDivElement>(setPopperElement, ref, popoverRef);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getYearDisplayName } from './getYearDisplayName';
|
|
2
|
+
|
|
3
|
+
describe(getYearDisplayName, () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
jest.spyOn(Intl, 'DisplayNames').mockImplementation(() => ({
|
|
8
|
+
resolvedOptions: () => ({
|
|
9
|
+
fallback: 'code',
|
|
10
|
+
locale: 'fr',
|
|
11
|
+
style: 'short',
|
|
12
|
+
type: 'dateTimeField',
|
|
13
|
+
}),
|
|
14
|
+
of: () => 'année',
|
|
15
|
+
}));
|
|
16
|
+
});
|
|
17
|
+
it('should return a label', () => {
|
|
18
|
+
expect(getYearDisplayName('fr-FR')).toEqual('année');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const getYearDisplayName = (locale: string) => {
|
|
2
|
+
let label: string | undefined;
|
|
3
|
+
try {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
const displayNames = new Intl.DisplayNames(locale, { type: 'dateTimeField' });
|
|
7
|
+
label = displayNames.of('year');
|
|
8
|
+
} catch (error) {
|
|
9
|
+
label = '';
|
|
10
|
+
}
|
|
11
|
+
return label;
|
|
12
|
+
};
|