@linagora/twake-mui 17.0.0 → 18.0.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/CHANGELOG.md +26 -0
- package/dist/components/ActionsMenu/Actions/download.d.ts +1 -3
- package/dist/components/ActionsMenu/Actions/download.js +1 -2
- package/dist/components/NestedSelect/ItemRow.d.ts +13 -0
- package/dist/components/NestedSelect/ItemRow.js +28 -0
- package/dist/components/NestedSelect/NestedSelect.stories.d.ts +19 -0
- package/dist/components/NestedSelect/NestedSelect.stories.js +149 -0
- package/dist/components/NestedSelect/NestedSelectBottomSheet.d.ts +12 -0
- package/dist/components/NestedSelect/NestedSelectBottomSheet.js +27 -0
- package/dist/components/NestedSelect/NestedSelectModal.d.ts +9 -0
- package/dist/components/NestedSelect/NestedSelectModal.js +36 -0
- package/dist/components/NestedSelect/NestedSelectResponsive.d.ts +6 -0
- package/dist/components/NestedSelect/NestedSelectResponsive.js +13 -0
- package/dist/components/NestedSelect/helpers.d.ts +3 -0
- package/dist/components/NestedSelect/helpers.js +22 -0
- package/dist/components/NestedSelect/index.d.ts +8 -0
- package/dist/components/NestedSelect/index.js +71 -0
- package/dist/components/NestedSelect/types.d.ts +72 -0
- package/dist/components/NestedSelect/types.js +1 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.js +8 -0
- package/dist/providers/ConfirmDialogProvider/ConfirmDialogProvider.stories.d.ts +7 -0
- package/dist/providers/ConfirmDialogProvider/ConfirmDialogProvider.stories.js +31 -0
- package/dist/providers/ConfirmDialogProvider/index.d.ts +6 -0
- package/dist/providers/ConfirmDialogProvider/index.js +15 -0
- package/dist/providers/ConfirmDialogProvider/useConfirmDialog.d.ts +8 -0
- package/dist/providers/ConfirmDialogProvider/useConfirmDialog.js +9 -0
- package/dist/providers/SelectionProvider/SelectionProvider.stories.d.ts +6 -0
- package/dist/providers/SelectionProvider/SelectionProvider.stories.js +25 -0
- package/dist/providers/SelectionProvider/index.d.ts +7 -0
- package/dist/providers/SelectionProvider/index.js +28 -0
- package/dist/providers/SelectionProvider/useSelection.d.ts +13 -0
- package/dist/providers/SelectionProvider/useSelection.js +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
# @linagora/twake-mui [18.0.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@17.1.0...@linagora/twake-mui@18.0.0) (2026-10-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
* feat(twake-mui)!: Remove encryptedUrl from the download action ([bf7480f](https://github.com/linagora/twake-ui/commit/bf7480f37d89b166850cc50fdf566b421eef00cb))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **twake-mui:** Migrate ConfirmDialogProvider from cozy-ui ([eb9de6d](https://github.com/linagora/twake-ui/commit/eb9de6d3ac69b83455d127bb44f826f26af9e2ec))
|
|
10
|
+
* **twake-mui:** Migrate SelectionProvider from cozy-ui ([50b91f4](https://github.com/linagora/twake-ui/commit/50b91f446ca91c498514debe1034010db2ac5557))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* `download` no longer accepts `encryptedUrl` and
|
|
16
|
+
`DownloadFileOptions` no longer has `url`. Apps that downloaded an
|
|
17
|
+
encrypted file through this option must pass its URL to their own
|
|
18
|
+
`downloadFile` implementation instead.
|
|
19
|
+
|
|
20
|
+
# @linagora/twake-mui [17.1.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@17.0.0...@linagora/twake-mui@17.1.0) (2026-10-22)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* **twake-mui:** Migrate NestedSelect from cozy-ui ([112d694](https://github.com/linagora/twake-ui/commit/112d69461cab48248b088c1da2c722b18d2dbffc))
|
|
26
|
+
|
|
1
27
|
# @linagora/twake-mui [17.0.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@16.3.0...@linagora/twake-mui@17.0.0) (2026-10-22)
|
|
2
28
|
|
|
3
29
|
|
|
@@ -3,13 +3,11 @@ import { Action, ActionDocument } from '../types';
|
|
|
3
3
|
export interface DownloadFileOptions<Client> {
|
|
4
4
|
client: Client;
|
|
5
5
|
file: ActionDocument;
|
|
6
|
-
url?: string;
|
|
7
6
|
webviewIntent?: WebviewService;
|
|
8
7
|
driveId?: string;
|
|
9
8
|
}
|
|
10
9
|
export interface DownloadActionOptions<Client> {
|
|
11
10
|
client: Client;
|
|
12
|
-
encryptedUrl?: string;
|
|
13
11
|
downloadFile: (options: DownloadFileOptions<Client>) => unknown;
|
|
14
12
|
}
|
|
15
|
-
export declare const download: <Client>({ client,
|
|
13
|
+
export declare const download: <Client>({ client, downloadFile }: DownloadActionOptions<Client>) => Action;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Download } from '@linagora/twake-icons';
|
|
2
2
|
import { makeAction } from './makeAction';
|
|
3
3
|
import { getActionsI18n } from '../locales';
|
|
4
|
-
export const download = ({ client,
|
|
4
|
+
export const download = ({ client, downloadFile }) => {
|
|
5
5
|
const { t } = getActionsI18n();
|
|
6
6
|
return makeAction({
|
|
7
7
|
name: 'download',
|
|
@@ -10,7 +10,6 @@ export const download = ({ client, encryptedUrl, downloadFile }) => {
|
|
|
10
10
|
action: (docs, { webviewIntent, driveId }) => downloadFile({
|
|
11
11
|
client,
|
|
12
12
|
file: docs[0],
|
|
13
|
-
url: encryptedUrl,
|
|
14
13
|
webviewIntent,
|
|
15
14
|
driveId
|
|
16
15
|
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NestedSelectItem, NestedSelectRadioPosition } from './types';
|
|
3
|
+
export interface ItemRowProps {
|
|
4
|
+
item: NestedSelectItem;
|
|
5
|
+
onClick: (item: NestedSelectItem) => void;
|
|
6
|
+
isSelected: boolean;
|
|
7
|
+
radioPosition?: NestedSelectRadioPosition;
|
|
8
|
+
isLast?: boolean;
|
|
9
|
+
ellipsis?: boolean;
|
|
10
|
+
noDivider?: boolean;
|
|
11
|
+
}
|
|
12
|
+
declare const ItemRow: ({ item, onClick, isSelected, radioPosition, isLast, ellipsis, noDivider }: ItemRowProps) => React.ReactElement;
|
|
13
|
+
export default ItemRow;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Icon, Right } from '@linagora/twake-icons';
|
|
2
|
+
import { Divider, ListItemIcon, Radio, Typography } from '@mui/material';
|
|
3
|
+
import { useTheme } from '@mui/material/styles';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
6
|
+
import ListItemButton from '../ListItemButton';
|
|
7
|
+
import ListItemText from '../ListItemText';
|
|
8
|
+
const ItemRow = ({ item, onClick, isSelected, radioPosition, isLast, ellipsis, noDivider }) => {
|
|
9
|
+
const { isDesktop } = useBreakpoints();
|
|
10
|
+
const theme = useTheme();
|
|
11
|
+
const hasChildren = (item.children?.length ?? 0) > 0;
|
|
12
|
+
return (React.createElement(React.Fragment, null,
|
|
13
|
+
React.createElement(ListItemButton, { gutters: isDesktop ? 'double' : 'default', ellipsis: ellipsis, onClick: () => onClick(item) },
|
|
14
|
+
radioPosition === 'left' && (React.createElement(ListItemIcon, null,
|
|
15
|
+
React.createElement(Radio, { readOnly: true, checked: isSelected }))),
|
|
16
|
+
item.icon && React.createElement(ListItemIcon, null, item.icon),
|
|
17
|
+
React.createElement(ListItemText, { primary: item.title, secondary: item.description }),
|
|
18
|
+
hasChildren && (React.createElement(ListItemIcon, null,
|
|
19
|
+
React.createElement(Icon, { icon: Right, color: theme.vars?.palette.text.secondary }))),
|
|
20
|
+
radioPosition === 'right' && !hasChildren && (React.createElement("div", { className: "u-flex u-flex-items-center" },
|
|
21
|
+
item.info && (React.createElement(Typography, { variant: "body2", color: "textSecondary" }, item.info)),
|
|
22
|
+
React.createElement(ListItemIcon, null,
|
|
23
|
+
React.createElement(Radio, { readOnly: true, edge: "end", checked: isSelected })))),
|
|
24
|
+
radioPosition === 'right' && item.action && (React.createElement(Divider, { orientation: "vertical", flexItem: true })),
|
|
25
|
+
item.action?.Component({ item, ...item.action.props })),
|
|
26
|
+
!noDivider && !isLast && React.createElement(Divider, null)));
|
|
27
|
+
};
|
|
28
|
+
export default ItemRow;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
interface ExampleArgs {
|
|
4
|
+
noTitle: boolean;
|
|
5
|
+
noDivider: boolean;
|
|
6
|
+
leftRadio: boolean;
|
|
7
|
+
withSearch: boolean;
|
|
8
|
+
withEllipsis: boolean;
|
|
9
|
+
withHeaders: boolean;
|
|
10
|
+
canSelectParent: boolean;
|
|
11
|
+
withFocus: boolean;
|
|
12
|
+
}
|
|
13
|
+
declare const Example: ({ noTitle, noDivider, leftRadio, withSearch, withEllipsis, withHeaders, canSelectParent, withFocus }: ExampleArgs) => React.ReactElement;
|
|
14
|
+
declare const meta: Meta<typeof Example>;
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
export declare const Default: Story;
|
|
18
|
+
export declare const Screenshot: Story;
|
|
19
|
+
export declare const ResponsiveScreenshot: Story;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Icon, Setting } from '@linagora/twake-icons';
|
|
2
|
+
import { Alert, Button, IconButton, ListItemSecondaryAction, Paper, Stack, Typography } from '@mui/material';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import NestedSelectResponsive from './NestedSelectResponsive';
|
|
5
|
+
import NestedSelect from './index';
|
|
6
|
+
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
7
|
+
import Avatar from '../Avatar';
|
|
8
|
+
const LONG_TEXT = 'Augusta Ada King-Noel, Countess of Lovelace, was an English mathematician and writer, chiefly known for her work on Charles Babbage proposed mechanical general-purpose computer.';
|
|
9
|
+
const letterOption = (letter, description, key) => ({
|
|
10
|
+
id: key ? `${letter}_${key}` : letter,
|
|
11
|
+
key,
|
|
12
|
+
title: letter,
|
|
13
|
+
description,
|
|
14
|
+
icon: React.createElement(Avatar, { color: "sugarCoral" }, letter[0])
|
|
15
|
+
});
|
|
16
|
+
const SettingAction = ({ item, onClick }) => (React.createElement(ListItemSecondaryAction, null,
|
|
17
|
+
React.createElement(IconButton, { "aria-label": "Settings", onClick: () => onClick(item) },
|
|
18
|
+
React.createElement(Icon, { icon: Setting }))));
|
|
19
|
+
const makeOptions = ({ withHeaders = false, focusedId } = {}) => ({
|
|
20
|
+
focusedId,
|
|
21
|
+
header: withHeaders && (React.createElement(Alert, { className: "u-mt-1 u-mh-1", icon: false }, "This is a header for options")),
|
|
22
|
+
childrenHeader: withHeaders
|
|
23
|
+
? (level) => level === 1 ? (React.createElement(Alert, { className: "u-mt-1 u-mh-1" }, "This is specific option header for first children")) : (React.createElement(Alert, { className: "u-mt-1 u-mh-1", icon: false, severity: "secondary" }, "This is specific option header for other children"))
|
|
24
|
+
: undefined,
|
|
25
|
+
children: [
|
|
26
|
+
{ ...letterOption('A', 'is for Apple'), info: 'info' },
|
|
27
|
+
{
|
|
28
|
+
...letterOption('B', 'is for Balloon'),
|
|
29
|
+
children: [
|
|
30
|
+
{
|
|
31
|
+
...letterOption('B1'),
|
|
32
|
+
children: [letterOption('B1a'), letterOption('B1b')]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
...letterOption('B2'),
|
|
36
|
+
header: withHeaders && (React.createElement(Alert, { className: "u-mt-1 u-mh-1", icon: false, severity: "success" }, "This is overriden specific option!")),
|
|
37
|
+
children: [letterOption('B2a'), letterOption('B2b')]
|
|
38
|
+
},
|
|
39
|
+
letterOption('B3')
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
...letterOption('C', 'is for crayon'),
|
|
44
|
+
action: {
|
|
45
|
+
Component: SettingAction,
|
|
46
|
+
props: { onClick: () => undefined }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
letterOption('D', 'is for Drums !'),
|
|
50
|
+
letterOption(`E. A very long option, that should be ellipsed. ${LONG_TEXT}`, `Its description is also very long and will be ellipsed. ${LONG_TEXT}`),
|
|
51
|
+
letterOption('F'),
|
|
52
|
+
letterOption('G'),
|
|
53
|
+
letterOption('H', 'for hero', 'hero'),
|
|
54
|
+
letterOption('H', 'for helicopter', 'helicopter'),
|
|
55
|
+
letterOption('I'),
|
|
56
|
+
letterOption('J'),
|
|
57
|
+
letterOption('K'),
|
|
58
|
+
letterOption('L'),
|
|
59
|
+
letterOption('M'),
|
|
60
|
+
letterOption('N')
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
const makeSearchOptions = () => ({
|
|
64
|
+
placeholderSearch: 'Placeholder Search',
|
|
65
|
+
noDataLabel: 'No Data Found',
|
|
66
|
+
onSearch: value => (makeOptions().children ?? []).filter(option => typeof option.description === 'string' &&
|
|
67
|
+
option.description.toLowerCase().includes(value.toLowerCase()))
|
|
68
|
+
});
|
|
69
|
+
const transformParentItem = (item) => ({
|
|
70
|
+
...item,
|
|
71
|
+
title: React.createElement(React.Fragment, null,
|
|
72
|
+
"All of ",
|
|
73
|
+
item.title)
|
|
74
|
+
});
|
|
75
|
+
const Example = ({ noTitle, noDivider, leftRadio, withSearch, withEllipsis, withHeaders, canSelectParent, withFocus }) => {
|
|
76
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
77
|
+
const [selectedItem, setSelectedItem] = useState(letterOption('A'));
|
|
78
|
+
const handleSelect = (item) => {
|
|
79
|
+
setSelectedItem(item);
|
|
80
|
+
setIsOpen(false);
|
|
81
|
+
};
|
|
82
|
+
return (React.createElement(React.Fragment, null,
|
|
83
|
+
React.createElement(Typography, { className: "u-mb-1" },
|
|
84
|
+
"Selected item: ",
|
|
85
|
+
selectedItem.title,
|
|
86
|
+
" ",
|
|
87
|
+
selectedItem.description),
|
|
88
|
+
React.createElement(Button, { onClick: () => setIsOpen(true) }, "Select"),
|
|
89
|
+
isOpen && (React.createElement(NestedSelectResponsive, { canSelectParent: canSelectParent, onSelect: handleSelect, onClose: () => setIsOpen(false), isSelected: item => item.id === selectedItem.id, options: makeOptions({
|
|
90
|
+
withHeaders,
|
|
91
|
+
focusedId: withFocus ? selectedItem.id : undefined
|
|
92
|
+
}), radioPosition: leftRadio ? 'left' : 'right', title: noTitle ? undefined : 'Please select letter', transformParentItem: transformParentItem, searchOptions: withSearch ? makeSearchOptions() : undefined, ellipsis: withEllipsis, noDivider: noDivider }))));
|
|
93
|
+
};
|
|
94
|
+
const meta = {
|
|
95
|
+
title: 'NestedSelect',
|
|
96
|
+
component: Example,
|
|
97
|
+
tags: ['autodocs'],
|
|
98
|
+
args: {
|
|
99
|
+
noTitle: false,
|
|
100
|
+
noDivider: false,
|
|
101
|
+
leftRadio: false,
|
|
102
|
+
withSearch: false,
|
|
103
|
+
withEllipsis: true,
|
|
104
|
+
withHeaders: false,
|
|
105
|
+
canSelectParent: true,
|
|
106
|
+
withFocus: false
|
|
107
|
+
},
|
|
108
|
+
argTypes: {
|
|
109
|
+
noTitle: { control: 'boolean' },
|
|
110
|
+
noDivider: { control: 'boolean' },
|
|
111
|
+
leftRadio: { control: 'boolean' },
|
|
112
|
+
withSearch: { control: 'boolean' },
|
|
113
|
+
withEllipsis: { control: 'boolean' },
|
|
114
|
+
withHeaders: { control: 'boolean' },
|
|
115
|
+
canSelectParent: { control: 'boolean' },
|
|
116
|
+
withFocus: { control: 'boolean' }
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
export default meta;
|
|
120
|
+
export const Default = {};
|
|
121
|
+
const Section = ({ label, ...props }) => (React.createElement("section", null,
|
|
122
|
+
React.createElement("h3", null, label),
|
|
123
|
+
React.createElement(Paper, { variant: "outlined" },
|
|
124
|
+
React.createElement(NestedSelect, { onSelect: () => undefined, isSelected: item => item.title === 'A' || item.title === 'B1a', options: {
|
|
125
|
+
...makeOptions(),
|
|
126
|
+
children: makeOptions().children?.slice(0, 5)
|
|
127
|
+
}, title: "Please select letter", transformParentItem: transformParentItem, ...props }))));
|
|
128
|
+
// Visual Regression - Combined view for Argos testing
|
|
129
|
+
export const Screenshot = {
|
|
130
|
+
tags: ['argos', '!autodocs'],
|
|
131
|
+
render: () => (React.createElement(Stack, { spacing: 4 },
|
|
132
|
+
React.createElement(Section, { label: "Default" }),
|
|
133
|
+
React.createElement(Section, { label: "Left radio", radioPosition: "left" }),
|
|
134
|
+
React.createElement(Section, { label: "No divider", noDivider: true }),
|
|
135
|
+
React.createElement(Section, { label: "No ellipsis", ellipsis: false }),
|
|
136
|
+
React.createElement(Section, { label: "With search", searchOptions: makeSearchOptions() }),
|
|
137
|
+
React.createElement(Section, { label: "With headers", options: makeOptions({ withHeaders: true }) }),
|
|
138
|
+
React.createElement(Section, { label: "Focused on B1a, with selectable parent", canSelectParent: true, options: makeOptions({ withHeaders: true, focusedId: 'B1a' }) })))
|
|
139
|
+
};
|
|
140
|
+
const ResponsiveExample = () => {
|
|
141
|
+
const { isMobile } = useBreakpoints();
|
|
142
|
+
return (React.createElement(NestedSelectResponsive, { radioPosition: isMobile ? 'left' : 'right', canSelectParent: true, onSelect: () => undefined, isSelected: item => item.title === 'C', options: makeOptions(), title: "Please select letter", transformParentItem: transformParentItem, componentsProps: { bottomsheet: { skipAnimation: true } }, onClose: () => undefined }));
|
|
143
|
+
};
|
|
144
|
+
// Visual Regression - dialog on desktop, bottom sheet on mobile
|
|
145
|
+
export const ResponsiveScreenshot = {
|
|
146
|
+
tags: ['argos', '!autodocs'],
|
|
147
|
+
parameters: { layout: 'fullscreen' },
|
|
148
|
+
render: () => React.createElement(ResponsiveExample, null)
|
|
149
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NestedSelectProps } from './types';
|
|
3
|
+
import type { BottomSheetProps } from '../BottomSheet/types';
|
|
4
|
+
export interface NestedSelectBottomSheetProps extends NestedSelectProps {
|
|
5
|
+
onClose?: () => void;
|
|
6
|
+
componentsProps?: {
|
|
7
|
+
bottomsheet?: Omit<BottomSheetProps, 'backdrop' | 'onClose' | 'children'>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** NestedSelect in a bottom sheet with a backdrop */
|
|
11
|
+
declare const NestedSelectBottomSheet: ({ onClose, componentsProps, ...props }: NestedSelectBottomSheetProps) => React.ReactElement;
|
|
12
|
+
export default NestedSelectBottomSheet;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ArrowUp, Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { Divider, IconButton, Typography } from '@mui/material';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { useExtendI18n, useI18n } from 'twake-i18n';
|
|
6
|
+
import NestedSelect from './index';
|
|
7
|
+
import BottomSheet from '../BottomSheet';
|
|
8
|
+
import BottomSheetHeader from '../BottomSheet/BottomSheetHeader';
|
|
9
|
+
import BottomSheetItem from '../BottomSheet/BottomSheetItem';
|
|
10
|
+
import { locales } from '../Dialogs/locales';
|
|
11
|
+
const NestedSelectSheetHeader = ({ title, showBack, onClickBack }) => {
|
|
12
|
+
useExtendI18n(locales);
|
|
13
|
+
const { t } = useI18n();
|
|
14
|
+
if (!title)
|
|
15
|
+
return null;
|
|
16
|
+
return (React.createElement(React.Fragment, null,
|
|
17
|
+
React.createElement(BottomSheetHeader, { className: cx('u-h-3', { 'u-pl-half u-pr-3': showBack }) },
|
|
18
|
+
showBack && (React.createElement(IconButton, { "aria-label": t('Dialogs.backButton'), onClick: onClickBack },
|
|
19
|
+
React.createElement(Icon, { icon: ArrowUp, rotate: -90 }))),
|
|
20
|
+
React.createElement(Typography, { className: "u-w-100", variant: "h6", align: "center" }, title)),
|
|
21
|
+
React.createElement(Divider, null)));
|
|
22
|
+
};
|
|
23
|
+
/** NestedSelect in a bottom sheet with a backdrop */
|
|
24
|
+
const NestedSelectBottomSheet = ({ onClose, componentsProps, ...props }) => (React.createElement(BottomSheet, { ...componentsProps?.bottomsheet, backdrop: true, onClose: onClose },
|
|
25
|
+
React.createElement(BottomSheetItem, { disableGutters: true },
|
|
26
|
+
React.createElement(NestedSelect, { HeaderComponent: NestedSelectSheetHeader, ...props }))));
|
|
27
|
+
export default NestedSelectBottomSheet;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NestedSelectProps } from './types';
|
|
3
|
+
import { DialogLayoutProps } from '../Dialogs/useDialogLayout';
|
|
4
|
+
export interface NestedSelectModalProps extends NestedSelectProps {
|
|
5
|
+
onClose?: DialogLayoutProps['onClose'];
|
|
6
|
+
}
|
|
7
|
+
/** NestedSelect in a dialog, with a back button to the parent list */
|
|
8
|
+
declare const NestedSelectModal: ({ onClose, ...props }: NestedSelectModalProps) => React.ReactElement;
|
|
9
|
+
export default NestedSelectModal;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DialogContent, DialogTitle, List } from '@mui/material';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import React, { createContext, useContext } from 'react';
|
|
4
|
+
import NestedSelect from './index';
|
|
5
|
+
import Dialog from '../Dialog';
|
|
6
|
+
import DialogNavButtons from '../Dialogs/DialogNavButtons';
|
|
7
|
+
import { useDialogLayout } from '../Dialogs/useDialogLayout';
|
|
8
|
+
// NestedSelect renders the header and the content as components, so they read the layout here
|
|
9
|
+
const DialogLayoutContext = createContext(null);
|
|
10
|
+
const useDialogLayoutContext = () => {
|
|
11
|
+
const layout = useContext(DialogLayoutContext);
|
|
12
|
+
if (!layout)
|
|
13
|
+
throw new Error('Rendered outside NestedSelectModal');
|
|
14
|
+
return layout;
|
|
15
|
+
};
|
|
16
|
+
const NestedSelectDialogHeader = ({ title, showBack, onClickBack }) => {
|
|
17
|
+
const { id, fullScreen, onClose, dialogTitleProps } = useDialogLayoutContext();
|
|
18
|
+
return (React.createElement(React.Fragment, null,
|
|
19
|
+
React.createElement(DialogNavButtons, { id: id, fullScreen: fullScreen, onClose: onClose, onBack: showBack ? onClickBack : undefined }),
|
|
20
|
+
title && (React.createElement(DialogTitle, { ...dialogTitleProps, className: cx(dialogTitleProps.className, {
|
|
21
|
+
dialogTitleWithBack: showBack
|
|
22
|
+
}) }, title))));
|
|
23
|
+
};
|
|
24
|
+
const NestedSelectDialogContent = ({ children }) => {
|
|
25
|
+
const { dialogContentProps } = useDialogLayoutContext();
|
|
26
|
+
return (React.createElement(DialogContent, { ...dialogContentProps },
|
|
27
|
+
React.createElement(List, null, children)));
|
|
28
|
+
};
|
|
29
|
+
/** NestedSelect in a dialog, with a back button to the parent list */
|
|
30
|
+
const NestedSelectModal = ({ onClose, ...props }) => {
|
|
31
|
+
const layout = useDialogLayout({ open: true, onClose, disableGutters: true });
|
|
32
|
+
return (React.createElement(Dialog, { ...layout.dialogProps },
|
|
33
|
+
React.createElement(DialogLayoutContext.Provider, { value: { ...layout, onClose } },
|
|
34
|
+
React.createElement(NestedSelect, { HeaderComponent: NestedSelectDialogHeader, ContentComponent: NestedSelectDialogContent, ...props }))));
|
|
35
|
+
};
|
|
36
|
+
export default NestedSelectModal;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { NestedSelectBottomSheetProps } from './NestedSelectBottomSheet';
|
|
3
|
+
export type NestedSelectResponsiveProps = NestedSelectBottomSheetProps;
|
|
4
|
+
/** NestedSelect in a bottom sheet on mobile, in a dialog otherwise */
|
|
5
|
+
declare const NestedSelectResponsive: (props: NestedSelectResponsiveProps) => React.ReactElement;
|
|
6
|
+
export default NestedSelectResponsive;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import NestedSelectBottomSheet from './NestedSelectBottomSheet';
|
|
3
|
+
import NestedSelectModal from './NestedSelectModal';
|
|
4
|
+
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
5
|
+
/** NestedSelect in a bottom sheet on mobile, in a dialog otherwise */
|
|
6
|
+
const NestedSelectResponsive = (props) => {
|
|
7
|
+
const { isMobile } = useBreakpoints();
|
|
8
|
+
if (isMobile)
|
|
9
|
+
return React.createElement(NestedSelectBottomSheet, { ...props });
|
|
10
|
+
const { componentsProps: _componentsProps, ...modalProps } = props;
|
|
11
|
+
return React.createElement(NestedSelectModal, { ...modalProps });
|
|
12
|
+
};
|
|
13
|
+
export default NestedSelectResponsive;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { NestedSelectItem, NestedSelectOptions } from './types';
|
|
2
|
+
/** Path from the item matching `options.focusedId` up to `options`, deepest first */
|
|
3
|
+
export declare const makeHistory: (options: NestedSelectOptions | undefined, canSelectParent?: boolean) => NestedSelectItem[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Path from the item matching `options.focusedId` up to `options`, deepest first */
|
|
2
|
+
export const makeHistory = (options, canSelectParent = false) => {
|
|
3
|
+
if (!options)
|
|
4
|
+
return [];
|
|
5
|
+
const focusedId = options.focusedId ?? null;
|
|
6
|
+
if (!focusedId)
|
|
7
|
+
return [options];
|
|
8
|
+
const history = [];
|
|
9
|
+
const findFocusedItem = (item) => {
|
|
10
|
+
const isFound = item.children?.some(child => child.id === focusedId || findFocusedItem(child)) ?? false;
|
|
11
|
+
if (isFound) {
|
|
12
|
+
if (canSelectParent) {
|
|
13
|
+
const focusedItem = item.children?.find(child => child.id === focusedId);
|
|
14
|
+
if (focusedItem?.children)
|
|
15
|
+
history.push(focusedItem);
|
|
16
|
+
}
|
|
17
|
+
history.push(item);
|
|
18
|
+
}
|
|
19
|
+
return isFound;
|
|
20
|
+
};
|
|
21
|
+
return findFocusedItem(options) ? history : [options];
|
|
22
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NestedSelectProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Select to choose an item among a list. Clicking an item that has children
|
|
5
|
+
* shows its children instead of selecting it.
|
|
6
|
+
*/
|
|
7
|
+
declare const NestedSelect: ({ onSelect, isSelected, options, title, canSelectParent, transformParentItem, radioPosition, searchOptions, ellipsis, noDivider, ContentComponent, HeaderComponent }: NestedSelectProps) => React.ReactElement;
|
|
8
|
+
export default NestedSelect;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { List, Typography } from '@mui/material';
|
|
2
|
+
import { styled } from '@mui/material/styles';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
import { useDebounce } from 'rooks';
|
|
6
|
+
import ItemRow from './ItemRow';
|
|
7
|
+
import { makeHistory } from './helpers';
|
|
8
|
+
import SearchBar from '../SearchBar';
|
|
9
|
+
const SEARCH_DELAY = 250;
|
|
10
|
+
const NestedSelectRoot = styled('div', {
|
|
11
|
+
shouldForwardProp: prop => prop !== 'hasTitle'
|
|
12
|
+
})({
|
|
13
|
+
variants: [{ props: { hasTitle: false }, style: { marginTop: '4rem' } }]
|
|
14
|
+
});
|
|
15
|
+
const keepItem = (item) => item;
|
|
16
|
+
const getItemKey = (item, index) => item.key ?? (typeof item.title === 'string' ? item.title : index);
|
|
17
|
+
/**
|
|
18
|
+
* Select to choose an item among a list. Clicking an item that has children
|
|
19
|
+
* shows its children instead of selecting it.
|
|
20
|
+
*/
|
|
21
|
+
const NestedSelect = ({ onSelect, isSelected, options, title, canSelectParent, transformParentItem = keepItem, radioPosition = 'right', searchOptions, ellipsis, noDivider, ContentComponent = List, HeaderComponent = null }) => {
|
|
22
|
+
const [history, setHistory] = useState(() => makeHistory(options, canSelectParent));
|
|
23
|
+
const [searchValue, setSearchValue] = useState('');
|
|
24
|
+
const [searchResult, setSearchResult] = useState(null);
|
|
25
|
+
const setSearchResultLater = useDebounce(setSearchResult, SEARCH_DELAY);
|
|
26
|
+
const current = history[0];
|
|
27
|
+
const level = history.length - 1;
|
|
28
|
+
const currentTitle = current.title ?? title;
|
|
29
|
+
const { children: _children, ...currentWithoutChildren } = current;
|
|
30
|
+
const parentItem = transformParentItem(currentWithoutChildren);
|
|
31
|
+
const items = searchResult ?? current.children ?? [];
|
|
32
|
+
const handleBack = () => {
|
|
33
|
+
setHistory(([, ...parents]) => parents);
|
|
34
|
+
};
|
|
35
|
+
const handleClickItem = (item) => {
|
|
36
|
+
if (item.children && item.children.length > 0) {
|
|
37
|
+
setHistory(previous => [item, ...previous]);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
onSelect(item);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const handleSearchChange = (event) => {
|
|
44
|
+
if (!searchOptions)
|
|
45
|
+
return;
|
|
46
|
+
setSearchValue(event.target.value);
|
|
47
|
+
setSearchResultLater(searchOptions.onSearch(event.target.value));
|
|
48
|
+
};
|
|
49
|
+
const handleSearchClear = () => {
|
|
50
|
+
setSearchValue('');
|
|
51
|
+
setSearchResultLater(null);
|
|
52
|
+
};
|
|
53
|
+
const renderItemRow = (item, key, isLast) => (React.createElement(ItemRow, { key: key, item: item, onClick: handleClickItem, isSelected: isSelected(item, level), radioPosition: radioPosition, isLast: isLast, ellipsis: ellipsis, noDivider: noDivider }));
|
|
54
|
+
const header = level === 0
|
|
55
|
+
? options.header
|
|
56
|
+
: (current.header ??
|
|
57
|
+
(typeof options.childrenHeader === 'function'
|
|
58
|
+
? options.childrenHeader(level)
|
|
59
|
+
: options.childrenHeader));
|
|
60
|
+
return (React.createElement(NestedSelectRoot, { hasTitle: Boolean(currentTitle) },
|
|
61
|
+
HeaderComponent && (React.createElement(HeaderComponent, { title: currentTitle, showBack: level > 0, onClickBack: handleBack })),
|
|
62
|
+
header,
|
|
63
|
+
React.createElement(ContentComponent, null,
|
|
64
|
+
canSelectParent &&
|
|
65
|
+
level > 0 &&
|
|
66
|
+
renderItemRow(parentItem, 'parent', false),
|
|
67
|
+
searchOptions && level === 0 && (React.createElement("div", { className: cx('u-mh-1 u-mb-half', { 'u-mt-half': !currentTitle }) },
|
|
68
|
+
React.createElement(SearchBar, { placeholder: searchOptions.placeholderSearch, value: searchValue, onChange: handleSearchChange, onClear: handleSearchClear }))),
|
|
69
|
+
searchResult?.length === 0 ? (React.createElement(Typography, { variant: "body1", className: "u-flex u-flex-justify-center u-mb-1" }, searchOptions?.noDataLabel)) : (items.map((item, index) => renderItemRow(item, getItemKey(item, index), index === items.length - 1))))));
|
|
70
|
+
};
|
|
71
|
+
export default NestedSelect;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
export interface NestedSelectItemAction {
|
|
3
|
+
/**
|
|
4
|
+
* Called as a function with the item and `props`, so that a returned
|
|
5
|
+
* `ListItemSecondaryAction` is placed outside the row button
|
|
6
|
+
*/
|
|
7
|
+
Component(props: {
|
|
8
|
+
item: NestedSelectItem;
|
|
9
|
+
}): React.ReactNode;
|
|
10
|
+
props?: object;
|
|
11
|
+
}
|
|
12
|
+
export interface NestedSelectItem {
|
|
13
|
+
/** Matched against `focusedId` to open the select on this item */
|
|
14
|
+
id?: string;
|
|
15
|
+
/** React key of the row, `title` is used without it */
|
|
16
|
+
key?: string;
|
|
17
|
+
title?: React.ReactNode;
|
|
18
|
+
description?: React.ReactNode;
|
|
19
|
+
/** Shown on the left of the row */
|
|
20
|
+
icon?: React.ReactElement;
|
|
21
|
+
/** Shown before the radio */
|
|
22
|
+
info?: React.ReactNode;
|
|
23
|
+
/** Shown above the list of this item's children */
|
|
24
|
+
header?: React.ReactNode;
|
|
25
|
+
/** Items shown instead of selecting this one */
|
|
26
|
+
children?: NestedSelectItem[];
|
|
27
|
+
/** Shown on the right of the row */
|
|
28
|
+
action?: NestedSelectItemAction;
|
|
29
|
+
}
|
|
30
|
+
export interface NestedSelectOptions extends NestedSelectItem {
|
|
31
|
+
/** Opens the select on the item with this `id` */
|
|
32
|
+
focusedId?: string | null;
|
|
33
|
+
/** Shown above the children of any item, unless the item has its own `header` */
|
|
34
|
+
childrenHeader?: React.ReactNode | ((level: number) => React.ReactNode);
|
|
35
|
+
}
|
|
36
|
+
export interface NestedSelectSearchOptions {
|
|
37
|
+
placeholderSearch: string;
|
|
38
|
+
noDataLabel: string;
|
|
39
|
+
onSearch: (value: string) => NestedSelectItem[];
|
|
40
|
+
}
|
|
41
|
+
export interface NestedSelectHeaderProps {
|
|
42
|
+
title?: React.ReactNode;
|
|
43
|
+
showBack: boolean;
|
|
44
|
+
onClickBack: () => void;
|
|
45
|
+
}
|
|
46
|
+
export type NestedSelectRadioPosition = 'left' | 'right';
|
|
47
|
+
export interface NestedSelectProps {
|
|
48
|
+
/** Called with the chosen item, unless it has children */
|
|
49
|
+
onSelect: (item: NestedSelectItem) => void;
|
|
50
|
+
/** Whether the row looks selected, `level` being the depth of the shown list */
|
|
51
|
+
isSelected: (item: NestedSelectItem, level: number) => boolean;
|
|
52
|
+
options: NestedSelectOptions;
|
|
53
|
+
/** Shown when the current item has no title */
|
|
54
|
+
title?: React.ReactNode;
|
|
55
|
+
/** Shows the parent item at the top of its children, to select it */
|
|
56
|
+
canSelectParent?: boolean;
|
|
57
|
+
/** Changes the parent item before it is shown at the top of its children */
|
|
58
|
+
transformParentItem?: (item: NestedSelectItem) => NestedSelectItem;
|
|
59
|
+
radioPosition?: NestedSelectRadioPosition;
|
|
60
|
+
/** Shows a search bar above the first level */
|
|
61
|
+
searchOptions?: NestedSelectSearchOptions;
|
|
62
|
+
/** Ellipses the row texts, `true` by default */
|
|
63
|
+
ellipsis?: boolean;
|
|
64
|
+
/** Removes the divider between rows */
|
|
65
|
+
noDivider?: boolean;
|
|
66
|
+
/** Wraps the rows */
|
|
67
|
+
ContentComponent?: React.ElementType<{
|
|
68
|
+
children?: React.ReactNode;
|
|
69
|
+
}>;
|
|
70
|
+
/** Shows the title and the back button */
|
|
71
|
+
HeaderComponent?: React.ComponentType<NestedSelectHeaderProps> | null;
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -73,11 +73,17 @@ export { default as MidEllipsis } from './components/MidEllipsis';
|
|
|
73
73
|
export { default as NavigationList } from './components/NavigationList';
|
|
74
74
|
export { default as NavigationListHeader } from './components/NavigationList/NavigationListHeader';
|
|
75
75
|
export { default as NavigationListSection } from './components/NavigationList/NavigationListSection';
|
|
76
|
+
export { default as NestedSelect } from './components/NestedSelect';
|
|
77
|
+
export { default as NestedSelectModal } from './components/NestedSelect/NestedSelectModal';
|
|
78
|
+
export { default as NestedSelectBottomSheet } from './components/NestedSelect/NestedSelectBottomSheet';
|
|
79
|
+
export { default as NestedSelectResponsive } from './components/NestedSelect/NestedSelectResponsive';
|
|
76
80
|
export { default as PointerAlert } from './components/PointerAlert';
|
|
77
|
-
export type { AlertProviderProps } from './providers/AlertProvider';
|
|
78
|
-
export type { AlertContextValue, ShowAlert, ShowAlertOptions } from './providers/AlertProvider/useAlert';
|
|
79
81
|
export { default as AlertProvider } from './providers/AlertProvider';
|
|
80
82
|
export { useAlert } from './providers/AlertProvider/useAlert';
|
|
83
|
+
export { default as ConfirmDialogProvider } from './providers/ConfirmDialogProvider';
|
|
84
|
+
export { useConfirmDialog } from './providers/ConfirmDialogProvider/useConfirmDialog';
|
|
85
|
+
export { default as SelectionProvider } from './providers/SelectionProvider';
|
|
86
|
+
export { useSelection } from './providers/SelectionProvider/useSelection';
|
|
81
87
|
export { default as ProgressionBanner } from './components/ProgressionBanner';
|
|
82
88
|
export { default as Tabs } from './components/Tabs';
|
|
83
89
|
export { default as Switch } from './components/Switch';
|
|
@@ -156,6 +162,12 @@ export type { DialogCloseButtonProps } from './components/Dialogs/DialogCloseBut
|
|
|
156
162
|
export type { DialogBackButtonProps } from './components/Dialogs/DialogBackButton';
|
|
157
163
|
export type { DialogCloseReason, DialogLayout, DialogLayoutComponentsProps, DialogLayoutProps } from './components/Dialogs/useDialogLayout';
|
|
158
164
|
export type { PointerAlertProps, PointerAlertDirection } from './components/PointerAlert';
|
|
165
|
+
export type { AlertProviderProps } from './providers/AlertProvider';
|
|
166
|
+
export type { AlertContextValue, ShowAlert, ShowAlertOptions } from './providers/AlertProvider/useAlert';
|
|
167
|
+
export type { ConfirmDialogProviderProps } from './providers/ConfirmDialogProvider';
|
|
168
|
+
export type { ConfirmDialogContextValue, ShowConfirmDialogOptions } from './providers/ConfirmDialogProvider/useConfirmDialog';
|
|
169
|
+
export type { SelectionProviderProps } from './providers/SelectionProvider';
|
|
170
|
+
export type { SelectionContextValue } from './providers/SelectionProvider/useSelection';
|
|
159
171
|
export type { ProgressionBannerProps } from './components/ProgressionBanner';
|
|
160
172
|
export type { EmptyProps, EmptyIconSize, EmptyComponentsProps } from './components/Empty';
|
|
161
173
|
export type { DropdownButtonProps } from './components/DropdownButton';
|
|
@@ -176,6 +188,10 @@ export type { MidEllipsisProps } from './components/MidEllipsis';
|
|
|
176
188
|
export type { NavigationListProps } from './components/NavigationList';
|
|
177
189
|
export type { NavigationListHeaderProps } from './components/NavigationList/NavigationListHeader';
|
|
178
190
|
export type { NavigationListSectionProps } from './components/NavigationList/NavigationListSection';
|
|
191
|
+
export type { NestedSelectHeaderProps, NestedSelectItem, NestedSelectItemAction, NestedSelectOptions, NestedSelectProps, NestedSelectRadioPosition, NestedSelectSearchOptions } from './components/NestedSelect/types';
|
|
192
|
+
export type { NestedSelectModalProps } from './components/NestedSelect/NestedSelectModal';
|
|
193
|
+
export type { NestedSelectBottomSheetProps } from './components/NestedSelect/NestedSelectBottomSheet';
|
|
194
|
+
export type { NestedSelectResponsiveProps } from './components/NestedSelect/NestedSelectResponsive';
|
|
179
195
|
export type { TabsProps } from './components/Tabs';
|
|
180
196
|
export type { SwitchProps } from './components/Switch';
|
|
181
197
|
export type { SearchBarProps } from './components/SearchBar/types';
|
package/dist/index.js
CHANGED
|
@@ -76,9 +76,17 @@ export { default as MidEllipsis } from './components/MidEllipsis';
|
|
|
76
76
|
export { default as NavigationList } from './components/NavigationList';
|
|
77
77
|
export { default as NavigationListHeader } from './components/NavigationList/NavigationListHeader';
|
|
78
78
|
export { default as NavigationListSection } from './components/NavigationList/NavigationListSection';
|
|
79
|
+
export { default as NestedSelect } from './components/NestedSelect';
|
|
80
|
+
export { default as NestedSelectModal } from './components/NestedSelect/NestedSelectModal';
|
|
81
|
+
export { default as NestedSelectBottomSheet } from './components/NestedSelect/NestedSelectBottomSheet';
|
|
82
|
+
export { default as NestedSelectResponsive } from './components/NestedSelect/NestedSelectResponsive';
|
|
79
83
|
export { default as PointerAlert } from './components/PointerAlert';
|
|
80
84
|
export { default as AlertProvider } from './providers/AlertProvider';
|
|
81
85
|
export { useAlert } from './providers/AlertProvider/useAlert';
|
|
86
|
+
export { default as ConfirmDialogProvider } from './providers/ConfirmDialogProvider';
|
|
87
|
+
export { useConfirmDialog } from './providers/ConfirmDialogProvider/useConfirmDialog';
|
|
88
|
+
export { default as SelectionProvider } from './providers/SelectionProvider';
|
|
89
|
+
export { useSelection } from './providers/SelectionProvider/useSelection';
|
|
82
90
|
export { default as ProgressionBanner } from './components/ProgressionBanner';
|
|
83
91
|
export { default as Tabs } from './components/Tabs';
|
|
84
92
|
export { default as Switch } from './components/Switch';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ShowConfirmDialogOptions } from './useConfirmDialog';
|
|
3
|
+
type StoryArgs = Pick<ShowConfirmDialogOptions, 'title' | 'content'>;
|
|
4
|
+
declare const meta: Meta<StoryArgs>;
|
|
5
|
+
export default meta;
|
|
6
|
+
type Story = StoryObj<typeof meta>;
|
|
7
|
+
export declare const Default: Story;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Button } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import ConfirmDialogProvider from './index';
|
|
4
|
+
import { useConfirmDialog } from './useConfirmDialog';
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'ConfirmDialogProvider',
|
|
7
|
+
parameters: { layout: 'centered' },
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
title: { control: 'text' },
|
|
11
|
+
content: { control: 'text' }
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export default meta;
|
|
15
|
+
const ShowConfirmDialogButton = ({ args }) => {
|
|
16
|
+
const { showConfirmDialog, closeConfirmDialog } = useConfirmDialog();
|
|
17
|
+
return (React.createElement(Button, { onClick: () => showConfirmDialog({
|
|
18
|
+
...args,
|
|
19
|
+
actions: (React.createElement(React.Fragment, null,
|
|
20
|
+
React.createElement(Button, { variant: "outlined", onClick: closeConfirmDialog }, "Cancel"),
|
|
21
|
+
React.createElement(Button, null, "Ok")))
|
|
22
|
+
}) }, "Show ConfirmDialog"));
|
|
23
|
+
};
|
|
24
|
+
export const Default = {
|
|
25
|
+
args: {
|
|
26
|
+
title: 'Title of the ConfirmDialog',
|
|
27
|
+
content: 'Content of the ConfirmDialog'
|
|
28
|
+
},
|
|
29
|
+
render: args => (React.createElement(ConfirmDialogProvider, null,
|
|
30
|
+
React.createElement(ShowConfirmDialogButton, { args: args })))
|
|
31
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { ConfirmDialogContext } from './useConfirmDialog';
|
|
3
|
+
import ConfirmDialog from '../../components/Dialogs/ConfirmDialog';
|
|
4
|
+
const ConfirmDialogProvider = ({ children }) => {
|
|
5
|
+
const [state, setState] = useState({ open: false });
|
|
6
|
+
const { open, ...confirmDialogProps } = state;
|
|
7
|
+
const value = useMemo(() => ({
|
|
8
|
+
showConfirmDialog: (options) => setState({ ...options, open: true }),
|
|
9
|
+
closeConfirmDialog: () => setState(current => ({ ...current, open: false }))
|
|
10
|
+
}), []);
|
|
11
|
+
return (React.createElement(ConfirmDialogContext.Provider, { value: value },
|
|
12
|
+
children,
|
|
13
|
+
open && (React.createElement(ConfirmDialog, { open: true, onClose: value.closeConfirmDialog, ...confirmDialogProps }))));
|
|
14
|
+
};
|
|
15
|
+
export default ConfirmDialogProvider;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfirmDialogProps } from '../../components/Dialogs/ConfirmDialog';
|
|
2
|
+
export type ShowConfirmDialogOptions = Omit<ConfirmDialogProps, 'open'>;
|
|
3
|
+
export interface ConfirmDialogContextValue {
|
|
4
|
+
showConfirmDialog: (options: ShowConfirmDialogOptions) => void;
|
|
5
|
+
closeConfirmDialog: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare const ConfirmDialogContext: import("react").Context<ConfirmDialogContextValue | null>;
|
|
8
|
+
export declare const useConfirmDialog: () => ConfirmDialogContextValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const ConfirmDialogContext = createContext(null);
|
|
3
|
+
export const useConfirmDialog = () => {
|
|
4
|
+
const context = useContext(ConfirmDialogContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('useConfirmDialog must be used within a ConfirmDialogProvider');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Button, Stack, Typography } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import SelectionProvider from './index';
|
|
4
|
+
import { useSelection } from './useSelection';
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'SelectionProvider',
|
|
7
|
+
component: SelectionProvider,
|
|
8
|
+
parameters: { layout: 'centered' },
|
|
9
|
+
tags: ['autodocs']
|
|
10
|
+
};
|
|
11
|
+
export default meta;
|
|
12
|
+
const SelectionExample = () => {
|
|
13
|
+
const { selectedItemsId, addSelectedItem, removeSelectedItem } = useSelection();
|
|
14
|
+
return (React.createElement(Stack, { spacing: 1 },
|
|
15
|
+
React.createElement(Typography, null,
|
|
16
|
+
"selectedItemsId : ",
|
|
17
|
+
JSON.stringify(selectedItemsId)),
|
|
18
|
+
React.createElement(Stack, { direction: "row", spacing: 1 },
|
|
19
|
+
React.createElement(Button, { onClick: () => addSelectedItem('01') }, "Add Item"),
|
|
20
|
+
React.createElement(Button, { onClick: () => removeSelectedItem('01') }, "Remove Item"))));
|
|
21
|
+
};
|
|
22
|
+
export const Default = {
|
|
23
|
+
render: () => (React.createElement(SelectionProvider, null,
|
|
24
|
+
React.createElement(SelectionExample, null)))
|
|
25
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
export interface SelectionProviderProps {
|
|
3
|
+
children?: ReactNode;
|
|
4
|
+
}
|
|
5
|
+
/** Shares the ids of the selected items with its children */
|
|
6
|
+
declare const SelectionProvider: React.FC<SelectionProviderProps>;
|
|
7
|
+
export default SelectionProvider;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { SelectionContext } from './useSelection';
|
|
3
|
+
/** Shares the ids of the selected items with its children */
|
|
4
|
+
const SelectionProvider = ({ children }) => {
|
|
5
|
+
const [selectedItemsId, setSelectedItemsId] = useState([]);
|
|
6
|
+
const isSelectedItem = (itemId) => selectedItemsId.includes(itemId);
|
|
7
|
+
const addSelectedItem = (itemId) => setSelectedItemsId(current => [...current, itemId]);
|
|
8
|
+
const removeSelectedItem = (itemId) => setSelectedItemsId(current => current.filter(id => id !== itemId));
|
|
9
|
+
const toggleSelectedItem = (itemId) => isSelectedItem(itemId)
|
|
10
|
+
? removeSelectedItem(itemId)
|
|
11
|
+
: addSelectedItem(itemId);
|
|
12
|
+
const selectAll = (itemsId) => setSelectedItemsId(itemsId);
|
|
13
|
+
const unselectAll = () => setSelectedItemsId([]);
|
|
14
|
+
const isSelectedAllItems = (itemsId) => itemsId.length === selectedItemsId.length && itemsId.every(isSelectedItem);
|
|
15
|
+
const toggleSelectAllItems = (itemsId) => isSelectedAllItems(itemsId) ? unselectAll() : selectAll(itemsId);
|
|
16
|
+
return (React.createElement(SelectionContext.Provider, { value: {
|
|
17
|
+
selectedItemsId,
|
|
18
|
+
addSelectedItem,
|
|
19
|
+
removeSelectedItem,
|
|
20
|
+
toggleSelectedItem,
|
|
21
|
+
selectAll,
|
|
22
|
+
toggleSelectAllItems,
|
|
23
|
+
isSelectedItem,
|
|
24
|
+
isSelectionEnabled: selectedItemsId.length > 0,
|
|
25
|
+
isSelectedAllItems
|
|
26
|
+
} }, children));
|
|
27
|
+
};
|
|
28
|
+
export default SelectionProvider;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SelectionContextValue {
|
|
2
|
+
selectedItemsId: string[];
|
|
3
|
+
addSelectedItem: (itemId: string) => void;
|
|
4
|
+
removeSelectedItem: (itemId: string) => void;
|
|
5
|
+
toggleSelectedItem: (itemId: string) => void;
|
|
6
|
+
selectAll: (itemsId: string[]) => void;
|
|
7
|
+
toggleSelectAllItems: (itemsId: string[]) => void;
|
|
8
|
+
isSelectedItem: (itemId: string) => boolean;
|
|
9
|
+
isSelectionEnabled: boolean;
|
|
10
|
+
isSelectedAllItems: (itemsId: string[]) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const SelectionContext: import("react").Context<SelectionContextValue | null>;
|
|
13
|
+
export declare const useSelection: () => SelectionContextValue;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const SelectionContext = createContext(null);
|
|
3
|
+
export const useSelection = () => {
|
|
4
|
+
const context = useContext(SelectionContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('useSelection must be used within a SelectionProvider');
|
|
7
|
+
}
|
|
8
|
+
return context;
|
|
9
|
+
};
|