@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/components/ActionsMenu/Actions/download.d.ts +1 -3
  3. package/dist/components/ActionsMenu/Actions/download.js +1 -2
  4. package/dist/components/NestedSelect/ItemRow.d.ts +13 -0
  5. package/dist/components/NestedSelect/ItemRow.js +28 -0
  6. package/dist/components/NestedSelect/NestedSelect.stories.d.ts +19 -0
  7. package/dist/components/NestedSelect/NestedSelect.stories.js +149 -0
  8. package/dist/components/NestedSelect/NestedSelectBottomSheet.d.ts +12 -0
  9. package/dist/components/NestedSelect/NestedSelectBottomSheet.js +27 -0
  10. package/dist/components/NestedSelect/NestedSelectModal.d.ts +9 -0
  11. package/dist/components/NestedSelect/NestedSelectModal.js +36 -0
  12. package/dist/components/NestedSelect/NestedSelectResponsive.d.ts +6 -0
  13. package/dist/components/NestedSelect/NestedSelectResponsive.js +13 -0
  14. package/dist/components/NestedSelect/helpers.d.ts +3 -0
  15. package/dist/components/NestedSelect/helpers.js +22 -0
  16. package/dist/components/NestedSelect/index.d.ts +8 -0
  17. package/dist/components/NestedSelect/index.js +71 -0
  18. package/dist/components/NestedSelect/types.d.ts +72 -0
  19. package/dist/components/NestedSelect/types.js +1 -0
  20. package/dist/index.d.ts +18 -2
  21. package/dist/index.js +8 -0
  22. package/dist/providers/ConfirmDialogProvider/ConfirmDialogProvider.stories.d.ts +7 -0
  23. package/dist/providers/ConfirmDialogProvider/ConfirmDialogProvider.stories.js +31 -0
  24. package/dist/providers/ConfirmDialogProvider/index.d.ts +6 -0
  25. package/dist/providers/ConfirmDialogProvider/index.js +15 -0
  26. package/dist/providers/ConfirmDialogProvider/useConfirmDialog.d.ts +8 -0
  27. package/dist/providers/ConfirmDialogProvider/useConfirmDialog.js +9 -0
  28. package/dist/providers/SelectionProvider/SelectionProvider.stories.d.ts +6 -0
  29. package/dist/providers/SelectionProvider/SelectionProvider.stories.js +25 -0
  30. package/dist/providers/SelectionProvider/index.d.ts +7 -0
  31. package/dist/providers/SelectionProvider/index.js +28 -0
  32. package/dist/providers/SelectionProvider/useSelection.d.ts +13 -0
  33. package/dist/providers/SelectionProvider/useSelection.js +9 -0
  34. 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, encryptedUrl, downloadFile }: DownloadActionOptions<Client>) => Action;
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, encryptedUrl, downloadFile }) => {
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,6 @@
1
+ import React, { ReactNode } from 'react';
2
+ export interface ConfirmDialogProviderProps {
3
+ children?: ReactNode;
4
+ }
5
+ declare const ConfirmDialogProvider: React.FC<ConfirmDialogProviderProps>;
6
+ export default ConfirmDialogProvider;
@@ -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,6 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import SelectionProvider from './index';
3
+ declare const meta: Meta<typeof SelectionProvider>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Default: Story;
@@ -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
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linagora/twake-mui",
3
3
  "type": "module",
4
- "version": "17.0.0",
4
+ "version": "18.0.0",
5
5
  "description": "MUI v9 theme system for Twake applications",
6
6
  "author": "Twake",
7
7
  "license": "MIT",