@12-apps/ui 6.24.8 → 6.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/components/data-display/Alert/Alert.helpers.js +5 -1
- package/dist/_internal/components/data-display/Alert/Alert.js +3 -3
- package/dist/_internal/components/data-display/Alert/Alert.styles.js +2 -2
- package/dist/_internal/components/data-display/CardKit/card-actions-context.js +4 -4
- package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +5 -5
- package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +19 -19
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +24 -24
- package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +15 -15
- package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +16 -16
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +18 -18
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +16 -16
- package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +4 -4
- package/dist/_internal/components/data-display/index.js +3 -3
- package/dist/_internal/components/form/CategorySelect/CategoryPanel.js +8 -8
- package/dist/_internal/components/form/CategorySelect/CategoryPanelFoot.js +2 -2
- package/dist/_internal/components/form/CategorySelect/CategoryPanelHead.js +2 -2
- package/dist/_internal/components/form/CategorySelect/CategoryPanelList.js +4 -4
- package/dist/_internal/components/form/CategorySelect/CategoryQuickActions.js +2 -2
- package/dist/_internal/components/form/CategorySelect/CategoryRows.js +7 -9
- package/dist/_internal/components/form/CategorySelect/CategorySelect.js +14 -14
- package/dist/_internal/components/form/CategorySelect/CategorySelect.styles.js +3 -1
- package/dist/_internal/components/form/CategorySelect/CategoryTrigger.js +2 -2
- package/dist/_internal/components/form/CategorySelect/category-keyboard.js +2 -2
- package/dist/_internal/components/form/CategorySelect/category-tree.js +11 -3
- package/dist/_internal/components/form/CategorySelect/useCategoryCommit.js +2 -2
- package/dist/_internal/components/form/CategorySelect/useCategoryKeyHandler.js +3 -3
- package/dist/_internal/components/form/CategorySelect/useCategorySelect.js +2 -2
- package/dist/_internal/components/form/total-form/Fields/CategoryField.js +15 -15
- package/dist/_internal/components/form/total-form/Fields/index.js +16 -16
- package/dist/_internal/components/index.js +3 -3
- package/dist/{chunk-WNMYWQ7D.js → chunk-4Y6SPFKW.js} +10 -8
- package/dist/chunk-4Y6SPFKW.js.map +1 -0
- package/dist/{chunk-KM5O4ICK.js → chunk-7LCXGJN5.js} +2 -2
- package/dist/{chunk-NLJV4CPF.js → chunk-7MN6I5NK.js} +2 -2
- package/dist/{chunk-PN5CQKA2.js → chunk-AXTLFK4T.js} +2 -2
- package/dist/{chunk-4TXIZ5WI.js → chunk-CJUOBTLL.js} +2 -2
- package/dist/{chunk-57Z2ND3N.js → chunk-CXLAJZSE.js} +2 -2
- package/dist/{chunk-E3PUH5IF.js → chunk-EQ7PWGBN.js} +2 -2
- package/dist/{chunk-EJQW2IYW.js → chunk-FPFUXRD2.js} +2 -2
- package/dist/{chunk-KIHQE5YA.js → chunk-GVDJQMFI.js} +2 -2
- package/dist/{chunk-N3QUEBNS.js → chunk-L7VH6IMR.js} +14 -1
- package/dist/chunk-L7VH6IMR.js.map +1 -0
- package/dist/{chunk-OFOYMZRY.js → chunk-NPN7JTRM.js} +22 -47
- package/dist/chunk-NPN7JTRM.js.map +1 -0
- package/dist/{chunk-OVLNF4AZ.js → chunk-P7IVAMAU.js} +24 -35
- package/dist/chunk-P7IVAMAU.js.map +1 -0
- package/dist/chunk-PN4HVM43.js +160 -0
- package/dist/chunk-PN4HVM43.js.map +1 -0
- package/dist/{chunk-WWM2YTW4.js → chunk-R2AZOWEZ.js} +21 -27
- package/dist/chunk-R2AZOWEZ.js.map +1 -0
- package/dist/{chunk-2GB5CIN6.js → chunk-RZVUY2MO.js} +8 -4
- package/dist/chunk-RZVUY2MO.js.map +1 -0
- package/dist/{chunk-FBI6QB3O.js → chunk-TVPPA4RD.js} +3 -3
- package/dist/{chunk-KFWIOFMS.js → chunk-TWY5FOBY.js} +3 -3
- package/dist/{chunk-6ZY5XU4J.js → chunk-UIZH4DAE.js} +2 -2
- package/dist/{chunk-ISH42UC7.js → chunk-USJPZ5Q7.js} +2 -2
- package/dist/{chunk-ZUIJ3N7N.js → chunk-V6Z4XS3I.js} +5 -3
- package/dist/chunk-V6Z4XS3I.js.map +1 -0
- package/dist/{chunk-SYJMI2NP.js → chunk-VNTOBLIB.js} +2 -2
- package/dist/{chunk-JM64OEU6.js → chunk-WAPJ2DL3.js} +10 -13
- package/dist/chunk-WAPJ2DL3.js.map +1 -0
- package/dist/{chunk-ZAB65STX.js → chunk-XHHR6G35.js} +2 -2
- package/dist/{chunk-WIT3Y7JT.js → chunk-Y2B3I3IH.js} +2 -2
- package/dist/{chunk-R6SNYC3D.js → chunk-Y3KRUT7O.js} +2 -2
- package/dist/{chunk-TPLKJHNN.js → chunk-ZC7RQDZ5.js} +8 -7
- package/dist/chunk-ZC7RQDZ5.js.map +1 -0
- package/dist/{chunk-TFO6GBW7.js → chunk-ZS44PY7T.js} +5 -1
- package/dist/chunk-ZS44PY7T.js.map +1 -0
- package/dist/{chunk-AJ6XEO4E.js → chunk-ZUY65MFH.js} +2 -2
- package/dist/data-display/Alert.js +3 -3
- package/dist/data-display/CardKit.js +5 -5
- package/dist/data-display/DataViews.js +24 -24
- package/dist/form/CategorySelect.js +24 -16
- package/dist/form/total-form.js +16 -16
- package/dist/native/data-display/Alert.js +18 -3
- package/dist/native/data-display/Alert.js.map +1 -1
- package/dist/types/components/data-display/Alert/Alert.base.d.ts +28 -2
- package/dist/types/components/data-display/Alert/Alert.base.d.ts.map +1 -1
- package/dist/types/components/data-display/Alert/Alert.helpers.d.ts +39 -0
- package/dist/types/components/data-display/Alert/Alert.helpers.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts +6 -0
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryQuickActions.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryRows.d.ts +12 -24
- package/dist/types/components/form/CategorySelect/CategoryRows.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategorySelect.d.ts +10 -7
- package/dist/types/components/form/CategorySelect/CategorySelect.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategorySelect.styles.d.ts +11 -0
- package/dist/types/components/form/CategorySelect/CategorySelect.styles.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts +10 -2
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/category-keyboard.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/category-tree.d.ts +52 -20
- package/dist/types/components/form/CategorySelect/category-tree.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/index.d.ts +1 -1
- package/dist/types/components/form/CategorySelect/index.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/useCategorySelect.d.ts +2 -0
- package/dist/types/components/form/CategorySelect/useCategorySelect.d.ts.map +1 -1
- package/dist/types-native/components/data-display/Alert/Alert.base.d.ts +28 -2
- package/dist/types-native/components/data-display/Alert/Alert.base.d.ts.map +1 -1
- package/dist/types-native/components/data-display/Alert/Alert.helpers.d.ts +39 -0
- package/dist/types-native/components/data-display/Alert/Alert.helpers.d.ts.map +1 -1
- package/dist/types-native/components/data-display/Alert/Alert.native.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/data-display/Alert/Alert.base.ts +29 -2
- package/src/components/data-display/Alert/Alert.helpers.ts +50 -0
- package/src/components/data-display/Alert/Alert.md +29 -1
- package/src/components/data-display/Alert/Alert.native.tsx +8 -4
- package/src/components/data-display/Alert/Alert.tsx +5 -5
- package/src/components/form/CategorySelect/CategoryPanel.tsx +1 -0
- package/src/components/form/CategorySelect/CategoryPanelList.tsx +42 -34
- package/src/components/form/CategorySelect/CategoryQuickActions.tsx +6 -4
- package/src/components/form/CategorySelect/CategoryRows.tsx +45 -81
- package/src/components/form/CategorySelect/CategorySelect.md +47 -13
- package/src/components/form/CategorySelect/CategorySelect.styles.ts +13 -0
- package/src/components/form/CategorySelect/CategorySelect.tsx +16 -14
- package/src/components/form/CategorySelect/CategorySelect.types.ts +10 -2
- package/src/components/form/CategorySelect/category-keyboard.ts +27 -31
- package/src/components/form/CategorySelect/category-tree.ts +143 -63
- package/src/components/form/CategorySelect/index.ts +4 -0
- package/src/components/form/CategorySelect/useCategorySelect.ts +15 -1
- package/dist/chunk-2GB5CIN6.js.map +0 -1
- package/dist/chunk-JM64OEU6.js.map +0 -1
- package/dist/chunk-LR6MSKTT.js +0 -124
- package/dist/chunk-LR6MSKTT.js.map +0 -1
- package/dist/chunk-N3QUEBNS.js.map +0 -1
- package/dist/chunk-OFOYMZRY.js.map +0 -1
- package/dist/chunk-OVLNF4AZ.js.map +0 -1
- package/dist/chunk-TFO6GBW7.js.map +0 -1
- package/dist/chunk-TPLKJHNN.js.map +0 -1
- package/dist/chunk-WNMYWQ7D.js.map +0 -1
- package/dist/chunk-WWM2YTW4.js.map +0 -1
- package/dist/chunk-ZUIJ3N7N.js.map +0 -1
- /package/dist/{chunk-KM5O4ICK.js.map → chunk-7LCXGJN5.js.map} +0 -0
- /package/dist/{chunk-NLJV4CPF.js.map → chunk-7MN6I5NK.js.map} +0 -0
- /package/dist/{chunk-PN5CQKA2.js.map → chunk-AXTLFK4T.js.map} +0 -0
- /package/dist/{chunk-4TXIZ5WI.js.map → chunk-CJUOBTLL.js.map} +0 -0
- /package/dist/{chunk-57Z2ND3N.js.map → chunk-CXLAJZSE.js.map} +0 -0
- /package/dist/{chunk-E3PUH5IF.js.map → chunk-EQ7PWGBN.js.map} +0 -0
- /package/dist/{chunk-EJQW2IYW.js.map → chunk-FPFUXRD2.js.map} +0 -0
- /package/dist/{chunk-KIHQE5YA.js.map → chunk-GVDJQMFI.js.map} +0 -0
- /package/dist/{chunk-FBI6QB3O.js.map → chunk-TVPPA4RD.js.map} +0 -0
- /package/dist/{chunk-KFWIOFMS.js.map → chunk-TWY5FOBY.js.map} +0 -0
- /package/dist/{chunk-6ZY5XU4J.js.map → chunk-UIZH4DAE.js.map} +0 -0
- /package/dist/{chunk-ISH42UC7.js.map → chunk-USJPZ5Q7.js.map} +0 -0
- /package/dist/{chunk-SYJMI2NP.js.map → chunk-VNTOBLIB.js.map} +0 -0
- /package/dist/{chunk-ZAB65STX.js.map → chunk-XHHR6G35.js.map} +0 -0
- /package/dist/{chunk-WIT3Y7JT.js.map → chunk-Y2B3I3IH.js.map} +0 -0
- /package/dist/{chunk-R6SNYC3D.js.map → chunk-Y3KRUT7O.js.map} +0 -0
- /package/dist/{chunk-AJ6XEO4E.js.map → chunk-ZUY65MFH.js.map} +0 -0
|
@@ -5,12 +5,8 @@ import Box from '@mui/material/Box/index.js';
|
|
|
5
5
|
import Button from '@mui/material/Button/index.js';
|
|
6
6
|
import Skeleton from '@mui/material/Skeleton/index.js';
|
|
7
7
|
|
|
8
|
-
import { categoryCheckState, isLeafCategory } from './category-tree';
|
|
9
|
-
import {
|
|
10
|
-
CategoryHeadRow,
|
|
11
|
-
CategorySectionHeading,
|
|
12
|
-
SubcategoryRow,
|
|
13
|
-
} from './CategoryRows';
|
|
8
|
+
import { categoryCheckState, isLeafCategory, leavesOf } from './category-tree';
|
|
9
|
+
import { CategorySectionHeading, CategoryTreeRow } from './CategoryRows';
|
|
14
10
|
import { emptySx, listSx, skeletonSx } from './CategorySelect.styles';
|
|
15
11
|
import type { CategoryGroup } from './CategorySelect.types';
|
|
16
12
|
|
|
@@ -33,6 +29,12 @@ interface PanelListProps {
|
|
|
33
29
|
loading: boolean;
|
|
34
30
|
showCounts: boolean;
|
|
35
31
|
allowParentSelection: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* True when the tree has a level BELOW the subcategories. It turns the
|
|
34
|
+
* section-heading treatment off — see {@link CategoryNodeRows}. Derived from
|
|
35
|
+
* the options the panel state already holds, never a caller's decision.
|
|
36
|
+
*/
|
|
37
|
+
deepTree: boolean;
|
|
36
38
|
isExpanded: (categoryId: string) => boolean;
|
|
37
39
|
onToggleExpanded: (categoryId: string) => void;
|
|
38
40
|
onActivateCategory: (group: CategoryGroup) => void;
|
|
@@ -114,60 +116,66 @@ function NoResults({
|
|
|
114
116
|
);
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
/**
|
|
120
|
+
* One node: its heading or its row, then whichever children are visible.
|
|
121
|
+
*
|
|
122
|
+
* The SECTION-HEADING treatment — an inert uppercase label with its children
|
|
123
|
+
* always on show — belongs to a two-level tree and only there. It is what makes
|
|
124
|
+
* the "mover para…" picker read as "category, then the subcategory you pick",
|
|
125
|
+
* and it works because a two-level category has nothing to fold TO: everything
|
|
126
|
+
* under it is already the thing you came for. A third level breaks both halves
|
|
127
|
+
* at once — the heading has no chevron, so the panel opens onto every item in
|
|
128
|
+
* the catalogue, which is the flat list this component exists to replace. So a
|
|
129
|
+
* deep tree drops the treatment and every frame becomes a foldable row instead.
|
|
130
|
+
*/
|
|
131
|
+
function CategoryNodeRows({
|
|
132
|
+
node,
|
|
133
|
+
depth,
|
|
120
134
|
props,
|
|
121
135
|
}: {
|
|
122
|
-
|
|
136
|
+
node: CategoryGroup;
|
|
137
|
+
depth: number;
|
|
123
138
|
props: PanelListProps;
|
|
124
139
|
}): React.JSX.Element {
|
|
125
140
|
const { draft, query, single, allowParentSelection, rowIds, activeIndex, sheet } = props;
|
|
126
|
-
const categoryId =
|
|
141
|
+
const categoryId = node.category.id;
|
|
142
|
+
const leaf = isLeafCategory(node);
|
|
127
143
|
const expanded = props.isExpanded(categoryId);
|
|
128
144
|
// A childless category is the leaf, so it is selectable whatever the mode says
|
|
129
145
|
// — the heading reading has no subcategory to point at, and would leave the
|
|
130
146
|
// row inert.
|
|
131
|
-
const selectable = allowParentSelection ||
|
|
132
|
-
const asHeading = single && !selectable;
|
|
133
|
-
const selectedCount =
|
|
147
|
+
const selectable = allowParentSelection || leaf;
|
|
148
|
+
const asHeading = single && !selectable && !props.deepTree;
|
|
149
|
+
const selectedCount = leaf ? 0 : leavesOf(node).filter((id) => draft.has(id)).length;
|
|
134
150
|
|
|
135
151
|
return (
|
|
136
152
|
<>
|
|
137
153
|
{asHeading ? (
|
|
138
|
-
<CategorySectionHeading option={
|
|
154
|
+
<CategorySectionHeading option={node.category} query={query} />
|
|
139
155
|
) : (
|
|
140
|
-
<
|
|
141
|
-
option={
|
|
156
|
+
<CategoryTreeRow
|
|
157
|
+
option={node.category}
|
|
142
158
|
query={query}
|
|
159
|
+
depth={depth}
|
|
160
|
+
branch={!leaf}
|
|
143
161
|
expanded={expanded}
|
|
144
162
|
active={rowIds[activeIndex] === categoryId}
|
|
145
163
|
sheet={sheet}
|
|
146
|
-
|
|
164
|
+
{...(selectable ? { checkState: categoryCheckState(node, draft) } : {})}
|
|
147
165
|
single={single}
|
|
148
|
-
expandable={!isLeafCategory(group)}
|
|
149
166
|
selectedCount={selectedCount}
|
|
150
167
|
showCounts={props.showCounts}
|
|
151
168
|
onToggleExpanded={() => props.onToggleExpanded(categoryId)}
|
|
152
|
-
onActivate={() =>
|
|
169
|
+
onActivate={() =>
|
|
170
|
+
leaf ? props.onActivateSubcategory(categoryId) : props.onActivateCategory(node)
|
|
171
|
+
}
|
|
153
172
|
dataTestId={props.dataTestId}
|
|
154
173
|
copy={props.copy}
|
|
155
174
|
/>
|
|
156
175
|
)}
|
|
157
176
|
{(expanded || asHeading) &&
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
key={sub.id}
|
|
161
|
-
option={sub}
|
|
162
|
-
query={query}
|
|
163
|
-
selected={draft.has(sub.id)}
|
|
164
|
-
active={rowIds[activeIndex] === sub.id}
|
|
165
|
-
sheet={sheet}
|
|
166
|
-
single={single}
|
|
167
|
-
showCounts={props.showCounts}
|
|
168
|
-
onActivate={() => props.onActivateSubcategory(sub.id)}
|
|
169
|
-
dataTestId={props.dataTestId}
|
|
170
|
-
/>
|
|
177
|
+
node.subcategories.map((sub) => (
|
|
178
|
+
<CategoryNodeRows key={sub.category.id} node={sub} depth={depth + 1} props={props} />
|
|
171
179
|
))}
|
|
172
180
|
</>
|
|
173
181
|
);
|
|
@@ -200,7 +208,7 @@ export function CategoryPanelList(props: PanelListProps): React.JSX.Element {
|
|
|
200
208
|
return (
|
|
201
209
|
<>
|
|
202
210
|
{groups.map((group) => (
|
|
203
|
-
<
|
|
211
|
+
<CategoryNodeRows key={group.category.id} node={group} depth={0} props={props} />
|
|
204
212
|
))}
|
|
205
213
|
</>
|
|
206
214
|
);
|
|
@@ -5,7 +5,7 @@ import { useMemo } from 'react';
|
|
|
5
5
|
|
|
6
6
|
import type { CategorySelectCopy } from '../../../copy';
|
|
7
7
|
|
|
8
|
-
import { collectLeafIds } from './category-tree';
|
|
8
|
+
import { collectBranchIds, collectLeafIds } from './category-tree';
|
|
9
9
|
import type { CategorySelectState } from './useCategorySelect';
|
|
10
10
|
|
|
11
11
|
/** The panel's text-button treatment. */
|
|
@@ -41,9 +41,11 @@ export function CategoryQuickActions({
|
|
|
41
41
|
}): React.JSX.Element {
|
|
42
42
|
const allLeafIds = useMemo(() => collectLeafIds(state.allGroups), [state.allGroups]);
|
|
43
43
|
const everythingPicked = allLeafIds.length > 0 && state.draft.size >= allLeafIds.length;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Measured over every foldable node at every depth: on a deep tree the top
|
|
45
|
+
// storey can be open while the items under it are not, and a label reading
|
|
46
|
+
// "Recolher tudo" there would be describing a state the list is not in.
|
|
47
|
+
const branchIds = useMemo(() => collectBranchIds(state.allGroups), [state.allGroups]);
|
|
48
|
+
const allExpanded = branchIds.length > 0 && branchIds.every((id) => state.isExpanded(id));
|
|
47
49
|
|
|
48
50
|
return (
|
|
49
51
|
<>
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
markSx,
|
|
13
13
|
METRICS,
|
|
14
14
|
radioSx,
|
|
15
|
+
rowIndent,
|
|
15
16
|
rowMetaSx,
|
|
16
17
|
rowNameSx,
|
|
17
18
|
rowSx,
|
|
@@ -57,18 +58,20 @@ export function CategoryRadio({ on }: { on: boolean }): React.JSX.Element {
|
|
|
57
58
|
return <Box sx={(theme) => radioSx(theme, on)} aria-hidden="true" />;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
interface
|
|
61
|
+
interface CategoryTreeRowProps {
|
|
61
62
|
option: CategorySelectOption;
|
|
62
63
|
query: string;
|
|
64
|
+
/** 0 for a top-level category; each level below indents one more step. */
|
|
65
|
+
depth: number;
|
|
66
|
+
/** True when there is something under this row to fold. */
|
|
67
|
+
branch: boolean;
|
|
63
68
|
expanded: boolean;
|
|
64
69
|
active: boolean;
|
|
65
70
|
sheet: boolean;
|
|
66
|
-
/** Absent
|
|
71
|
+
/** Absent where the row cannot be picked — a category acting as a frame. */
|
|
67
72
|
checkState?: CategoryCheckState;
|
|
68
73
|
/** Single-select draws a radio for the control: choosing a row is exclusive. */
|
|
69
74
|
single: boolean;
|
|
70
|
-
/** False for a childless category — there is nothing under it to disclose. */
|
|
71
|
-
expandable: boolean;
|
|
72
75
|
selectedCount: number;
|
|
73
76
|
showCounts: boolean;
|
|
74
77
|
onToggleExpanded: () => void;
|
|
@@ -77,7 +80,7 @@ interface CategoryHeadRowProps {
|
|
|
77
80
|
copy: CategorySelectCopy;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
/** The trailing number: how many
|
|
83
|
+
/** The trailing number: how many leaves are picked below, else the item count. */
|
|
81
84
|
function rowMeta(
|
|
82
85
|
selectedCount: number,
|
|
83
86
|
showCounts: boolean,
|
|
@@ -88,19 +91,19 @@ function rowMeta(
|
|
|
88
91
|
return null;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
/** The disclosure chevron, or the space it would have taken on a
|
|
92
|
-
function
|
|
94
|
+
/** The disclosure chevron, or the space it would have taken on a leaf row. */
|
|
95
|
+
function RowDisclosure({
|
|
93
96
|
option,
|
|
94
97
|
expanded,
|
|
95
|
-
|
|
98
|
+
branch,
|
|
96
99
|
onToggleExpanded,
|
|
97
100
|
dataTestId,
|
|
98
101
|
copy,
|
|
99
102
|
}: Pick<
|
|
100
|
-
|
|
101
|
-
'option' | 'expanded' | '
|
|
103
|
+
CategoryTreeRowProps,
|
|
104
|
+
'option' | 'expanded' | 'branch' | 'onToggleExpanded' | 'dataTestId' | 'copy'
|
|
102
105
|
>): React.JSX.Element {
|
|
103
|
-
if (!
|
|
106
|
+
if (!branch) {
|
|
104
107
|
return <Box sx={{ width: METRICS.chevronButton, flex: '0 0 auto' }} />;
|
|
105
108
|
}
|
|
106
109
|
return (
|
|
@@ -121,11 +124,11 @@ function HeadRowDisclosure({
|
|
|
121
124
|
);
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
/** The row's selection control, or the space it would have taken on a
|
|
125
|
-
function
|
|
127
|
+
/** The row's selection control, or the space it would have taken on a frame. */
|
|
128
|
+
function RowControl({
|
|
126
129
|
checkState,
|
|
127
130
|
single,
|
|
128
|
-
}: Pick<
|
|
131
|
+
}: Pick<CategoryTreeRowProps, 'checkState' | 'single'>): React.JSX.Element {
|
|
129
132
|
if (!checkState) return <Box sx={{ width: METRICS.boxSize, flex: '0 0 auto' }} />;
|
|
130
133
|
// Single-select commits the moment a row is chosen, so the control is a radio
|
|
131
134
|
// there: a checkbox would promise the accumulation this mode does not do.
|
|
@@ -134,51 +137,63 @@ function HeadRowControl({
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
137
|
-
*
|
|
140
|
+
* The test id a row answers to.
|
|
138
141
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* itself the leaf.
|
|
142
|
+
* Keyed on DEPTH rather than on whether the row has children, because that is
|
|
143
|
+
* what the ids meant when the tree was two levels and specs were written against
|
|
144
|
+
* them: `-category-<id>` is a top-level row — childless or not — and `-option-`
|
|
145
|
+
* is anything filed under one.
|
|
144
146
|
*/
|
|
145
|
-
|
|
147
|
+
function rowTestId(dataTestId: string, depth: number, id: string): string {
|
|
148
|
+
return depth === 0 ? `${dataTestId}-category-${id}` : `${dataTestId}-option-${id}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* One row of the tree: disclosure chevron, optional control, name, meta.
|
|
153
|
+
*
|
|
154
|
+
* The chevron is its OWN button inside the row — clicking it only folds, while
|
|
155
|
+
* clicking the row does the row's job (expand as a frame, or mark when the row
|
|
156
|
+
* is selectable). A LEAF has neither a fold nor a frame to be: it draws no
|
|
157
|
+
* chevron, and it always carries the control, because it is what you pick.
|
|
158
|
+
*/
|
|
159
|
+
export function CategoryTreeRow({
|
|
146
160
|
option,
|
|
147
161
|
query,
|
|
162
|
+
depth,
|
|
163
|
+
branch,
|
|
148
164
|
expanded,
|
|
149
165
|
active,
|
|
150
166
|
sheet,
|
|
151
167
|
checkState,
|
|
152
168
|
single,
|
|
153
|
-
expandable,
|
|
154
169
|
selectedCount,
|
|
155
170
|
showCounts,
|
|
156
171
|
onToggleExpanded,
|
|
157
172
|
onActivate,
|
|
158
173
|
dataTestId,
|
|
159
174
|
copy,
|
|
160
|
-
}:
|
|
175
|
+
}: CategoryTreeRowProps): React.JSX.Element {
|
|
161
176
|
const meta = rowMeta(selectedCount, showCounts, option.count);
|
|
162
177
|
return (
|
|
163
178
|
<Box
|
|
164
179
|
component="div"
|
|
165
180
|
role={checkState ? 'option' : 'button'}
|
|
166
181
|
aria-selected={checkState ? checkState === 'on' : undefined}
|
|
167
|
-
aria-expanded={
|
|
168
|
-
data-testid={
|
|
169
|
-
sx={(theme) => rowSx(theme, active, sheet)}
|
|
182
|
+
aria-expanded={branch ? expanded : undefined}
|
|
183
|
+
data-testid={rowTestId(dataTestId, depth, option.id)}
|
|
184
|
+
sx={(theme) => ({ ...rowSx(theme, active, sheet), ...rowIndent(depth) })}
|
|
170
185
|
onClick={onActivate}
|
|
171
186
|
>
|
|
172
|
-
<
|
|
187
|
+
<RowDisclosure
|
|
173
188
|
option={option}
|
|
174
189
|
expanded={expanded}
|
|
175
|
-
|
|
190
|
+
branch={branch}
|
|
176
191
|
onToggleExpanded={onToggleExpanded}
|
|
177
192
|
dataTestId={dataTestId}
|
|
178
193
|
copy={copy}
|
|
179
194
|
/>
|
|
180
|
-
<
|
|
181
|
-
<Box component="span" sx={(theme) => rowNameSx(theme,
|
|
195
|
+
<RowControl checkState={checkState} single={single} />
|
|
196
|
+
<Box component="span" sx={(theme) => rowNameSx(theme, depth === 0)}>
|
|
182
197
|
<HighlightedName text={option.name} query={query} />
|
|
183
198
|
</Box>
|
|
184
199
|
{meta && (
|
|
@@ -190,57 +205,6 @@ export function CategoryHeadRow({
|
|
|
190
205
|
);
|
|
191
206
|
}
|
|
192
207
|
|
|
193
|
-
interface SubcategoryRowProps {
|
|
194
|
-
option: CategorySelectOption;
|
|
195
|
-
query: string;
|
|
196
|
-
selected: boolean;
|
|
197
|
-
active: boolean;
|
|
198
|
-
sheet: boolean;
|
|
199
|
-
/** Single-select mode draws a radio; multi-select draws a checkbox. */
|
|
200
|
-
single: boolean;
|
|
201
|
-
showCounts: boolean;
|
|
202
|
-
onActivate: () => void;
|
|
203
|
-
dataTestId: string;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function SubcategoryRow({
|
|
207
|
-
option,
|
|
208
|
-
query,
|
|
209
|
-
selected,
|
|
210
|
-
active,
|
|
211
|
-
sheet,
|
|
212
|
-
single,
|
|
213
|
-
showCounts,
|
|
214
|
-
onActivate,
|
|
215
|
-
dataTestId,
|
|
216
|
-
}: SubcategoryRowProps): React.JSX.Element {
|
|
217
|
-
return (
|
|
218
|
-
<Box
|
|
219
|
-
component="div"
|
|
220
|
-
role="option"
|
|
221
|
-
aria-selected={selected}
|
|
222
|
-
data-testid={`${dataTestId}-option-${option.id}`}
|
|
223
|
-
sx={(theme) => ({ ...rowSx(theme, active, sheet), paddingLeft: '14px' })}
|
|
224
|
-
onClick={onActivate}
|
|
225
|
-
>
|
|
226
|
-
<Box sx={{ width: METRICS.chevronButton, flex: '0 0 auto' }} />
|
|
227
|
-
{single ? (
|
|
228
|
-
<CategoryRadio on={selected} />
|
|
229
|
-
) : (
|
|
230
|
-
<CategoryCheckbox state={selected ? 'on' : 'off'} />
|
|
231
|
-
)}
|
|
232
|
-
<Box component="span" sx={(theme) => rowNameSx(theme, false)}>
|
|
233
|
-
<HighlightedName text={option.name} query={query} />
|
|
234
|
-
</Box>
|
|
235
|
-
{showCounts && option.count !== undefined && (
|
|
236
|
-
<Box component="span" sx={(theme) => rowMetaSx(theme, false)}>
|
|
237
|
-
{option.count}
|
|
238
|
-
</Box>
|
|
239
|
-
)}
|
|
240
|
-
</Box>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
208
|
/** The category as a non-interactive heading (single-select, leaf-only). */
|
|
245
209
|
export function CategorySectionHeading({
|
|
246
210
|
option,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CategorySelect
|
|
2
2
|
|
|
3
|
-
A hierarchical
|
|
4
|
-
you pick.**
|
|
3
|
+
A hierarchical picker: **the categories are the frame, whatever is filed at the
|
|
4
|
+
bottom is what you pick.** Two levels by default; as many as the payload has.
|
|
5
5
|
|
|
6
6
|
```tsx
|
|
7
7
|
import { CategorySelect } from '@12-apps/ui/form/CategorySelect';
|
|
@@ -33,7 +33,32 @@ Never pre-indent `name` (`"— Águas"`). The row draws the nesting; a baked-in
|
|
|
33
33
|
travels everywhere the label goes, including the closed trigger.
|
|
34
34
|
|
|
35
35
|
An option whose `parentId` matches nothing is promoted to top level rather than
|
|
36
|
-
dropped, so a partial payload never makes a category invisible.
|
|
36
|
+
dropped, so a partial payload never makes a category invisible. So is an option
|
|
37
|
+
on a `parentId` RING — two rows naming each other would otherwise build a loop
|
|
38
|
+
every walk of the tree follows until the stack gives out.
|
|
39
|
+
|
|
40
|
+
### Depth
|
|
41
|
+
|
|
42
|
+
**Nothing caps it at two.** Point a `parentId` at a subcategory and you get a
|
|
43
|
+
third storey; the row indents one more step, folds, searches and reports its
|
|
44
|
+
path like every level above it.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const options = [
|
|
48
|
+
{ id: 'beb', name: 'Bebidas' },
|
|
49
|
+
{ id: 'beb.refri', name: 'Refrigerantes', parentId: 'beb' },
|
|
50
|
+
{ id: 'p.coca', name: 'Coca-Cola', parentId: 'beb.refri' }, // an ITEM
|
|
51
|
+
];
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Two things follow from a deeper tree, and both are the component's own doing:
|
|
55
|
+
|
|
56
|
+
- **A category stands for the leaves at the BOTTOM of it**, never for the
|
|
57
|
+
subcategories in between. `Bebidas` ticked means every drink under every one of
|
|
58
|
+
its subcategories, so the value a caller receives is uniformly leaves.
|
|
59
|
+
- **The section-heading treatment switches off** (see below), because a heading
|
|
60
|
+
has no chevron and a three-level tree drawn flat is the list this component is
|
|
61
|
+
adopted to replace.
|
|
37
62
|
|
|
38
63
|
## Which rows are selectable
|
|
39
64
|
|
|
@@ -42,14 +67,14 @@ parent that HAS children can be ticked in its own right.
|
|
|
42
67
|
|
|
43
68
|
| `allowParentSelection` | has children | the row |
|
|
44
69
|
| --- | --- | --- |
|
|
45
|
-
| `false` (default) | yes | a **
|
|
70
|
+
| `false` (default) | yes | a **frame** — no control; what is filed under it is what you tick |
|
|
46
71
|
| `false` | no | **selectable** — it is itself the leaf |
|
|
47
|
-
| `true` | yes | **selectable** — ticking it selects **every
|
|
72
|
+
| `true` | yes | **selectable** — ticking it selects **every leaf under it** |
|
|
48
73
|
| `true` | no | **selectable** |
|
|
49
74
|
|
|
50
75
|
Two of those deserve their reasons written down.
|
|
51
76
|
|
|
52
|
-
**A parent with children, flag on, selects its
|
|
77
|
+
**A parent with children, flag on, selects its LEAVES** — not an id of its own.
|
|
53
78
|
That is what makes the value a caller receives uniform: always leaves, never a
|
|
54
79
|
mix of leaves and groups that every consumer would have to re-expand. The chip
|
|
55
80
|
tray collapses a fully-selected category back to one chip bearing the category's
|
|
@@ -80,7 +105,8 @@ const [categories, setCategories] = useState<string[]>([]);
|
|
|
80
105
|
### `single` — the "move to…" picker
|
|
81
106
|
|
|
82
107
|
Choosing a row commits immediately; there is nothing to batch. The trigger then
|
|
83
|
-
reads `Pai › Filha`,
|
|
108
|
+
reads the whole path — `Pai › Filha`, or `Pai › Filha › Item` on a deeper tree —
|
|
109
|
+
so the chosen leaf keeps its context once the panel has closed over it.
|
|
84
110
|
|
|
85
111
|
```tsx
|
|
86
112
|
<CategorySelect
|
|
@@ -94,8 +120,16 @@ reads `Pai › Filha`, so the chosen leaf keeps its context.
|
|
|
94
120
|
|
|
95
121
|
## Behaviour worth knowing
|
|
96
122
|
|
|
97
|
-
- **Opens
|
|
98
|
-
|
|
123
|
+
- **Opens with the top level unfolded.** On a two-level tree that is everything.
|
|
124
|
+
On a deeper one the items stay one click away, which is the whole point of
|
|
125
|
+
filing them under something. A **search reveals every hit regardless** — folded
|
|
126
|
+
or not — and clearing it restores exactly what you had folded.
|
|
127
|
+
- **A frame you cannot tick is still a foldable ROW in a deep tree.** In a
|
|
128
|
+
two-level tree, single-select draws it as an inert uppercase heading with its
|
|
129
|
+
subcategories always on show: there is nothing to fold to, since everything
|
|
130
|
+
under it is already the thing you came for. Add a third level and that reading
|
|
131
|
+
breaks twice over — the heading has no chevron, so the panel opens onto the
|
|
132
|
+
whole catalogue — so the treatment is dropped and every frame gets a chevron.
|
|
99
133
|
- **Selected categories pin above the list.** They survive scrolling and
|
|
100
134
|
searching — a tick made at the bottom of a ten-category tree does not vanish
|
|
101
135
|
the moment you look for the next one.
|
|
@@ -112,9 +146,9 @@ reads `Pai › Filha`, so the chosen leaf keeps its context.
|
|
|
112
146
|
| Key | Does |
|
|
113
147
|
| --- | --- |
|
|
114
148
|
| `↑` `↓` | Move the cursor |
|
|
115
|
-
| `→` | Expand the
|
|
116
|
-
| `←` | Collapse it, or jump from a
|
|
117
|
-
| `Space` | Mark the row (single-select: pick it) — a
|
|
149
|
+
| `→` | Expand the row, at any depth |
|
|
150
|
+
| `←` | Collapse it, or jump from a leaf to its parent |
|
|
151
|
+
| `Space` | Mark the row (single-select: pick it) — a frame expands instead |
|
|
118
152
|
| `Enter` | Apply (single-select: pick, and expand on a heading, as `Space` does) |
|
|
119
153
|
| `Esc` | Cancel, returning focus to the trigger |
|
|
120
154
|
|
|
@@ -126,7 +160,7 @@ the field.
|
|
|
126
160
|
## Styling
|
|
127
161
|
|
|
128
162
|
Geometry is the design's absolute pixels — 38px trigger, 340px panel, 36px rows,
|
|
129
|
-
1.5px checkbox borders. Those are deliberately not derived from `theme.spacing`,
|
|
163
|
+
14px of indent per level, 1.5px checkbox borders. Those are deliberately not derived from `theme.spacing`,
|
|
130
164
|
which is an 8px grid and would round the design away.
|
|
131
165
|
|
|
132
166
|
**Colours come from the theme.** A tenant can white-label the palette, and the
|
|
@@ -28,6 +28,8 @@ export const METRICS = {
|
|
|
28
28
|
rowRadius: 8,
|
|
29
29
|
boxSize: 18,
|
|
30
30
|
chevronButton: 22,
|
|
31
|
+
/** Extra left padding per level of nesting. */
|
|
32
|
+
rowIndentStep: 14,
|
|
31
33
|
footerButton: 32,
|
|
32
34
|
sheetFooterButton: 38,
|
|
33
35
|
/** Below this width the panel becomes a bottom sheet. */
|
|
@@ -239,6 +241,17 @@ export const rowSx = (theme: Theme, active: boolean, sheet: boolean): CSSObject
|
|
|
239
241
|
'&:focus-visible': { outline: `2px solid ${theme.palette.primary.main}`, outlineOffset: 1 },
|
|
240
242
|
});
|
|
241
243
|
|
|
244
|
+
/**
|
|
245
|
+
* How far a row sits in from the panel edge, by depth.
|
|
246
|
+
*
|
|
247
|
+
* One step per level, on top of {@link rowSx}'s own 8px gutter — depth 0 keeps
|
|
248
|
+
* that gutter, and every level below it adds `METRICS.rowIndentStep`. Absolute
|
|
249
|
+
* like the rest of this file: the step is the design's own 14px, not a spacing
|
|
250
|
+
* multiple that would round it.
|
|
251
|
+
*/
|
|
252
|
+
export const rowIndent = (depth: number): CSSObject =>
|
|
253
|
+
depth === 0 ? {} : { paddingLeft: `${depth * METRICS.rowIndentStep}px` };
|
|
254
|
+
|
|
242
255
|
export const rowNameSx = (theme: Theme, isCategory: boolean): CSSObject => ({
|
|
243
256
|
flex: 1,
|
|
244
257
|
minWidth: 0,
|
|
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef } from 'react';
|
|
|
4
4
|
import { useTheme } from '@mui/material/styles/index.js';
|
|
5
5
|
import useMediaQuery from '@mui/material/useMediaQuery/index.js';
|
|
6
6
|
|
|
7
|
-
import { summarizeSelection } from './category-tree';
|
|
7
|
+
import { categoryPath, summarizeSelection } from './category-tree';
|
|
8
8
|
import { CategoryPanel } from './CategoryPanel';
|
|
9
9
|
import { CategoryPanelSurface } from './CategoryPanelSurface';
|
|
10
10
|
import { CategoryTrigger } from './CategoryTrigger';
|
|
@@ -26,14 +26,13 @@ function draftChanged(draft: ReadonlySet<string>, applied: readonly string[]): b
|
|
|
26
26
|
return draft.size !== applied.length || applied.some((id) => !draft.has(id));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* "Pai › Filha" for a chosen id, so the trigger carries its context once the
|
|
31
|
+
* panel has closed over it. The whole path, at whatever depth the row sits.
|
|
32
|
+
*/
|
|
30
33
|
function singleLabel(groups: CategoryGroup[], value: string | null): string | undefined {
|
|
31
34
|
if (!value) return undefined;
|
|
32
|
-
|
|
33
|
-
if (asCategory) return asCategory.category.name;
|
|
34
|
-
const owner = groups.find((group) => group.subcategories.some((sub) => sub.id === value));
|
|
35
|
-
const sub = owner?.subcategories.find((candidate) => candidate.id === value);
|
|
36
|
-
return sub && owner ? `${owner.category.name} › ${sub.name}` : undefined;
|
|
35
|
+
return categoryPath(groups, value)?.join(' › ');
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
/** The optional props, resolved once so the component body stays branch-free. */
|
|
@@ -85,19 +84,22 @@ function usePanelFocus(
|
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
/**
|
|
88
|
-
* A hierarchical category picker: categories as the frame,
|
|
89
|
-
* thing you choose.
|
|
87
|
+
* A hierarchical category picker: categories as the frame, whatever is filed
|
|
88
|
+
* under them as the thing you choose.
|
|
90
89
|
*
|
|
91
90
|
* Two modes off one component. `multi` is the FILTER — ticks accumulate in a
|
|
92
91
|
* draft and reach `onChange` only on Apply, so the list behind the panel does not
|
|
93
92
|
* reload on every click. `single` is the "move to…" picker, where choosing a row
|
|
94
93
|
* commits immediately because there is nothing to batch.
|
|
95
94
|
*
|
|
96
|
-
* The tree
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
95
|
+
* The tree nests as deep as the payload does — a picker can file ITEMS under a
|
|
96
|
+
* subcategory and get a third storey with no extra configuration. Opening
|
|
97
|
+
* unfolds the top level, and a search reveals every hit whatever is folded.
|
|
98
|
+
*
|
|
99
|
+
* Selected categories pin above the list so they survive scrolling and
|
|
100
|
+
* searching, search is accent-insensitive (`agua` finds `Águas`), and the whole
|
|
101
|
+
* tree is keyboard-drivable — ↑↓ to move, → to open, ← to close, Space to mark,
|
|
102
|
+
* Enter to apply, Esc to cancel.
|
|
101
103
|
*
|
|
102
104
|
* Under {@link METRICS.sheetBreakpoint} the panel becomes a bottom sheet, which
|
|
103
105
|
* is the only way a 290px-tall list stays usable one-handed.
|
|
@@ -22,10 +22,18 @@ export interface CategorySelectOption {
|
|
|
22
22
|
count?: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* One node of the category tree: an option together with whatever sits under it.
|
|
27
|
+
*
|
|
28
|
+
* Recursive rather than "a category and its flat subcategories" because the tree
|
|
29
|
+
* a caller hands over is not always two deep — a picker can file ITEMS under a
|
|
30
|
+
* subcategory, and the level below still has to nest, fold and search like the
|
|
31
|
+
* one above it. A node with an empty `subcategories` is the leaf.
|
|
32
|
+
*/
|
|
26
33
|
export interface CategoryGroup {
|
|
27
34
|
category: CategorySelectOption;
|
|
28
|
-
|
|
35
|
+
/** The nodes directly below it, each a group in its own right. */
|
|
36
|
+
subcategories: CategoryGroup[];
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/** One entry in the "selected" summary: a whole category, or a single leaf. */
|