@axinom/mosaic-ui 0.75.0-rc.5 → 0.77.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 (183) hide show
  1. package/dist/THIRD-PARTY-LICENSES.txt +1296 -0
  2. package/dist/cjs/components/ActionBar/ActionBar.d.ts +26 -0
  3. package/dist/cjs/components/ActionBar/ActionBar.d.ts.map +1 -0
  4. package/dist/cjs/components/ActionBar/ActionBar.model.d.ts +49 -0
  5. package/dist/cjs/components/ActionBar/ActionBar.model.d.ts.map +1 -0
  6. package/dist/cjs/components/ActionBar/ActionBarButton/ActionBarButton.d.ts +22 -0
  7. package/dist/cjs/components/ActionBar/ActionBarButton/ActionBarButton.d.ts.map +1 -0
  8. package/dist/cjs/components/ActionBar/helpers.d.ts +22 -0
  9. package/dist/cjs/components/ActionBar/helpers.d.ts.map +1 -0
  10. package/dist/cjs/components/ActionBar/index.d.ts +3 -0
  11. package/dist/cjs/components/ActionBar/index.d.ts.map +1 -0
  12. package/dist/cjs/components/Actions/Action/Action.d.ts.map +1 -1
  13. package/dist/cjs/components/ConfirmDialog/index.d.ts +1 -0
  14. package/dist/cjs/components/ConfirmDialog/index.d.ts.map +1 -1
  15. package/dist/cjs/components/ConfirmDialog/useConfirmableAction.d.ts +55 -0
  16. package/dist/cjs/components/ConfirmDialog/useConfirmableAction.d.ts.map +1 -0
  17. package/dist/cjs/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
  18. package/dist/cjs/components/Explorer/Explorer.d.ts.map +1 -1
  19. package/dist/cjs/components/Explorer/QuickEdit/useQuickEdit.d.ts +14 -3
  20. package/dist/cjs/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -1
  21. package/dist/cjs/components/Explorer/helpers/useActions.d.ts +15 -4
  22. package/dist/cjs/components/Explorer/helpers/useActions.d.ts.map +1 -1
  23. package/dist/cjs/components/Explorer/helpers/useSubtitle.d.ts.map +1 -1
  24. package/dist/cjs/components/List/List.d.ts +17 -3
  25. package/dist/cjs/components/List/List.d.ts.map +1 -1
  26. package/dist/cjs/components/List/List.model.d.ts +5 -1
  27. package/dist/cjs/components/List/List.model.d.ts.map +1 -1
  28. package/dist/cjs/components/List/ListHeader/ListHeader.d.ts +10 -1
  29. package/dist/cjs/components/List/ListHeader/ListHeader.d.ts.map +1 -1
  30. package/dist/cjs/components/List/ListRow/ListRow.d.ts +16 -5
  31. package/dist/cjs/components/List/ListRow/ListRow.d.ts.map +1 -1
  32. package/dist/cjs/components/List/ListRowRenderer/ListRowRenderer.d.ts.map +1 -1
  33. package/dist/cjs/components/List/helpers.d.ts +11 -3
  34. package/dist/cjs/components/List/helpers.d.ts.map +1 -1
  35. package/dist/cjs/components/List/useColumnsSize.d.ts +4 -2
  36. package/dist/cjs/components/List/useColumnsSize.d.ts.map +1 -1
  37. package/dist/cjs/components/PageHeader/PageHeader.d.ts +4 -1
  38. package/dist/cjs/components/PageHeader/PageHeader.d.ts.map +1 -1
  39. package/dist/cjs/components/PageHeader/PageHeader.model.d.ts +37 -1
  40. package/dist/cjs/components/PageHeader/PageHeader.model.d.ts.map +1 -1
  41. package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts +3 -0
  42. package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  43. package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +8 -0
  44. package/dist/cjs/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
  45. package/dist/cjs/components/PageHeader/helpers.d.ts +9 -0
  46. package/dist/cjs/components/PageHeader/helpers.d.ts.map +1 -0
  47. package/dist/cjs/components/index.d.ts +1 -0
  48. package/dist/cjs/components/index.d.ts.map +1 -1
  49. package/dist/cjs/hooks/index.d.ts +1 -0
  50. package/dist/cjs/hooks/index.d.ts.map +1 -1
  51. package/dist/cjs/hooks/useElementWidthObserver/useElementWidthObserver.d.ts.map +1 -0
  52. package/dist/cjs/index.js +4 -4
  53. package/dist/cjs/index.js.map +1 -1
  54. package/dist/esm/components/ActionBar/ActionBar.d.ts +26 -0
  55. package/dist/esm/components/ActionBar/ActionBar.d.ts.map +1 -0
  56. package/dist/esm/components/ActionBar/ActionBar.model.d.ts +49 -0
  57. package/dist/esm/components/ActionBar/ActionBar.model.d.ts.map +1 -0
  58. package/dist/esm/components/ActionBar/ActionBarButton/ActionBarButton.d.ts +22 -0
  59. package/dist/esm/components/ActionBar/ActionBarButton/ActionBarButton.d.ts.map +1 -0
  60. package/dist/esm/components/ActionBar/helpers.d.ts +22 -0
  61. package/dist/esm/components/ActionBar/helpers.d.ts.map +1 -0
  62. package/dist/esm/components/ActionBar/index.d.ts +3 -0
  63. package/dist/esm/components/ActionBar/index.d.ts.map +1 -0
  64. package/dist/esm/components/Actions/Action/Action.d.ts.map +1 -1
  65. package/dist/esm/components/ConfirmDialog/index.d.ts +1 -0
  66. package/dist/esm/components/ConfirmDialog/index.d.ts.map +1 -1
  67. package/dist/esm/components/ConfirmDialog/useConfirmableAction.d.ts +55 -0
  68. package/dist/esm/components/ConfirmDialog/useConfirmableAction.d.ts.map +1 -0
  69. package/dist/esm/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
  70. package/dist/esm/components/Explorer/Explorer.d.ts.map +1 -1
  71. package/dist/esm/components/Explorer/QuickEdit/useQuickEdit.d.ts +14 -3
  72. package/dist/esm/components/Explorer/QuickEdit/useQuickEdit.d.ts.map +1 -1
  73. package/dist/esm/components/Explorer/helpers/useActions.d.ts +15 -4
  74. package/dist/esm/components/Explorer/helpers/useActions.d.ts.map +1 -1
  75. package/dist/esm/components/Explorer/helpers/useSubtitle.d.ts.map +1 -1
  76. package/dist/esm/components/List/List.d.ts +17 -3
  77. package/dist/esm/components/List/List.d.ts.map +1 -1
  78. package/dist/esm/components/List/List.model.d.ts +5 -1
  79. package/dist/esm/components/List/List.model.d.ts.map +1 -1
  80. package/dist/esm/components/List/ListHeader/ListHeader.d.ts +10 -1
  81. package/dist/esm/components/List/ListHeader/ListHeader.d.ts.map +1 -1
  82. package/dist/esm/components/List/ListRow/ListRow.d.ts +16 -5
  83. package/dist/esm/components/List/ListRow/ListRow.d.ts.map +1 -1
  84. package/dist/esm/components/List/ListRowRenderer/ListRowRenderer.d.ts.map +1 -1
  85. package/dist/esm/components/List/helpers.d.ts +11 -3
  86. package/dist/esm/components/List/helpers.d.ts.map +1 -1
  87. package/dist/esm/components/List/useColumnsSize.d.ts +4 -2
  88. package/dist/esm/components/List/useColumnsSize.d.ts.map +1 -1
  89. package/dist/esm/components/PageHeader/PageHeader.d.ts +4 -1
  90. package/dist/esm/components/PageHeader/PageHeader.d.ts.map +1 -1
  91. package/dist/esm/components/PageHeader/PageHeader.model.d.ts +37 -1
  92. package/dist/esm/components/PageHeader/PageHeader.model.d.ts.map +1 -1
  93. package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts +3 -0
  94. package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  95. package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +8 -0
  96. package/dist/esm/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
  97. package/dist/esm/components/PageHeader/helpers.d.ts +9 -0
  98. package/dist/esm/components/PageHeader/helpers.d.ts.map +1 -0
  99. package/dist/esm/components/index.d.ts +1 -0
  100. package/dist/esm/components/index.d.ts.map +1 -1
  101. package/dist/esm/hooks/index.d.ts +1 -0
  102. package/dist/esm/hooks/index.d.ts.map +1 -1
  103. package/dist/esm/hooks/useElementWidthObserver/useElementWidthObserver.d.ts.map +1 -0
  104. package/dist/esm/index.js +4 -4
  105. package/dist/esm/index.js.map +1 -1
  106. package/package.json +7 -3
  107. package/src/components/ActionBar/ActionBar.model.ts +61 -0
  108. package/src/components/ActionBar/ActionBar.module.scss +64 -0
  109. package/src/components/ActionBar/ActionBar.spec.tsx +398 -0
  110. package/src/components/ActionBar/ActionBar.stories.tsx +205 -0
  111. package/src/components/ActionBar/ActionBar.tsx +170 -0
  112. package/src/components/ActionBar/ActionBarButton/ActionBarButton.module.scss +140 -0
  113. package/src/components/ActionBar/ActionBarButton/ActionBarButton.tsx +193 -0
  114. package/src/components/ActionBar/helpers.spec.ts +41 -0
  115. package/src/components/ActionBar/helpers.ts +54 -0
  116. package/src/components/ActionBar/index.ts +2 -0
  117. package/src/components/Actions/Action/Action.spec.tsx +64 -2
  118. package/src/components/Actions/Action/Action.tsx +6 -1
  119. package/src/components/ConfirmDialog/index.ts +1 -0
  120. package/src/components/ConfirmDialog/useConfirmableAction.tsx +159 -0
  121. package/src/components/EmptyStation/EmptyStation.stories.tsx +97 -0
  122. package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +2 -0
  123. package/src/components/Explorer/Explorer.spec.tsx +97 -8
  124. package/src/components/Explorer/Explorer.tsx +26 -13
  125. package/src/components/Explorer/NavigationExplorer/NavigationExplorer.stories.tsx +124 -1
  126. package/src/components/Explorer/QuickEdit/useQuickEdit.spec.tsx +94 -50
  127. package/src/components/Explorer/QuickEdit/useQuickEdit.tsx +101 -42
  128. package/src/components/Explorer/SelectionExplorer/SelectionExplorer.spec.tsx +46 -13
  129. package/src/components/Explorer/helpers/useActions.ts +125 -73
  130. package/src/components/Explorer/helpers/useSubtitle.spec.tsx +33 -23
  131. package/src/components/Explorer/helpers/useSubtitle.tsx +18 -26
  132. package/src/components/List/List.model.ts +5 -1
  133. package/src/components/List/List.module.scss +25 -6
  134. package/src/components/List/List.spec.tsx +498 -1
  135. package/src/components/List/List.stories.tsx +137 -8
  136. package/src/components/List/List.tsx +305 -194
  137. package/src/components/List/ListHeader/ListHeader.module.scss +14 -0
  138. package/src/components/List/ListHeader/ListHeader.spec.tsx +1 -0
  139. package/src/components/List/ListHeader/ListHeader.tsx +25 -8
  140. package/src/components/List/ListRow/ListRow.module.scss +1 -1
  141. package/src/components/List/ListRow/ListRow.spec.tsx +109 -22
  142. package/src/components/List/ListRow/ListRow.tsx +52 -13
  143. package/src/components/List/ListRowRenderer/ListRowRenderer.tsx +28 -22
  144. package/src/components/List/helpers.spec.ts +29 -0
  145. package/src/components/List/helpers.ts +17 -9
  146. package/src/components/List/useColumnsSize.ts +17 -7
  147. package/src/components/PageHeader/PageHeader.model.ts +38 -1
  148. package/src/components/PageHeader/PageHeader.module.scss +39 -18
  149. package/src/components/PageHeader/PageHeader.spec.tsx +407 -18
  150. package/src/components/PageHeader/PageHeader.stories.tsx +95 -12
  151. package/src/components/PageHeader/PageHeader.tsx +89 -38
  152. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +8 -0
  153. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.module.scss +50 -42
  154. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.spec.tsx +193 -1
  155. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.stories.tsx +36 -0
  156. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +81 -130
  157. package/src/components/PageHeader/helpers.spec.ts +60 -0
  158. package/src/components/PageHeader/helpers.ts +36 -0
  159. package/src/components/index.ts +1 -0
  160. package/src/hooks/index.ts +1 -0
  161. package/src/styles/variables.scss +41 -7
  162. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +0 -35
  163. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +0 -1
  164. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts +0 -6
  165. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts.map +0 -1
  166. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts +0 -3
  167. package/dist/cjs/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts.map +0 -1
  168. package/dist/cjs/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +0 -1
  169. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts +0 -35
  170. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +0 -1
  171. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts +0 -6
  172. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.d.ts.map +0 -1
  173. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts +0 -3
  174. package/dist/esm/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.d.ts.map +0 -1
  175. package/dist/esm/components/PageHeader/helpers/useElementWidthObserver.d.ts.map +0 -1
  176. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.module.scss +0 -77
  177. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.spec.tsx +0 -122
  178. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +0 -235
  179. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContext.ts +0 -15
  180. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroupsContextProvider.tsx +0 -37
  181. /package/dist/cjs/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.d.ts +0 -0
  182. /package/dist/esm/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.d.ts +0 -0
  183. /package/src/{components/PageHeader/helpers → hooks/useElementWidthObserver}/useElementWidthObserver.tsx +0 -0
