@openedx/paragon 23.4.4 → 23.5.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/dist/DataTable/hooks.js +48 -2
- package/dist/DataTable/hooks.js.map +1 -1
- package/dist/DataTable/index.js +18 -9
- package/dist/DataTable/index.js.map +1 -1
- package/dist/DataTable/selection/ControlledSelectionStatus.js +7 -17
- package/dist/DataTable/selection/ControlledSelectionStatus.js.map +1 -1
- package/dist/DataTable/selection/data/actions.js +5 -0
- package/dist/DataTable/selection/data/reducer.js +12 -1
- package/dist/Form/_index.scss +4 -1
- package/dist/Modal/AlertModal.js.map +1 -1
- package/dist/Modal/ModalDialog.d.ts +1 -1
- package/dist/Modal/ModalDialog.js +1 -1
- package/dist/Modal/ModalDialog.js.map +1 -1
- package/dist/Modal/_ModalDialog.scss +1 -1
- package/dist/Toast/ToastContainer.d.ts +6 -0
- package/dist/Toast/ToastContainer.js +19 -29
- package/dist/Toast/ToastContainer.js.map +1 -1
- package/dist/Toast/index.d.ts +59 -0
- package/dist/Toast/index.js.map +1 -1
- package/dist/Toast/index.scss +2 -2
- package/dist/core.css +6 -4
- package/dist/core.css.map +1 -1
- package/dist/core.min.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23 -23
- package/dist/theme-urls.json +6 -6
- package/lib/build-tokens.js +1 -0
- package/package.json +1 -1
- package/src/DataTable/README.md +111 -78
- package/src/DataTable/hooks.jsx +55 -2
- package/src/DataTable/index.jsx +28 -16
- package/src/DataTable/selection/ControlledSelectionStatus.jsx +8 -23
- package/src/DataTable/selection/data/actions.js +5 -0
- package/src/DataTable/selection/data/reducer.js +12 -1
- package/src/DataTable/selection/tests/ControlledSelectionStatus.test.jsx +4 -23
- package/src/DataTable/selection/tests/reducer.test.js +4 -0
- package/src/DataTable/tests/DataTable.test.jsx +99 -3
- package/src/Form/_index.scss +4 -1
- package/src/Modal/AlertModal.jsx +5 -5
- package/src/Modal/ModalDialog.tsx +2 -2
- package/src/Modal/_ModalDialog.scss +1 -1
- package/src/Modal/alert-modal.mdx +4 -0
- package/src/Modal/fullscreen-modal.mdx +1 -0
- package/src/Modal/marketing-modal.mdx +1 -0
- package/src/Modal/modal-dialog.mdx +2 -2
- package/src/Modal/standard-modal.mdx +1 -0
- package/src/Modal/tests/AlertModal.test.jsx +4 -0
- package/src/Modal/tests/ModalDialog.test.tsx +3 -2
- package/src/Toast/README.md +4 -4
- package/src/Toast/{Toast.test.jsx → Toast.test.tsx} +23 -13
- package/src/Toast/ToastContainer.tsx +30 -0
- package/src/Toast/index.scss +2 -2
- package/src/Toast/{index.jsx → index.tsx} +27 -6
- package/src/index.d.ts +1 -1
- package/src/index.js +23 -23
- package/src/Toast/ToastContainer.jsx +0 -40
|
@@ -4,7 +4,10 @@ import { IntlProvider } from 'react-intl';
|
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
5
|
|
|
6
6
|
import ControlledSelectionStatus from '../ControlledSelectionStatus';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearSelectionAction,
|
|
9
|
+
setSelectAllRowsAllPagesAction,
|
|
10
|
+
} from '../data/actions';
|
|
8
11
|
import DataTableContext from '../../DataTableContext';
|
|
9
12
|
import {
|
|
10
13
|
SELECT_ALL_TEST_ID,
|
|
@@ -88,28 +91,6 @@ describe('<ControlledSelectionStatus />', () => {
|
|
|
88
91
|
const selectAllButton = screen.queryByTestId(SELECT_ALL_TEST_ID);
|
|
89
92
|
expect(selectAllButton).not.toBeInTheDocument();
|
|
90
93
|
});
|
|
91
|
-
|
|
92
|
-
it('selects any unselected page rows', () => {
|
|
93
|
-
const selectedRows = Array(instance.itemCount).map((item, index) => ({ id: index + 1 }));
|
|
94
|
-
const dispatchSpy = jest.fn();
|
|
95
|
-
render(
|
|
96
|
-
<ControlledSelectionStatusWrapper
|
|
97
|
-
value={{
|
|
98
|
-
...instance,
|
|
99
|
-
controlledTableSelections: [
|
|
100
|
-
{
|
|
101
|
-
selectedRows,
|
|
102
|
-
isEntireTableSelected: true,
|
|
103
|
-
},
|
|
104
|
-
dispatchSpy,
|
|
105
|
-
],
|
|
106
|
-
}}
|
|
107
|
-
/>,
|
|
108
|
-
);
|
|
109
|
-
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
|
110
|
-
const action = setSelectedRowsAction(instance.page, instance.itemCount);
|
|
111
|
-
expect(dispatchSpy).toHaveBeenCalledWith(action);
|
|
112
|
-
});
|
|
113
94
|
});
|
|
114
95
|
|
|
115
96
|
describe('individual rows selected', () => {
|
|
@@ -25,6 +25,7 @@ describe('DataTable selections reducer', () => {
|
|
|
25
25
|
const action = setSelectedRowsAction(rows, itemCount);
|
|
26
26
|
const updatedState = selectionsReducer(defaultInitialState, action);
|
|
27
27
|
expect(updatedState).toEqual({
|
|
28
|
+
...defaultInitialState,
|
|
28
29
|
isEntireTableSelected: true,
|
|
29
30
|
selectedRows: rows,
|
|
30
31
|
});
|
|
@@ -38,6 +39,7 @@ describe('DataTable selections reducer', () => {
|
|
|
38
39
|
const updatedState = selectionsReducer(initialState, action);
|
|
39
40
|
expect(updatedState).toEqual({
|
|
40
41
|
isEntireTableSelected: true,
|
|
42
|
+
isSelectAllEnabled: true,
|
|
41
43
|
selectedRows: initialState.selectedRows,
|
|
42
44
|
});
|
|
43
45
|
});
|
|
@@ -70,6 +72,7 @@ describe('DataTable selections reducer', () => {
|
|
|
70
72
|
const action = addSelectedRowAction(row, itemCount);
|
|
71
73
|
const updatedState = selectionsReducer(defaultInitialState, action);
|
|
72
74
|
expect(updatedState).toEqual({
|
|
75
|
+
...defaultInitialState,
|
|
73
76
|
selectedRows: [row],
|
|
74
77
|
isEntireTableSelected: true,
|
|
75
78
|
});
|
|
@@ -88,6 +91,7 @@ describe('DataTable selections reducer', () => {
|
|
|
88
91
|
const rows = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
89
92
|
const initialState = {
|
|
90
93
|
...defaultInitialState,
|
|
94
|
+
isSelectAllEnabled: false,
|
|
91
95
|
selectedRows: rows,
|
|
92
96
|
};
|
|
93
97
|
const action = clearPageSelectionAction([1, 2]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useContext } from 'react';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import * as reactTable from 'react-table';
|
|
5
5
|
import { IntlProvider } from 'react-intl';
|
|
@@ -212,7 +212,6 @@ describe('<DataTable />', () => {
|
|
|
212
212
|
pageCount: 3,
|
|
213
213
|
fetchData: jest.fn(),
|
|
214
214
|
};
|
|
215
|
-
|
|
216
215
|
render(<DataTableWrapper {...propsWithSelection} />);
|
|
217
216
|
const filtersButton = screen.getByRole('button', { name: 'Filters' });
|
|
218
217
|
|
|
@@ -228,6 +227,10 @@ describe('<DataTable />', () => {
|
|
|
228
227
|
// A filtered array is returned from the backend,
|
|
229
228
|
// and the element counter displays its length.
|
|
230
229
|
expect(selectAllButton).toHaveTextContent('Select all 7');
|
|
230
|
+
|
|
231
|
+
await userEvent.click(selectAllButton);
|
|
232
|
+
|
|
233
|
+
expect(screen.getByText('All 7 selected')).toBeInTheDocument();
|
|
231
234
|
});
|
|
232
235
|
|
|
233
236
|
describe('[legacy] controlled table selections', () => {
|
|
@@ -242,7 +245,7 @@ describe('<DataTable />', () => {
|
|
|
242
245
|
|
|
243
246
|
const contextValue = JSON.parse(contextDiv.getAttribute('data-contextvalue'));
|
|
244
247
|
expect(contextValue.controlledTableSelections).toEqual([
|
|
245
|
-
{ selectedRows: [], isEntireTableSelected: false },
|
|
248
|
+
{ selectedRows: [], isEntireTableSelected: false, isSelectAllEnabled: false },
|
|
246
249
|
null,
|
|
247
250
|
]);
|
|
248
251
|
});
|
|
@@ -312,5 +315,98 @@ describe('<DataTable />', () => {
|
|
|
312
315
|
expect(mockOnSelectedRowsChange).toHaveBeenCalledTimes(1);
|
|
313
316
|
expect(mockOnSelectedRowsChange).toHaveBeenCalledWith({});
|
|
314
317
|
});
|
|
318
|
+
it('Selects all rows across all pages with ControlledSelectionStatus component', async () => {
|
|
319
|
+
const selectColumn = {
|
|
320
|
+
id: 'selection',
|
|
321
|
+
Header: DataTable.ControlledSelectHeader,
|
|
322
|
+
Cell: DataTable.ControlledSelect,
|
|
323
|
+
disableSortBy: true,
|
|
324
|
+
};
|
|
325
|
+
const propsWithSelection = {
|
|
326
|
+
...props,
|
|
327
|
+
isPaginated: true,
|
|
328
|
+
isSelectable: true,
|
|
329
|
+
manualSelectColumn: selectColumn,
|
|
330
|
+
SelectionStatusComponent: DataTable.ControlledSelectionStatus,
|
|
331
|
+
initialState: {
|
|
332
|
+
pageIndex: 0,
|
|
333
|
+
pageSize: 2,
|
|
334
|
+
selectedRowIds: {
|
|
335
|
+
1: true,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
render(<DataTableWrapper {...propsWithSelection} />);
|
|
340
|
+
const selectAllButton = screen.getByTestId('test_selection_state_select_all_button');
|
|
341
|
+
|
|
342
|
+
await userEvent.click(selectAllButton);
|
|
343
|
+
|
|
344
|
+
expect(screen.getByText('All 7 selected')).toBeInTheDocument();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('Select all item rows with ControlledSelectionStatus Component', async () => {
|
|
348
|
+
const selectColumn = {
|
|
349
|
+
id: 'selection',
|
|
350
|
+
Header: DataTable.ControlledSelectHeader,
|
|
351
|
+
Cell: DataTable.ControlledSelect,
|
|
352
|
+
disableSortBy: true,
|
|
353
|
+
};
|
|
354
|
+
const propsWithSelection = {
|
|
355
|
+
...props,
|
|
356
|
+
isPaginated: true,
|
|
357
|
+
isSelectable: true,
|
|
358
|
+
manualSelectColumn: selectColumn,
|
|
359
|
+
SelectionStatusComponent: DataTable.ControlledSelectionStatus,
|
|
360
|
+
initialState: {
|
|
361
|
+
pageIndex: 0,
|
|
362
|
+
pageSize: 10,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
render(<DataTableWrapper {...propsWithSelection} />);
|
|
366
|
+
const selectAllRowsButton = screen.getByTitle('Toggle All Current Page Rows Selected');
|
|
367
|
+
|
|
368
|
+
await userEvent.click(selectAllRowsButton);
|
|
369
|
+
|
|
370
|
+
expect(screen.getByText('All 7 selected')).toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('Select all item rows individually with ControlledSelectionStatus Component', async () => {
|
|
374
|
+
const selectColumn = {
|
|
375
|
+
id: 'selection',
|
|
376
|
+
Header: DataTable.ControlledSelectHeader,
|
|
377
|
+
Cell: DataTable.ControlledSelect,
|
|
378
|
+
disableSortBy: true,
|
|
379
|
+
};
|
|
380
|
+
const propsWithSelection = {
|
|
381
|
+
...props,
|
|
382
|
+
isPaginated: true,
|
|
383
|
+
isSelectable: true,
|
|
384
|
+
manualSelectColumn: selectColumn,
|
|
385
|
+
SelectionStatusComponent: DataTable.ControlledSelectionStatus,
|
|
386
|
+
initialState: {
|
|
387
|
+
pageIndex: 0,
|
|
388
|
+
pageSize: 4,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
render(<DataTableWrapper {...propsWithSelection} />);
|
|
392
|
+
|
|
393
|
+
const selectAllRows = () => {
|
|
394
|
+
const selectRowsButtons = screen.getAllByTitle('Toggle Row Selected');
|
|
395
|
+
selectRowsButtons.forEach(async (button) => {
|
|
396
|
+
await userEvent.click(button);
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
// Select all page 1
|
|
400
|
+
selectAllRows();
|
|
401
|
+
|
|
402
|
+
// Paginate to page 2
|
|
403
|
+
const nextPageButton2 = screen.getByLabelText('Next, Page 2');
|
|
404
|
+
await userEvent.click(nextPageButton2);
|
|
405
|
+
|
|
406
|
+
// Select all page 2
|
|
407
|
+
selectAllRows();
|
|
408
|
+
|
|
409
|
+
await waitFor(() => expect(screen.getByText('All 7 selected')).toBeInTheDocument());
|
|
410
|
+
});
|
|
315
411
|
});
|
|
316
412
|
});
|
package/src/Form/_index.scss
CHANGED
|
@@ -294,7 +294,8 @@
|
|
|
294
294
|
|
|
295
295
|
.form-control:focus ~ &,
|
|
296
296
|
// select.form-control ~ &,
|
|
297
|
-
.form-control.has-value ~
|
|
297
|
+
.form-control.has-value ~ &,
|
|
298
|
+
.form-control:is(:-webkit-autofill, :autofill) ~ & {
|
|
298
299
|
.pgn__form-control-floating-label-text {
|
|
299
300
|
background-color: var(--pgn-color-form-input-bg-base);
|
|
300
301
|
}
|
|
@@ -408,6 +409,7 @@ select.form-control {
|
|
|
408
409
|
appearance: none;
|
|
409
410
|
height: var(--pgn-size-form-control-indicator-base);
|
|
410
411
|
width: var(--pgn-size-form-control-indicator-base);
|
|
412
|
+
min-width: var(--pgn-size-form-control-indicator-base);
|
|
411
413
|
background-color: var(--pgn-color-form-control-indicator-bg-base);
|
|
412
414
|
border:
|
|
413
415
|
solid var(--pgn-size-form-control-indicator-border-width)
|
|
@@ -468,6 +470,7 @@ select.form-control {
|
|
|
468
470
|
|
|
469
471
|
.pgn__form-switch-input {
|
|
470
472
|
width: var(--pgn-size-form-control-switch-width);
|
|
473
|
+
min-width: var(--pgn-size-form-control-switch-width);
|
|
471
474
|
border-radius: var(--pgn-size-form-control-switch-indicator-border-radius);
|
|
472
475
|
background-image: var(--pgn-other-content-form-control-switch-indicator-icon-off);
|
|
473
476
|
background-position: left center;
|
package/src/Modal/AlertModal.jsx
CHANGED
|
@@ -20,11 +20,11 @@ function AlertModal({
|
|
|
20
20
|
<ModalDialog.Header>
|
|
21
21
|
<ModalDialog.Title>
|
|
22
22
|
{icon && (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
<Icon
|
|
24
|
+
data-testid="title-icon"
|
|
25
|
+
src={icon}
|
|
26
|
+
className={classNames('pgn__alert-modal__title_icon')}
|
|
27
|
+
/>
|
|
28
28
|
)}
|
|
29
29
|
{props.title}
|
|
30
30
|
</ModalDialog.Title>
|
|
@@ -53,7 +53,7 @@ interface Props {
|
|
|
53
53
|
/** Specifies the z-index of the modal */
|
|
54
54
|
zIndex?: number;
|
|
55
55
|
/** Specifies whether overflow is visible in the modal */
|
|
56
|
-
isOverflowVisible
|
|
56
|
+
isOverflowVisible: boolean;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function ModalDialog({
|
|
@@ -70,7 +70,7 @@ function ModalDialog({
|
|
|
70
70
|
isFullscreenOnMobile = false,
|
|
71
71
|
isBlocking = false,
|
|
72
72
|
zIndex,
|
|
73
|
-
isOverflowVisible
|
|
73
|
+
isOverflowVisible,
|
|
74
74
|
}: Props) {
|
|
75
75
|
const isMobile = useMediaQuery({ query: '(max-width: 767.98px)' });
|
|
76
76
|
const showFullScreen = (isFullscreenOnMobile && isMobile);
|
|
@@ -143,6 +143,7 @@
|
|
|
143
143
|
|
|
144
144
|
.pgn__modal-title {
|
|
145
145
|
font-size: var(--pgn-typography-font-size-h3-base);
|
|
146
|
+
line-height: calc(var(--pgn-typography-font-size-h3-base) * var(--pgn-typography-headings-line-height));
|
|
146
147
|
margin-inline-end: 3rem; // roughly accomodate the width of the close buttonn
|
|
147
148
|
text-align: start;
|
|
148
149
|
}
|
|
@@ -330,7 +331,6 @@
|
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
.pgn__modal-title {
|
|
333
|
-
font-size: var(--pgn-typography-font-size-h4-base);
|
|
334
334
|
display: flex;
|
|
335
335
|
flex-grow: 1;
|
|
336
336
|
align-items: center;
|
|
@@ -36,6 +36,7 @@ This is the alert style `ModalDialog` composition. `AlertModal` passes all of it
|
|
|
36
36
|
<Button variant="danger">Submit</Button>
|
|
37
37
|
</ActionRow>
|
|
38
38
|
)}
|
|
39
|
+
isOverflowVisible={false}
|
|
39
40
|
>
|
|
40
41
|
<p>
|
|
41
42
|
I'm baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever.
|
|
@@ -67,6 +68,7 @@ const [isOpen, open, close] = useToggle(false);
|
|
|
67
68
|
<Button variant="primary">Delete</Button>
|
|
68
69
|
</ActionRow>
|
|
69
70
|
)}
|
|
71
|
+
isOverflowVisible={false}
|
|
70
72
|
>
|
|
71
73
|
<p>
|
|
72
74
|
Are your sure you want to delete this file? You can't undo this action.
|
|
@@ -95,6 +97,7 @@ const [isOpen, open, close] = useToggle(false);
|
|
|
95
97
|
<Button variant="danger">Acknowledge errror</Button>
|
|
96
98
|
</ActionRow>
|
|
97
99
|
)}
|
|
100
|
+
isOverflowVisible={false}
|
|
98
101
|
>
|
|
99
102
|
<p>
|
|
100
103
|
An unknown error has occured.
|
|
@@ -123,6 +126,7 @@ const [isOpen, open, close] = useToggle(false);
|
|
|
123
126
|
<Button variant="success">Confirm</Button>
|
|
124
127
|
</ActionRow>
|
|
125
128
|
)}
|
|
129
|
+
isOverflowVisible={false}
|
|
126
130
|
>
|
|
127
131
|
<p>
|
|
128
132
|
All good!
|
|
@@ -50,6 +50,7 @@ The ``MarketingModal`` is a preconfigured `ModalDialog` that accepts an image an
|
|
|
50
50
|
<Button>Submit</Button>
|
|
51
51
|
</ActionRow>
|
|
52
52
|
)}
|
|
53
|
+
isOverflowVisible={false}
|
|
53
54
|
>
|
|
54
55
|
<p>
|
|
55
56
|
I'm baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps. Tumeric messenger bag bicycle rights wayfarers, try-hard cronut blue bottle health goth. Sriracha tumblr cardigan, cloud bread succulents tumeric copper mug marfa semiotics woke next level organic roof party +1 try-hard.
|
|
@@ -47,7 +47,7 @@ label for the dialog element.
|
|
|
47
47
|
variant={modalVariant}
|
|
48
48
|
hasCloseButton
|
|
49
49
|
isFullscreenOnMobile
|
|
50
|
-
isOverflowVisible
|
|
50
|
+
isOverflowVisible={false}
|
|
51
51
|
>
|
|
52
52
|
<ModalDialog.Header>
|
|
53
53
|
<ModalDialog.Title>
|
|
@@ -125,7 +125,7 @@ label for the dialog element.
|
|
|
125
125
|
size={modalSize}
|
|
126
126
|
variant={modalVariant}
|
|
127
127
|
hasCloseButton
|
|
128
|
-
isOverflowVisible
|
|
128
|
+
isOverflowVisible={false}
|
|
129
129
|
>
|
|
130
130
|
<ModalDialog.Hero>
|
|
131
131
|
<ModalDialog.Hero.Background
|
|
@@ -38,6 +38,7 @@ The standard `ModalDialog` composition. `StandardModal` passes all of its props
|
|
|
38
38
|
<Button>Submit</Button>
|
|
39
39
|
</ActionRow>
|
|
40
40
|
)}
|
|
41
|
+
isOverflowVisible={false}
|
|
41
42
|
>
|
|
42
43
|
<p>
|
|
43
44
|
I'm baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps. Tumeric messenger bag bicycle rights wayfarers, try-hard cronut blue bottle health goth. Sriracha tumblr cardigan, cloud bread succulents tumeric copper mug marfa semiotics woke next level organic roof party +1 try-hard.
|
|
@@ -42,6 +42,7 @@ describe('<AlertModal />', () => {
|
|
|
42
42
|
isOpen={isOpen}
|
|
43
43
|
onClose={closeFn}
|
|
44
44
|
footerNode={<p>footer</p>}
|
|
45
|
+
isOverflowVisible={false}
|
|
45
46
|
>
|
|
46
47
|
<Body />
|
|
47
48
|
</AlertModal>,
|
|
@@ -60,6 +61,7 @@ describe('<AlertModal />', () => {
|
|
|
60
61
|
onClose={closeFn}
|
|
61
62
|
icon={Info}
|
|
62
63
|
footerNode={<p>footer</p>}
|
|
64
|
+
isOverflowVisible={false}
|
|
63
65
|
>
|
|
64
66
|
<Body />
|
|
65
67
|
</AlertModal>,
|
|
@@ -77,6 +79,7 @@ describe('<AlertModal />', () => {
|
|
|
77
79
|
onClose={closeFn}
|
|
78
80
|
icon={Info}
|
|
79
81
|
footerNode={<p>footer</p>}
|
|
82
|
+
isOverflowVisible={false}
|
|
80
83
|
>
|
|
81
84
|
<Body />
|
|
82
85
|
</AlertModal>,
|
|
@@ -94,6 +97,7 @@ describe('<AlertModal />', () => {
|
|
|
94
97
|
onClose={closeFn}
|
|
95
98
|
icon={Info}
|
|
96
99
|
footerNode={<p>footer</p>}
|
|
100
|
+
isOverflowVisible={false}
|
|
97
101
|
>
|
|
98
102
|
<Body />
|
|
99
103
|
</AlertModal>,
|
|
@@ -14,7 +14,7 @@ describe('ModalDialog', () => {
|
|
|
14
14
|
size="md"
|
|
15
15
|
variant="default"
|
|
16
16
|
hasCloseButton
|
|
17
|
-
isOverflowVisible
|
|
17
|
+
isOverflowVisible={false}
|
|
18
18
|
>
|
|
19
19
|
<ModalDialog.Header>
|
|
20
20
|
<ModalDialog.Title>The title</ModalDialog.Title>
|
|
@@ -43,6 +43,7 @@ describe('ModalDialog', () => {
|
|
|
43
43
|
<ModalDialog
|
|
44
44
|
title="My dialog"
|
|
45
45
|
onClose={onClose}
|
|
46
|
+
isOverflowVisible={false}
|
|
46
47
|
>
|
|
47
48
|
<ModalDialog.Header><ModalDialog.Title>The title</ModalDialog.Title></ModalDialog.Header>
|
|
48
49
|
<ModalDialog.Body><p>The hidden content</p></ModalDialog.Body>
|
|
@@ -65,7 +66,7 @@ describe('ModalDialog with Hero', () => {
|
|
|
65
66
|
size="md"
|
|
66
67
|
variant="default"
|
|
67
68
|
hasCloseButton
|
|
68
|
-
isOverflowVisible
|
|
69
|
+
isOverflowVisible={false}
|
|
69
70
|
>
|
|
70
71
|
<ModalDialog.Hero>
|
|
71
72
|
<ModalDialog.Hero.Background backgroundSrc="imageurl" />
|
package/src/Toast/README.md
CHANGED
|
@@ -5,7 +5,7 @@ components:
|
|
|
5
5
|
- Toast
|
|
6
6
|
categories:
|
|
7
7
|
- Overlays
|
|
8
|
-
status: '
|
|
8
|
+
status: 'Stable'
|
|
9
9
|
designStatus: 'Done'
|
|
10
10
|
devStatus: 'Done'
|
|
11
11
|
notes: ''
|
|
@@ -39,7 +39,7 @@ notes: ''
|
|
|
39
39
|
Example of a basic Toast.
|
|
40
40
|
</Toast>
|
|
41
41
|
|
|
42
|
-
<Button
|
|
42
|
+
<Button onClick={() => setShow(true)}>Show Toast</Button>
|
|
43
43
|
</>
|
|
44
44
|
);
|
|
45
45
|
}
|
|
@@ -64,7 +64,7 @@ notes: ''
|
|
|
64
64
|
Success! Example of a Toast with a button.
|
|
65
65
|
</Toast>
|
|
66
66
|
|
|
67
|
-
<Button
|
|
67
|
+
<Button onClick={() => setShow(true)}>Show Toast</Button>
|
|
68
68
|
</>
|
|
69
69
|
);
|
|
70
70
|
}
|
|
@@ -89,7 +89,7 @@ notes: ''
|
|
|
89
89
|
Success! Example of a Toast with a link.
|
|
90
90
|
</Toast>
|
|
91
91
|
|
|
92
|
-
<Button
|
|
92
|
+
<Button onClick={() => setShow(true)}>Show Toast</Button>
|
|
93
93
|
</>
|
|
94
94
|
);
|
|
95
95
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import { render, screen } from '@testing-library/react';
|
|
3
2
|
import { IntlProvider } from 'react-intl';
|
|
4
3
|
import userEvent from '@testing-library/user-event';
|
|
5
4
|
|
|
6
5
|
import Toast from '.';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
function ToastWrapper({ children, ...props }) {
|
|
7
|
+
function ToastWrapper({ children, ...props }: React.ComponentProps<typeof Toast>) {
|
|
10
8
|
return (
|
|
11
9
|
<IntlProvider locale="en">
|
|
12
10
|
<Toast {...props}>
|
|
@@ -17,7 +15,7 @@ function ToastWrapper({ children, ...props }) {
|
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
describe('<Toast />', () => {
|
|
20
|
-
const onCloseHandler = ()
|
|
18
|
+
const onCloseHandler = jest.fn();
|
|
21
19
|
const props = {
|
|
22
20
|
onClose: onCloseHandler,
|
|
23
21
|
show: true,
|
|
@@ -44,7 +42,7 @@ describe('<Toast />', () => {
|
|
|
44
42
|
{...props}
|
|
45
43
|
action={{
|
|
46
44
|
label: 'Optional action',
|
|
47
|
-
onClick: ()
|
|
45
|
+
onClick: jest.fn(),
|
|
48
46
|
}}
|
|
49
47
|
>
|
|
50
48
|
Success message.
|
|
@@ -55,38 +53,50 @@ describe('<Toast />', () => {
|
|
|
55
53
|
});
|
|
56
54
|
it('autohide is set to false on onMouseOver and true on onMouseLeave', async () => {
|
|
57
55
|
render(
|
|
58
|
-
<ToastWrapper
|
|
56
|
+
<ToastWrapper {...props}>
|
|
59
57
|
Success message.
|
|
60
58
|
</ToastWrapper>,
|
|
61
59
|
);
|
|
62
|
-
const toast = screen.
|
|
60
|
+
const toast = screen.getByRole('alert');
|
|
63
61
|
await userEvent.hover(toast);
|
|
64
62
|
setTimeout(() => {
|
|
65
|
-
expect(screen.getByText('Success message.')).
|
|
63
|
+
expect(screen.getByText('Success message.')).toBeTruthy();
|
|
66
64
|
expect(toast).toHaveLength(1);
|
|
67
65
|
}, 6000);
|
|
68
66
|
await userEvent.unhover(toast);
|
|
69
67
|
setTimeout(() => {
|
|
70
|
-
expect(screen.getByText('Success message.')).
|
|
68
|
+
expect(screen.getByText('Success message.')).toBeTruthy();
|
|
71
69
|
expect(toast).toHaveLength(1);
|
|
72
70
|
}, 6000);
|
|
73
71
|
});
|
|
74
72
|
it('autohide is set to false onFocus and true onBlur', async () => {
|
|
75
73
|
render(
|
|
76
|
-
<ToastWrapper
|
|
74
|
+
<ToastWrapper {...props}>
|
|
77
75
|
Success message.
|
|
78
76
|
</ToastWrapper>,
|
|
79
77
|
);
|
|
80
|
-
const toast = screen.
|
|
78
|
+
const toast = screen.getByRole('alert');
|
|
81
79
|
toast.focus();
|
|
82
80
|
setTimeout(() => {
|
|
83
|
-
expect(screen.getByText('Success message.')).
|
|
81
|
+
expect(screen.getByText('Success message.')).toBeTruthy();
|
|
84
82
|
expect(toast).toHaveLength(1);
|
|
85
83
|
}, 6000);
|
|
86
84
|
await userEvent.tab();
|
|
87
85
|
setTimeout(() => {
|
|
88
|
-
expect(screen.getByText('Success message.')).
|
|
86
|
+
expect(screen.getByText('Success message.')).toBeTruthy();
|
|
89
87
|
expect(toast).toHaveLength(1);
|
|
90
88
|
}, 6000);
|
|
91
89
|
});
|
|
90
|
+
it('should contain aria-atomic and aria-live', async () => {
|
|
91
|
+
render(
|
|
92
|
+
<ToastWrapper {...props}>
|
|
93
|
+
Success message.
|
|
94
|
+
</ToastWrapper>,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const toast = screen.getByRole('alert');
|
|
98
|
+
|
|
99
|
+
expect(toast).toHaveAttribute('aria-atomic', 'true');
|
|
100
|
+
expect(toast).toHaveAttribute('aria-live', 'assertive');
|
|
101
|
+
});
|
|
92
102
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ReactNode, useEffect, useState } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
|
|
4
|
+
interface ToastContainerProps {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const TOAST_ROOT_ID = 'toast-root';
|
|
9
|
+
|
|
10
|
+
function ToastContainer({ children }: ToastContainerProps) {
|
|
11
|
+
const [rootElement, setRootElement] = useState<HTMLElement | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (typeof document !== 'undefined') {
|
|
15
|
+
let existingElement = document.getElementById(TOAST_ROOT_ID);
|
|
16
|
+
|
|
17
|
+
if (!existingElement) {
|
|
18
|
+
existingElement = document.createElement('div');
|
|
19
|
+
existingElement.id = TOAST_ROOT_ID;
|
|
20
|
+
existingElement.className = 'toast-container';
|
|
21
|
+
document.body.appendChild(existingElement);
|
|
22
|
+
}
|
|
23
|
+
setRootElement(existingElement);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return rootElement ? ReactDOM.createPortal(children, rootElement) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default ToastContainer;
|
package/src/Toast/index.scss
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
background-color: var(--pgn-color-toast-bg);
|
|
17
17
|
box-shadow: var(--pgn-elevation-toast-box-shadow);
|
|
18
18
|
margin: 0;
|
|
19
|
-
padding:
|
|
19
|
+
padding: var(--pgn-spacing-spacer-base);
|
|
20
20
|
position: relative;
|
|
21
21
|
border-radius: var(--pgn-size-toast-border-radius);
|
|
22
22
|
z-index: 2;
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
& + .btn {
|
|
52
|
-
margin-top:
|
|
52
|
+
margin-top: var(--pgn-spacing-spacer-base);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
|
-
|
|
5
4
|
import BaseToast from 'react-bootstrap/Toast';
|
|
6
5
|
import { useIntl } from 'react-intl';
|
|
7
6
|
|
|
@@ -14,16 +13,40 @@ import IconButton from '../IconButton';
|
|
|
14
13
|
export const TOAST_CLOSE_LABEL_TEXT = 'Close';
|
|
15
14
|
export const TOAST_DELAY = 5000;
|
|
16
15
|
|
|
16
|
+
interface ToastAction {
|
|
17
|
+
label: string;
|
|
18
|
+
href?: string;
|
|
19
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ToastProps {
|
|
23
|
+
children: string;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
show: boolean;
|
|
26
|
+
action?: ToastAction;
|
|
27
|
+
closeLabel?: string;
|
|
28
|
+
delay?: number;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
function Toast({
|
|
18
|
-
action,
|
|
19
|
-
|
|
33
|
+
action,
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
closeLabel,
|
|
37
|
+
onClose,
|
|
38
|
+
show,
|
|
39
|
+
...rest
|
|
40
|
+
}: ToastProps) {
|
|
20
41
|
const intl = useIntl();
|
|
21
42
|
const [autoHide, setAutoHide] = useState(true);
|
|
43
|
+
|
|
22
44
|
const intlCloseLabel = closeLabel || intl.formatMessage({
|
|
23
45
|
id: 'pgn.Toast.closeLabel',
|
|
24
46
|
defaultMessage: 'Close',
|
|
25
47
|
description: 'Close label for Toast component',
|
|
26
48
|
});
|
|
49
|
+
|
|
27
50
|
return (
|
|
28
51
|
<ToastContainer>
|
|
29
52
|
<BaseToast
|
|
@@ -37,9 +60,7 @@ function Toast({
|
|
|
37
60
|
show={show}
|
|
38
61
|
{...rest}
|
|
39
62
|
>
|
|
40
|
-
<div
|
|
41
|
-
className="toast-header"
|
|
42
|
-
>
|
|
63
|
+
<div className="toast-header">
|
|
43
64
|
<p className="small">{children}</p>
|
|
44
65
|
<div className="toast-header-btn-container">
|
|
45
66
|
<IconButton
|
package/src/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { default as ModalDialog, MODAL_DIALOG_CLOSE_LABEL } from './Modal/ModalD
|
|
|
17
17
|
export { default as ModalLayer } from './Modal/ModalLayer';
|
|
18
18
|
export { default as Overlay, OverlayTrigger } from './Overlay';
|
|
19
19
|
export { default as Portal } from './Modal/Portal';
|
|
20
|
+
export { default as Toast, TOAST_CLOSE_LABEL_TEXT, TOAST_DELAY } from './Toast';
|
|
20
21
|
export { default as Tooltip } from './Tooltip';
|
|
21
22
|
export { default as useWindowSize, type WindowSizeData } from './hooks/useWindowSizeHook';
|
|
22
23
|
export { default as useToggle, type Toggler, type ToggleHandlers } from './hooks/useToggleHook';
|
|
@@ -163,7 +164,6 @@ export const
|
|
|
163
164
|
// from './Tabs';
|
|
164
165
|
/** @deprecated Replaced by `Form.Control`. */
|
|
165
166
|
export const TextArea: any; // from './TextArea';
|
|
166
|
-
export const Toast: any, TOAST_CLOSE_LABEL_TEXT: string, TOAST_DELAY: number; // from './Toast';
|
|
167
167
|
/** @deprecated Replaced by `Form.Group`. */
|
|
168
168
|
export const ValidationFormGroup: any; // from './ValidationFormGroup';
|
|
169
169
|
export const TransitionReplace: any; // from './TransitionReplace';
|