@@ -0,0 +1,205 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import React, { useMemo, useState } from 'react';
4
+ import { MemoryRouter } from 'react-router-dom';
5
+ import { action } from 'storybook/actions';
6
+ import { EmptyStation } from '../EmptyStation';
7
+ import { IconName } from '../Icons';
8
+ import { List, ListSelectMode } from '../List';
9
+ import { ActionBar } from './ActionBar';
10
+ import type { ActionBarAction } from './ActionBar.model';
11
+
12
+ type ActionBarStoryType = React.ComponentProps<typeof ActionBar> & {
13
+ containerWidth: string;
14
+ };
15
+
16
+ const actions: ActionBarAction[] = [
17
+ {
18
+ label: 'Edit',
19
+ icon: IconName.BulkEdit,
20
+ tag: 'BETA',
21
+ toggled: false,
22
+ onClick: action('Edit.onClick'),
23
+ },
24
+ {
25
+ label: 'Publish',
26
+ icon: IconName.Publish,
27
+ onClick: action('Publish.onClick'),
28
+ },
29
+ {
30
+ label: 'Unpublish',
31
+ icon: IconName.Unpublish,
32
+ confirmationMode: 'Simple',
33
+ onClick: action('Unpublish.onClick'),
34
+ },
35
+ {
36
+ label: 'Delete',
37
+ icon: IconName.Delete,
38
+ confirmationMode: 'Advanced',
39
+ confirmationConfig: {
40
+ title: 'Delete the selected items?',
41
+ body: 'The action is irreversible.',
42
+ },
43
+ onClick: action('Delete.onClick'),
44
+ },
45
+ {
46
+ label: 'Archive',
47
+ icon: IconName.Archive,
48
+ disabled: true,
49
+ onClick: action('Archive.onClick'),
50
+ },
51
+ {
52
+ label: 'Export Settings',
53
+ icon: IconName.External,
54
+ path: '/export-settings',
55
+ },
56
+ ];
57
+
58
+ const meta: Meta<ActionBarStoryType> = {
59
+ title: 'Other Components/ActionBar',
60
+ component: ActionBar,
61
+ argTypes: {
62
+ actions: { control: false },
63
+ containerWidth: {
64
+ description: `<b>[Storybook only]</b> Width of the element the bar is rendered in.`,
65
+ },
66
+ },
67
+ args: {
68
+ label: 'Selected 3',
69
+ actions,
70
+ containerWidth: '100%',
71
+ },
72
+ decorators: [
73
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
74
+ (Story) => (
75
+ <MemoryRouter>
76
+ <Story />
77
+ </MemoryRouter>
78
+ ),
79
+ ],
80
+ render: ({ containerWidth, ...args }) => (
81
+ <div style={{ width: containerWidth }}>
82
+ <ActionBar {...args} />
83
+ </div>
84
+ ),
85
+ };
86
+ export default meta;
87
+
88
+ export const Default: StoryObj<ActionBarStoryType> = {
89
+ parameters: {
90
+ docs: {
91
+ description: {
92
+ story: `The bar renders a label at its start and the actions at its end. Actions run a handler, optionally behind a 'Simple' or 'Advanced' confirmation, or navigate when they have a 'path'.
93
+ They can be toggled, disabled, and carry a tag.`,
94
+ },
95
+ },
96
+ },
97
+ };
98
+
99
+ export const Overflow: StoryObj<ActionBarStoryType> = {
100
+ parameters: {
101
+ docs: {
102
+ description: {
103
+ story: `Actions that do not fit the available width move into an overflow menu at the end of the bar. The menu shows the tag of an action as part of its label.`,
104
+ },
105
+ },
106
+ },
107
+ args: {
108
+ containerWidth: '520px',
109
+ },
110
+ };
111
+
112
+ interface CompositionStoryData {
113
+ id: number;
114
+ title: string;
115
+ desc: string;
116
+ }
117
+
118
+ export const ExplorerLikeComposition: StoryObj<ActionBarStoryType> = {
119
+ name: 'Explorer-like composition',
120
+ parameters: {
121
+ docs: {
122
+ description: {
123
+ story: `An Explorer-like screen composed from its parts: an action on the page header toggles the bulk actions, which the List renders on its action bar through its 'actionBar' property.
124
+ The List keeps the bar in place above its column header. Render the ActionBar directly for content that is not a List.`,
125
+ },
126
+ },
127
+ controls: { disable: true },
128
+ },
129
+ render: () =>
130
+ React.createElement(() => {
131
+ const data = useMemo<CompositionStoryData[]>(
132
+ () =>
133
+ Array.from({ length: 30 }, (_, index) => ({
134
+ id: index + 1,
135
+ title: `Item ${index + 1}`,
136
+ desc: faker.lorem.sentence(),
137
+ })),
138
+ [],
139
+ );
140
+ const [isBulkOpen, setIsBulkOpen] = useState<boolean>(false);
141
+ const [selectedCount, setSelectedCount] = useState<number>(0);
142
+
143
+ const noSelection = selectedCount === 0;
144
+
145
+ return (
146
+ <EmptyStation
147
+ title="Custom Explorer"
148
+ actions={[
149
+ {
150
+ kind: 'action',
151
+ label: 'Bulk Actions',
152
+ icon: IconName.Bulk,
153
+ toggled: isBulkOpen,
154
+ onClick: () => {
155
+ setIsBulkOpen((open) => !open);
156
+ setSelectedCount(0);
157
+ },
158
+ },
159
+ ]}
160
+ >
161
+ <List<CompositionStoryData>
162
+ columns={[
163
+ { propertyName: 'id', label: 'Id', size: '60px' },
164
+ { propertyName: 'title', label: 'Title' },
165
+ { propertyName: 'desc', label: 'Description', size: '3fr' },
166
+ ]}
167
+ data={data}
168
+ selectionMode={
169
+ isBulkOpen ? ListSelectMode.Multi : ListSelectMode.None
170
+ }
171
+ onItemSelected={(event) =>
172
+ setSelectedCount(
173
+ event.mode === 'SELECT_ALL'
174
+ ? data.length
175
+ : event.items?.length ?? 0,
176
+ )
177
+ }
178
+ actionBar={{
179
+ label:
180
+ isBulkOpen && !noSelection
181
+ ? `Selected ${selectedCount}`
182
+ : `Total ${data.length}`,
183
+ actions: isBulkOpen
184
+ ? [
185
+ {
186
+ label: 'Publish',
187
+ icon: IconName.Publish,
188
+ disabled: noSelection,
189
+ onClick: action('Publish.onClick'),
190
+ },
191
+ {
192
+ label: 'Delete',
193
+ icon: IconName.Delete,
194
+ confirmationMode: 'Simple',
195
+ disabled: noSelection,
196
+ onClick: action('Delete.onClick'),
197
+ },
198
+ ]
199
+ : [],
200
+ }}
201
+ />
202
+ </EmptyStation>
203
+ );
204
+ }),
205
+ };
@@ -0,0 +1,170 @@
1
+ import clsx from 'clsx';
2
+ import React, { useLayoutEffect, useMemo, useState } from 'react';
3
+ import { useElementWidthObserver } from '../../hooks/useElementWidthObserver/useElementWidthObserver';
4
+ import type { ActionData } from '../Actions';
5
+ import { InlineMenu } from '../InlineMenu';
6
+ import type { ActionBarAction, ActionBarData } from './ActionBar.model';
7
+ import classes from './ActionBar.module.scss';
8
+ import { ActionBarButton } from './ActionBarButton/ActionBarButton';
9
+ import { getVisibleActionCount, isActionBarNavigationAction } from './helpers';
10
+
11
+ /** Width of the overflow menu trigger. Has to match `.overflowMenuButton`. */
12
+ const OVERFLOW_MENU_WIDTH_PX = 50;
13
+
14
+ export interface ActionBarProps extends ActionBarData {
15
+ /** Height of the bar, not including its bottom border. (default: '44px') */
16
+ height?: string;
17
+ /** CSS Class name for additional styles */
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * Renders a bar with a label at its start and actions that run to its end, e.g.
23
+ * the bulk actions that apply to the items selected in a list.
24
+ *
25
+ * Actions that do not fit the available width are moved into an overflow menu.
26
+ *
27
+ * The List renders the bar on top of its column header through its `actionBar`
28
+ * property, and the PageHeader renders it below itself while an actions group
29
+ * is open. Render it directly to place it anywhere else.
30
+ *
31
+ * @example
32
+ * <ActionBar
33
+ * label={'Selected 3'}
34
+ * actions={[{ label: 'Delete', icon: IconName.Delete, onClick: deleteHandler }]}
35
+ * />
36
+ */
37
+ export const ActionBar: React.FC<ActionBarProps> = ({
38
+ label,
39
+ actions,
40
+ height = '44px',
41
+ className = '',
42
+ }) => {
43
+ const { width: availableWidth, ref: actionsRef } =
44
+ useElementWidthObserver<HTMLDivElement>();
45
+
46
+ /**
47
+ * Natural width of every action, measured while all of them are rendered.
48
+ * Tagged with the set it was taken for, so that a new set of actions is
49
+ * measured again instead of being collapsed by the widths of the old one.
50
+ */
51
+ const [measurement, setMeasurement] = useState<{
52
+ key: string;
53
+ widths: number[];
54
+ }>();
55
+
56
+ const actionsKey = actions
57
+ .map((action) =>
58
+ [action.label, action.tag ?? '', action.icon === undefined].join(':'),
59
+ )
60
+ .join('|');
61
+ const actionWidths =
62
+ measurement?.key === actionsKey ? measurement.widths : undefined;
63
+
64
+ useLayoutEffect(() => {
65
+ if (actionWidths !== undefined || actionsRef.current === null) {
66
+ return;
67
+ }
68
+
69
+ const widths = Array.from(actionsRef.current.children).map(
70
+ (child) => (child as HTMLElement).offsetWidth,
71
+ );
72
+
73
+ // Every action is rendered while the widths are unknown, so anything else
74
+ // means the measurement ran before the actions were laid out. Widths of 0
75
+ // are reported by environments without a layout engine, in which case the
76
+ // actions are left uncollapsed.
77
+ if (
78
+ widths.length !== actions.length ||
79
+ widths.some((width) => width <= 0)
80
+ ) {
81
+ return;
82
+ }
83
+
84
+ setMeasurement({ key: actionsKey, widths });
85
+ }, [actionWidths, actionsKey, actions.length, actionsRef]);
86
+
87
+ const visibleActionCount = useMemo(
88
+ () =>
89
+ actionWidths === undefined || availableWidth <= 0
90
+ ? actions.length
91
+ : getVisibleActionCount(
92
+ actionWidths,
93
+ availableWidth,
94
+ OVERFLOW_MENU_WIDTH_PX,
95
+ ),
96
+ [actionWidths, availableWidth, actions.length],
97
+ );
98
+
99
+ const overflowActions = actions.slice(visibleActionCount);
100
+
101
+ const customStyles = {
102
+ gridAutoRows: height,
103
+ } as React.CSSProperties;
104
+
105
+ return (
106
+ <div
107
+ className={clsx(classes.container, 'action-bar-container', className)}
108
+ style={customStyles}
109
+ data-test-id="action-bar"
110
+ >
111
+ <div className={classes.content}>
112
+ {label !== undefined && (
113
+ <span className={classes.label} data-test-id="action-bar-label">
114
+ {label}
115
+ </span>
116
+ )}
117
+ <div
118
+ className={clsx(classes.actions, {
119
+ [classes.measuring]: actionWidths === undefined,
120
+ })}
121
+ ref={actionsRef}
122
+ >
123
+ {actions.slice(0, visibleActionCount).map((action, index) => (
124
+ // Keyed by the set of actions, so that another set starts afresh
125
+ // rather than taking over e.g. a pending confirmation.
126
+ <ActionBarButton key={`${actionsKey}#${index}`} action={action} />
127
+ ))}
128
+ {overflowActions.length > 0 && (
129
+ <InlineMenu
130
+ actions={overflowActions.map(toActionData)}
131
+ placement="bottom-end"
132
+ showArrow={false}
133
+ width="250px"
134
+ buttonClassName={classes.overflowMenuButton}
135
+ className={classes.overflowMenu}
136
+ />
137
+ )}
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ };
143
+
144
+ /**
145
+ * Converts an action of the bar into the shape the overflow menu renders, which
146
+ * keeps the confirmation behavior or the link of the action intact.
147
+ */
148
+ const toActionData = (action: ActionBarAction): ActionData => {
149
+ // The menu has no room for a tag of its own, so it becomes part of the label.
150
+ const label = action.tag ? `${action.label} (${action.tag})` : action.label;
151
+ // The menu renders icon components only, so an image source is dropped.
152
+ const icon = typeof action.icon === 'string' ? undefined : action.icon;
153
+
154
+ return isActionBarNavigationAction(action)
155
+ ? {
156
+ label,
157
+ icon,
158
+ isDisabled: action.disabled,
159
+ path: action.path,
160
+ openInNewTab: action.openInNewTab,
161
+ }
162
+ : {
163
+ label,
164
+ icon,
165
+ isDisabled: action.disabled,
166
+ confirmationMode: action.confirmationMode,
167
+ confirmationConfig: action.confirmationConfig,
168
+ onActionSelected: action.onClick,
169
+ };
170
+ };
@@ -0,0 +1,140 @@
1
+ @import '../../../styles/common.scss';
2
+
3
+ .container {
4
+ all: unset; // remove all button and link styles
5
+
6
+ @include boxSizing;
7
+
8
+ height: 100%;
9
+ // Keeps the action from resizing while the 'Simple' confirmation replaces the
10
+ // label, which would otherwise shift the actions next to it.
11
+ min-width: var(--action-bar-action-min-width, $action-bar-action-min-width);
12
+ padding: 0 12px;
13
+
14
+ display: flex;
15
+ align-items: center;
16
+ gap: 8px;
17
+
18
+ color: var(--action-bar-action-color, $action-bar-action-color);
19
+ font-size: 14px;
20
+ white-space: nowrap;
21
+ cursor: pointer;
22
+ user-select: none;
23
+
24
+ .icon {
25
+ display: grid;
26
+ place-items: center;
27
+
28
+ > * {
29
+ height: var(--action-bar-action-icon-size, $action-bar-action-icon-size);
30
+ }
31
+ }
32
+
33
+ .label {
34
+ display: grid;
35
+ place-items: center;
36
+ overflow: hidden;
37
+ text-overflow: ellipsis;
38
+
39
+ > span {
40
+ overflow: hidden;
41
+ text-overflow: ellipsis;
42
+ }
43
+ }
44
+
45
+ // Follows the color of the action, so that it matches its state.
46
+ .tag {
47
+ padding: 1px 4px;
48
+
49
+ border: 1px solid currentColor;
50
+ border-radius: 3px;
51
+
52
+ font-size: var(
53
+ --action-bar-action-tag-font-size,
54
+ $action-bar-action-tag-font-size
55
+ );
56
+ font-weight: bold;
57
+ line-height: 1.2;
58
+ }
59
+
60
+ .actionIcon {
61
+ rect,
62
+ polygon,
63
+ polyline,
64
+ path,
65
+ circle {
66
+ stroke: var(--action-bar-action-color, $action-bar-action-color);
67
+ stroke-width: var(
68
+ --action-bar-action-icon-stroke-width,
69
+ $action-bar-action-icon-stroke-width
70
+ );
71
+ }
72
+ }
73
+
74
+ &:hover:not(:disabled) {
75
+ background-color: var(
76
+ --action-bar-action-hover-background-color,
77
+ $action-bar-action-hover-background-color
78
+ );
79
+ transition: background-color $confirmation-transition linear;
80
+ }
81
+
82
+ &.toggled {
83
+ color: var(
84
+ --action-bar-action-toggled-color,
85
+ $action-bar-action-toggled-color
86
+ );
87
+
88
+ // Keeps its background while hovered, like the selected option of a Select.
89
+ &,
90
+ &:hover:not(:disabled) {
91
+ background-color: var(
92
+ --action-bar-action-toggled-background-color,
93
+ $action-bar-action-toggled-background-color
94
+ );
95
+ }
96
+
97
+ .actionIcon {
98
+ rect,
99
+ polygon,
100
+ polyline,
101
+ path,
102
+ circle {
103
+ stroke: var(
104
+ --action-bar-action-toggled-color,
105
+ $action-bar-action-toggled-color
106
+ );
107
+ }
108
+ }
109
+ }
110
+
111
+ &.hasConfirm,
112
+ &.hasConfirm:hover:not(:disabled) {
113
+ background-color: var(
114
+ --action-bar-action-confirmation-background-color,
115
+ $action-bar-action-confirmation-background-color
116
+ );
117
+ }
118
+
119
+ &:disabled {
120
+ color: var(
121
+ --action-bar-action-disabled-color,
122
+ $action-bar-action-disabled-color
123
+ );
124
+
125
+ cursor: default;
126
+
127
+ .actionIcon {
128
+ rect,
129
+ polygon,
130
+ polyline,
131
+ path,
132
+ circle {
133
+ stroke: var(
134
+ --action-bar-action-disabled-color,
135
+ $action-bar-action-disabled-color
136
+ );
137
+ }
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,193 @@
1
+ import clsx from 'clsx';
2
+ import React, { type PropsWithChildren } from 'react';
3
+ import { Link } from 'react-router-dom';
4
+ import { noop } from '../../../helpers/utils';
5
+ import { useConfirmableAction } from '../../ConfirmDialog';
6
+ import { IconName, Icons } from '../../Icons';
7
+ import { ImgElement } from '../../VisualElements';
8
+ import type {
9
+ ActionBarAction,
10
+ ActionBarJsAction,
11
+ ActionBarNavigationAction,
12
+ } from '../ActionBar.model';
13
+ import { isActionBarNavigationAction } from '../helpers';
14
+ import classes from './ActionBarButton.module.scss';
15
+
16
+ export interface ActionBarButtonProps {
17
+ /** The action to render */
18
+ action: ActionBarAction;
19
+ /** CSS Class name for additional styles */
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * Renders a single action of the ActionBar: a button running the handler of the
25
+ * action, or a link if the action has a `path`.
26
+ *
27
+ * Supports the same confirmation modes as the other action components: in
28
+ * 'Simple' mode the label is replaced by a confirmation message until the
29
+ * action is clicked a second time, in 'Advanced' mode a confirmation dialog is
30
+ * opened above the action.
31
+ *
32
+ * @example
33
+ * <ActionBarButton action={{ label: 'Delete', icon: IconName.Delete, onClick: deleteHandler }} />
34
+ */
35
+ export const ActionBarButton: React.FC<ActionBarButtonProps> = ({
36
+ action,
37
+ className,
38
+ }) =>
39
+ isActionBarNavigationAction(action) ? (
40
+ <NavigationActionButton action={action} className={className} />
41
+ ) : (
42
+ <JsActionButton action={action} className={className} />
43
+ );
44
+
45
+ const JsActionButton: React.FC<{
46
+ action: ActionBarJsAction;
47
+ className?: string;
48
+ }> = ({ action, className }) => {
49
+ const {
50
+ label,
51
+ icon,
52
+ imgAlt,
53
+ tag,
54
+ disabled = false,
55
+ toggled,
56
+ confirmationMode = 'None',
57
+ confirmationConfig = {},
58
+ onClick = noop,
59
+ } = action;
60
+
61
+ const {
62
+ isConfirming,
63
+ isSimpleConfirmation,
64
+ onTriggerClicked,
65
+ triggerProps,
66
+ simpleConfirmation,
67
+ advancedConfirmation,
68
+ } = useConfirmableAction<HTMLButtonElement>({
69
+ confirmationMode,
70
+ confirmationConfig,
71
+ onConfirmed: onClick,
72
+ });
73
+
74
+ return (
75
+ <>
76
+ <button
77
+ type="button"
78
+ className={clsx(
79
+ classes.container,
80
+ {
81
+ [classes.toggled]: toggled,
82
+ [classes.hasConfirm]: isConfirming,
83
+ },
84
+ 'action-bar-action-container',
85
+ className,
86
+ )}
87
+ {...triggerProps}
88
+ onClick={(event) => {
89
+ event.preventDefault();
90
+ onTriggerClicked();
91
+ }}
92
+ disabled={disabled}
93
+ aria-pressed={toggled}
94
+ data-test-id="action"
95
+ data-test-toggled={toggled}
96
+ >
97
+ {/* The confirmation message takes up the room of the icon and the tag */}
98
+ <ActionContent
99
+ icon={isConfirming ? undefined : icon}
100
+ imgAlt={imgAlt}
101
+ tag={isConfirming ? undefined : tag}
102
+ >
103
+ {isSimpleConfirmation ? (
104
+ simpleConfirmation
105
+ ) : (
106
+ <span data-test-id="label">{label}</span>
107
+ )}
108
+ </ActionContent>
109
+ </button>
110
+ {advancedConfirmation}
111
+ </>
112
+ );
113
+ };
114
+
115
+ /**
116
+ * Renders a link to the path of the action. A disabled action renders as a
117
+ * disabled button instead, as a link cannot be disabled.
118
+ */
119
+ const NavigationActionButton: React.FC<{
120
+ action: ActionBarNavigationAction;
121
+ className?: string;
122
+ }> = ({ action, className }) => {
123
+ const {
124
+ label,
125
+ icon,
126
+ imgAlt,
127
+ tag,
128
+ disabled = false,
129
+ path,
130
+ openInNewTab = false,
131
+ } = action;
132
+
133
+ const classNames = clsx(
134
+ classes.container,
135
+ 'action-bar-action-container',
136
+ className,
137
+ );
138
+
139
+ const content = (
140
+ <ActionContent
141
+ icon={icon ?? (openInNewTab ? IconName.External : undefined)}
142
+ imgAlt={imgAlt}
143
+ tag={tag}
144
+ >
145
+ <span data-test-id="label">{label}</span>
146
+ </ActionContent>
147
+ );
148
+
149
+ return disabled ? (
150
+ <button type="button" className={classNames} disabled data-test-id="action">
151
+ {content}
152
+ </button>
153
+ ) : (
154
+ <Link
155
+ to={path}
156
+ target={openInNewTab ? '_blank' : undefined}
157
+ className={classNames}
158
+ data-test-id="action"
159
+ >
160
+ {content}
161
+ </Link>
162
+ );
163
+ };
164
+
165
+ /** The icon, label and tag of an action. */
166
+ const ActionContent: React.FC<
167
+ PropsWithChildren<{
168
+ icon?: IconName | string;
169
+ imgAlt?: string;
170
+ tag?: string;
171
+ }>
172
+ > = ({ icon, imgAlt, tag, children }) => (
173
+ <>
174
+ {icon !== undefined && (
175
+ <span className={classes.icon}>
176
+ {typeof icon === 'string' ? (
177
+ <ImgElement
178
+ src={icon}
179
+ {...(imgAlt ? { alt: imgAlt } : { decorative: true as const })}
180
+ />
181
+ ) : (
182
+ <Icons icon={icon} className={classes.actionIcon} />
183
+ )}
184
+ </span>
185
+ )}
186
+ <span className={classes.label}>{children}</span>
187
+ {tag && (
188
+ <span className={classes.tag} data-test-id="tag">
189
+ {tag}
190
+ </span>
191
+ )}
192
+ </>
193
+ );