@homebound/beam 2.354.2 → 2.354.4
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/Css.d.ts +12 -0
- package/dist/Css.js +12 -0
- package/dist/inputs/TreeSelectField/TreeSelectField.d.ts +4 -0
- package/dist/inputs/TreeSelectField/TreeSelectField.js +158 -110
- package/dist/inputs/TreeSelectField/utils.d.ts +3 -1
- package/dist/inputs/TreeSelectField/utils.js +21 -3
- package/package.json +2 -2
package/dist/Css.d.ts
CHANGED
|
@@ -3852,10 +3852,22 @@ declare class CssBuilder<T extends Properties> {
|
|
|
3852
3852
|
} & {
|
|
3853
3853
|
textOverflow: import("csstype").Property.TextOverflow | undefined;
|
|
3854
3854
|
}>;
|
|
3855
|
+
/** Sets `fontSize: value`. */
|
|
3856
|
+
fs(value: Properties["fontSize"]): CssBuilder<T & {
|
|
3857
|
+
fontSize: import("csstype").Property.FontSize<string | 0> | undefined;
|
|
3858
|
+
}>;
|
|
3859
|
+
/** Sets `fontSize: px`. */
|
|
3860
|
+
fsPx(px: number): CssBuilder<T & {
|
|
3861
|
+
fontSize: import("csstype").Property.FontSize<string | 0> | undefined;
|
|
3862
|
+
}>;
|
|
3855
3863
|
/** Sets `lineHeight: value`. */
|
|
3856
3864
|
lh(value: Properties["lineHeight"]): CssBuilder<T & {
|
|
3857
3865
|
lineHeight: import("csstype").Property.LineHeight<string | 0> | undefined;
|
|
3858
3866
|
}>;
|
|
3867
|
+
/** Sets `lineHeight: px`. */
|
|
3868
|
+
lhPx(px: number): CssBuilder<T & {
|
|
3869
|
+
lineHeight: import("csstype").Property.LineHeight<string | 0> | undefined;
|
|
3870
|
+
}>;
|
|
3859
3871
|
/** Sets `userSelect: "none"`. */
|
|
3860
3872
|
get usn(): CssBuilder<T & {
|
|
3861
3873
|
userSelect: import("csstype").Property.UserSelect | undefined;
|
package/dist/Css.js
CHANGED
|
@@ -3409,10 +3409,22 @@ class CssBuilder {
|
|
|
3409
3409
|
get truncate() {
|
|
3410
3410
|
return this.add("whiteSpace", "nowrap").add("overflow", "hidden").add("textOverflow", "ellipsis");
|
|
3411
3411
|
}
|
|
3412
|
+
/** Sets `fontSize: value`. */
|
|
3413
|
+
fs(value) {
|
|
3414
|
+
return this.add("fontSize", value);
|
|
3415
|
+
}
|
|
3416
|
+
/** Sets `fontSize: px`. */
|
|
3417
|
+
fsPx(px) {
|
|
3418
|
+
return this.fs(`${px}px`);
|
|
3419
|
+
}
|
|
3412
3420
|
/** Sets `lineHeight: value`. */
|
|
3413
3421
|
lh(value) {
|
|
3414
3422
|
return this.add("lineHeight", value);
|
|
3415
3423
|
}
|
|
3424
|
+
/** Sets `lineHeight: px`. */
|
|
3425
|
+
lhPx(px) {
|
|
3426
|
+
return this.lh(`${px}px`);
|
|
3427
|
+
}
|
|
3416
3428
|
// userSelect
|
|
3417
3429
|
/** Sets `userSelect: "none"`. */
|
|
3418
3430
|
get usn() {
|
|
@@ -38,6 +38,10 @@ export interface TreeSelectFieldProps<O, V extends Value> extends BeamFocusableP
|
|
|
38
38
|
defaultCollapsed?: boolean;
|
|
39
39
|
/** Allow the field's height to grow up to a predefined height (currently 72px), then scroll. */
|
|
40
40
|
multiline?: boolean;
|
|
41
|
+
/** Which selected options to display as chips in the input field when not focused.
|
|
42
|
+
* @default root */
|
|
43
|
+
chipDisplay?: "all" | "leaf" | "root";
|
|
44
|
+
disabledOptions?: V[];
|
|
41
45
|
}
|
|
42
46
|
export declare function TreeSelectField<O, V extends Value>(props: TreeSelectFieldProps<O, V>): JSX.Element;
|
|
43
47
|
export declare function TreeSelectField<O extends HasIdAndName<V>, V extends Value>(props: Optional<TreeSelectFieldProps<O, V>, "getOptionValue" | "getOptionLabel">): JSX.Element;
|
|
@@ -37,6 +37,7 @@ const ListBox_1 = require("../internal/ListBox");
|
|
|
37
37
|
const utils_1 = require("./utils");
|
|
38
38
|
const Value_1 = require("../Value");
|
|
39
39
|
const utils_2 = require("../utils");
|
|
40
|
+
const ComboBoxBase_1 = require("../internal/ComboBoxBase");
|
|
40
41
|
function TreeSelectField(props) {
|
|
41
42
|
const { getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
|
|
42
43
|
getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
|
|
@@ -62,72 +63,70 @@ exports.CollapsedContext = react_1.default.createContext({
|
|
|
62
63
|
getOptionValue: () => ({}),
|
|
63
64
|
});
|
|
64
65
|
function TreeSelectFieldBase(props) {
|
|
65
|
-
var _a, _b;
|
|
66
|
+
var _a, _b, _c;
|
|
66
67
|
const { fieldProps } = (0, PresentationContext_1.usePresentationContext)();
|
|
67
|
-
const { values, options, getOptionValue, getOptionLabel, getOptionMenuLabel = getOptionLabel, disabled, readOnly, labelStyle, borderless, contrast = false, nothingSelectedText = "", onSelect, defaultCollapsed = false, placeholder, fullWidth = (_a = fieldProps === null || fieldProps === void 0 ? void 0 : fieldProps.fullWidth) !== null && _a !== void 0 ? _a : false, ...otherProps } = props;
|
|
68
|
+
const { values, options, getOptionValue, getOptionLabel, getOptionMenuLabel = getOptionLabel, disabled, readOnly, labelStyle, borderless, contrast = false, nothingSelectedText = "", onSelect, defaultCollapsed = false, placeholder, fullWidth = (_a = fieldProps === null || fieldProps === void 0 ? void 0 : fieldProps.fullWidth) !== null && _a !== void 0 ? _a : false, chipDisplay = "root", disabledOptions, ...otherProps } = props;
|
|
68
69
|
const isDisabled = !!disabled;
|
|
69
70
|
const isReadOnly = !!readOnly;
|
|
70
71
|
const initialOptions = Array.isArray(options) ? options : options.current;
|
|
71
72
|
const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
|
|
72
73
|
const { collapsedKeys } = useTreeSelectFieldProvider();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return [option, ...((_b = (_a = option.children) === null || _a === void 0 ? void 0 : _a.flatMap(utils_1.flattenOptions)) !== null && _b !== void 0 ? _b : [])];
|
|
96
|
-
})) !== null && _a !== void 0 ? _a : [];
|
|
97
|
-
const selectedKeys = selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
|
|
98
|
-
// It is possible that all the children of a parent were considered selected `values`, but the parent wasn't.
|
|
74
|
+
// `disabledKeys` from ComboBoxState does not support additional meta for showing a disabled reason to the user
|
|
75
|
+
// This lookup map helps us cleanly prune out the optional reason text, then access it further down the component tree
|
|
76
|
+
const disabledOptionsWithReasons = Object.fromEntries((_b = disabledOptions === null || disabledOptions === void 0 ? void 0 : disabledOptions.map(ComboBoxBase_1.disabledOptionToKeyedTuple)) !== null && _b !== void 0 ? _b : []);
|
|
77
|
+
const initTreeFieldState = (0, react_1.useCallback)(() => {
|
|
78
|
+
// Figure out which options should be considered selected.
|
|
79
|
+
// This is a bit of a complex process because:
|
|
80
|
+
// 1. The `values` array could contain the values of the parent options, but not the children. So those children should be considered selected.
|
|
81
|
+
// 2. The `values` array could contain the values of the children, but not the parent. So the parent should be considered selected if all children are.
|
|
82
|
+
// Create a list of all selected values - using a Set to immediately dedupe the list.
|
|
83
|
+
const selectedKeys = new Set(values === null || values === void 0 ? void 0 : values.flatMap((v) => {
|
|
84
|
+
// Find the options that matches the value. These could be parents or a children.
|
|
85
|
+
const foundOptions = (0, utils_1.findOptions)(initialOptions, (0, Value_1.valueToKey)(v), getOptionValue);
|
|
86
|
+
// Go through the `foundOptions` and get the keys of the options and its children if it has any.
|
|
87
|
+
return foundOptions.flatMap(({ option }) => {
|
|
88
|
+
var _a, _b;
|
|
89
|
+
return [
|
|
90
|
+
(0, Value_1.valueToKey)(getOptionValue(option)),
|
|
91
|
+
...((_b = (_a = option.children) === null || _a === void 0 ? void 0 : _a.flatMap((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _b !== void 0 ? _b : []),
|
|
92
|
+
];
|
|
93
|
+
});
|
|
94
|
+
}));
|
|
95
|
+
// It is possible that all the children of a parent were considered selected `values`, but the parent wasn't included in the `values` array.
|
|
99
96
|
// In this case, the parent also should be considered a selected option.
|
|
100
97
|
function areAllChildrenSelected(maybeParent) {
|
|
101
|
-
const isSelected = selectedKeys.
|
|
102
|
-
//
|
|
103
|
-
if (isSelected)
|
|
104
|
-
return true;
|
|
105
|
-
// If we do not have children, then return the state of this leaf node.
|
|
106
|
-
if (!maybeParent.children || maybeParent.children.length === 0)
|
|
98
|
+
const isSelected = selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
|
|
99
|
+
// If already selected, or the options does not have children, then return its current state.
|
|
100
|
+
if (isSelected || !maybeParent.children || maybeParent.children.length === 0)
|
|
107
101
|
return isSelected;
|
|
108
102
|
// If we do have children, then check if all children are selected.
|
|
109
|
-
// if all are selected, then push this parent to the selectedKeys
|
|
103
|
+
// if all are selected, then push this parent to the selectedKeys
|
|
110
104
|
const areAllSelected = maybeParent.children.every(areAllChildrenSelected);
|
|
111
105
|
if (areAllSelected) {
|
|
112
|
-
selectedKeys.
|
|
113
|
-
selectedOptions.push(maybeParent);
|
|
106
|
+
selectedKeys.add((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
|
|
114
107
|
}
|
|
115
108
|
return areAllSelected;
|
|
116
109
|
}
|
|
117
110
|
initialOptions.forEach(areAllChildrenSelected);
|
|
118
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
return
|
|
124
|
-
return
|
|
125
|
-
};
|
|
126
|
-
|
|
111
|
+
// Using the `selectedKeys` Set, find the corresponding options that are selected. As it's a Set, its already deduped.
|
|
112
|
+
const selectedOptions = [...selectedKeys].flatMap((key) => {
|
|
113
|
+
// Find the first matching option, then this list will also be already deduped.
|
|
114
|
+
const maybeOption = (0, utils_1.findOption)(initialOptions, key, getOptionValue);
|
|
115
|
+
if (!maybeOption)
|
|
116
|
+
return [];
|
|
117
|
+
return [maybeOption.option];
|
|
118
|
+
});
|
|
119
|
+
// Store a list of the selected option labels so we can quickly refer to them later
|
|
120
|
+
const selectedOptionsLabels = chipDisplay === "root"
|
|
121
|
+
? initialOptions.flatMap((o) => getTopLevelSelections(o, selectedKeys, getOptionValue)).map(getOptionLabel)
|
|
122
|
+
: chipDisplay === "leaf"
|
|
123
|
+
? selectedOptions.filter((o) => !o.children || o.children.length === 0).map(getOptionLabel)
|
|
124
|
+
: selectedOptions.map(getOptionLabel);
|
|
125
|
+
const filteredOptions = initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue));
|
|
127
126
|
return {
|
|
128
|
-
selectedKeys,
|
|
127
|
+
selectedKeys: [...selectedKeys],
|
|
129
128
|
inputValue: selectedOptions.length === 1
|
|
130
|
-
? getOptionLabel(selectedOptions[0])
|
|
129
|
+
? getOptionLabel([...selectedOptions][0])
|
|
131
130
|
: isReadOnly && selectedOptions.length > 0
|
|
132
131
|
? selectedOptionsLabels.join(", ")
|
|
133
132
|
: selectedOptions.length === 0
|
|
@@ -140,7 +139,28 @@ function TreeSelectFieldBase(props) {
|
|
|
140
139
|
optionsLoading: false,
|
|
141
140
|
allowCollapsing: true,
|
|
142
141
|
};
|
|
143
|
-
}
|
|
142
|
+
}, [
|
|
143
|
+
initialOptions,
|
|
144
|
+
values,
|
|
145
|
+
chipDisplay,
|
|
146
|
+
getOptionLabel,
|
|
147
|
+
isReadOnly,
|
|
148
|
+
nothingSelectedText,
|
|
149
|
+
collapsedKeys,
|
|
150
|
+
getOptionValue,
|
|
151
|
+
]);
|
|
152
|
+
// Initialize the TreeFieldState
|
|
153
|
+
const [fieldState, setFieldState] = (0, react_1.useState)(() => initTreeFieldState());
|
|
154
|
+
// Reset the TreeFieldState if the values array changes and doesn't match the selectedOptions
|
|
155
|
+
(0, react_1.useEffect)(() => {
|
|
156
|
+
// if the values does not match the values in the fieldState, then update the fieldState
|
|
157
|
+
const selectedKeys = fieldState.selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
|
|
158
|
+
if (values &&
|
|
159
|
+
(values.length !== selectedKeys.length || !values.every((v) => selectedKeys.includes((0, Value_1.valueToKey)(v))))) {
|
|
160
|
+
setFieldState(initTreeFieldState());
|
|
161
|
+
}
|
|
162
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
163
|
+
}, [getOptionValue, initTreeFieldState, values]);
|
|
144
164
|
// React to collapsed keys and update the filtered options
|
|
145
165
|
const reactToCollapse = (0, react_1.useRef)(false);
|
|
146
166
|
(0, react_1.useEffect)(() => {
|
|
@@ -150,12 +170,12 @@ function TreeSelectFieldBase(props) {
|
|
|
150
170
|
allOptions,
|
|
151
171
|
inputValue,
|
|
152
172
|
...others,
|
|
153
|
-
filteredOptions: allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), inputValue))),
|
|
173
|
+
filteredOptions: allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue))),
|
|
154
174
|
}));
|
|
155
175
|
}
|
|
156
176
|
reactToCollapse.current = true;
|
|
157
177
|
},
|
|
158
|
-
//
|
|
178
|
+
// Only react to collapseKey changes. Other deps should be stable (`contains`, `getOptionLabel`, `getOptionValue`).
|
|
159
179
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
160
180
|
[collapsedKeys]);
|
|
161
181
|
// Update the filtered options when the input value changes
|
|
@@ -165,19 +185,16 @@ function TreeSelectFieldBase(props) {
|
|
|
165
185
|
...prevState,
|
|
166
186
|
inputValue,
|
|
167
187
|
allowCollapsing: inputValue.length === 0,
|
|
168
|
-
filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), inputValue))),
|
|
188
|
+
filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue))),
|
|
169
189
|
};
|
|
170
190
|
});
|
|
171
|
-
},
|
|
172
|
-
// TODO: validate this eslint-disable. It was automatically ignored as part of https://app.shortcut.com/homebound-team/story/40033/enable-react-hooks-exhaustive-deps-for-react-projects
|
|
173
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
174
|
-
[setFieldState]);
|
|
191
|
+
}, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
|
|
175
192
|
// Handle loading of the options in the case that they are loaded via a Promise.
|
|
176
193
|
const maybeInitLoad = (0, react_1.useCallback)(async (options, fieldState, setFieldState) => {
|
|
177
194
|
if (!Array.isArray(options)) {
|
|
178
195
|
setFieldState((prevState) => ({ ...prevState, optionsLoading: true }));
|
|
179
196
|
const loadedOptions = (await options.load()).options;
|
|
180
|
-
const filteredOptions = loadedOptions.flatMap((o) => levelOptions(o, 0, fieldState.inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), fieldState.inputValue)));
|
|
197
|
+
const filteredOptions = loadedOptions.flatMap((o) => levelOptions(o, 0, fieldState.inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), fieldState.inputValue)));
|
|
181
198
|
// Ensure the `unset` option is prepended to the top of the list if `unsetLabel` was provided
|
|
182
199
|
setFieldState((prevState) => ({
|
|
183
200
|
...prevState,
|
|
@@ -186,10 +203,7 @@ function TreeSelectFieldBase(props) {
|
|
|
186
203
|
optionsLoading: false,
|
|
187
204
|
}));
|
|
188
205
|
}
|
|
189
|
-
},
|
|
190
|
-
// TODO: validate this eslint-disable. It was automatically ignored as part of https://app.shortcut.com/homebound-team/story/40033/enable-react-hooks-exhaustive-deps-for-react-projects
|
|
191
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
192
|
-
[]);
|
|
206
|
+
}, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
|
|
193
207
|
// Only on the first open of the listbox, we want to load the options if they haven't been loaded yet.
|
|
194
208
|
const firstOpen = (0, react_1.useRef)(true);
|
|
195
209
|
function onOpenChange(isOpen) {
|
|
@@ -204,13 +218,14 @@ function TreeSelectFieldBase(props) {
|
|
|
204
218
|
setFieldState((prevState) => ({
|
|
205
219
|
...prevState,
|
|
206
220
|
inputValue: "",
|
|
207
|
-
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0)),
|
|
221
|
+
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
|
|
208
222
|
}));
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
// This is _always_ going to appear new. Maybe `useMemo`?
|
|
212
226
|
const comboBoxProps = {
|
|
213
227
|
...otherProps,
|
|
228
|
+
disabledKeys: Object.keys(disabledOptionsWithReasons),
|
|
214
229
|
placeholder: !values || values.length === 0 ? placeholder : "",
|
|
215
230
|
label: props.label,
|
|
216
231
|
inputValue: fieldState.inputValue,
|
|
@@ -222,7 +237,7 @@ function TreeSelectFieldBase(props) {
|
|
|
222
237
|
onInputChange,
|
|
223
238
|
onOpenChange,
|
|
224
239
|
children: ([item]) => (
|
|
225
|
-
// what we're telling it to render. look at padding here -
|
|
240
|
+
// what we're telling it to render. look at padding here - don't have to pass down to tree option - filtered options is where we're flat mapping
|
|
226
241
|
(0, jsx_runtime_1.jsx)(react_stately_1.Item, { textValue: getOptionLabel(item), children: getOptionMenuLabel(item) }, (0, Value_1.valueToKey)(getOptionValue(item)))),
|
|
227
242
|
};
|
|
228
243
|
const state = (0, react_stately_1.useComboBoxState)({
|
|
@@ -266,76 +281,87 @@ function TreeSelectFieldBase(props) {
|
|
|
266
281
|
// For added keys, we need to see if any other options should be added as well.
|
|
267
282
|
[...addedKeys].forEach((key) => {
|
|
268
283
|
var _a;
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
284
|
+
const maybeOptions = (0, utils_1.findOptions)(fieldState.allOptions, key, getOptionValue);
|
|
285
|
+
if (maybeOptions.length === 0)
|
|
271
286
|
return;
|
|
272
|
-
const { option, parents }
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
for (const { option, parents } of maybeOptions) {
|
|
288
|
+
// If the newly added option has children, then consider the children to be newly added keys as well.
|
|
289
|
+
if (option && option.children && option.children.length > 0) {
|
|
290
|
+
const childrenKeys = option.children
|
|
291
|
+
.flatMap(utils_1.flattenOptions)
|
|
292
|
+
.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))
|
|
293
|
+
.filter((childKey) => {
|
|
294
|
+
// remove children that are disabled
|
|
295
|
+
return !state.disabledKeys.has(childKey);
|
|
296
|
+
});
|
|
297
|
+
[key, ...childrenKeys].forEach(addedKeys.add, addedKeys);
|
|
298
|
+
}
|
|
299
|
+
// If this was a child that was selected, then see if every other child is also selected, and if so, consider the parent selected.
|
|
300
|
+
// Walk up the parents and determine their state.
|
|
301
|
+
for (const parent of parents.reverse()) {
|
|
302
|
+
const allChecked = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.every((child) => {
|
|
303
|
+
const childKey = (0, Value_1.valueToKey)(getOptionValue(child));
|
|
304
|
+
return addedKeys.has(childKey) || existingKeys.has(childKey) || state.disabledKeys.has(childKey);
|
|
305
|
+
});
|
|
306
|
+
if (allChecked) {
|
|
307
|
+
addedKeys.add((0, Value_1.valueToKey)(getOptionValue(parent)));
|
|
308
|
+
}
|
|
287
309
|
}
|
|
288
310
|
}
|
|
289
311
|
});
|
|
290
312
|
// For removed keys, we need to also unselect any children and parents of the removed key
|
|
291
313
|
[...removedKeys].forEach((key) => {
|
|
292
314
|
// Grab the option and parents of the option that was removed
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
315
|
+
const maybeOptions = (0, utils_1.findOptions)(fieldState.allOptions, key, getOptionValue);
|
|
316
|
+
if (maybeOptions.length === 0)
|
|
295
317
|
return;
|
|
296
|
-
const { option, parents }
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
for (const { option, parents } of maybeOptions) {
|
|
319
|
+
// If the option has children, then we need to unselect those children as well
|
|
320
|
+
if (option.children && option.children.length > 0) {
|
|
321
|
+
// Ensure we do not impact children that are disabled. They shouldn't be able to toggled on/off
|
|
322
|
+
const childrenKeys = option.children
|
|
323
|
+
.flatMap(utils_1.flattenOptions)
|
|
324
|
+
.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))
|
|
325
|
+
.filter((key) => !state.disabledKeys.has(key));
|
|
326
|
+
[key, ...childrenKeys].forEach(removedKeys.add, removedKeys);
|
|
327
|
+
}
|
|
328
|
+
// If the option has parents, then we need to unselect those parents as well
|
|
329
|
+
if (parents.length > 0) {
|
|
330
|
+
const parentKeys = parents.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
|
|
331
|
+
[key, ...parentKeys].forEach(removedKeys.add, removedKeys);
|
|
332
|
+
}
|
|
306
333
|
}
|
|
307
334
|
});
|
|
308
335
|
// Create the lists to update our TreeState with
|
|
309
336
|
const selectedKeys = new Set([...existingKeys, ...addedKeys].filter((x) => !removedKeys.has(x)));
|
|
310
|
-
const selectedOptions =
|
|
311
|
-
.
|
|
312
|
-
.
|
|
337
|
+
const selectedOptions = [...selectedKeys]
|
|
338
|
+
// Find the first option as it could be duplicated, but we only want one of them.
|
|
339
|
+
.map((key) => { var _a; return (_a = (0, utils_1.findOption)(fieldState.allOptions, key, getOptionValue)) === null || _a === void 0 ? void 0 : _a.option; })
|
|
340
|
+
.filter((o) => o); // filter out undefined
|
|
313
341
|
// Our `onSelect` callback provides three things:
|
|
314
342
|
// 1. ALL selected values + options
|
|
315
343
|
// 2. Only the top level selected values + options
|
|
316
344
|
// 3. Only the leaf selected values + options
|
|
317
345
|
// For the top level and leaf selections, we need to do some extra work to determine which options are the top level and leaf.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return [o];
|
|
322
|
-
// If this element has no children, then look through the children for top level selected options.
|
|
323
|
-
if (o.children)
|
|
324
|
-
return [...o.children.flatMap(getTopLevelSelections)];
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
const rootOptions = fieldState.allOptions.flatMap(getTopLevelSelections);
|
|
346
|
+
const allRootOptions = fieldState.allOptions.flatMap((o) => getTopLevelSelections(o, selectedKeys, getOptionValue));
|
|
347
|
+
// dedupe selected root options
|
|
348
|
+
const rootOptions = allRootOptions.filter((o, idx, self) => idx === self.findIndex((t) => getOptionValue(o) === getOptionValue(t)));
|
|
328
349
|
const rootValues = rootOptions.map(getOptionValue);
|
|
329
350
|
// Finally get the list of options that are just the "leaf" options, meaning they have no children.
|
|
330
351
|
const leafOptions = selectedOptions.filter((o) => !o.children || o.children.length === 0);
|
|
331
352
|
const leafValues = leafOptions.map(getOptionValue);
|
|
332
353
|
setFieldState((prevState) => ({
|
|
333
354
|
...prevState,
|
|
334
|
-
inputValue
|
|
335
|
-
|
|
355
|
+
// Since we reset the list of options upon selection changes, then set the `inputValue` to empty string to reflect that.
|
|
356
|
+
inputValue: "",
|
|
357
|
+
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
|
|
336
358
|
selectedKeys: [...selectedKeys],
|
|
337
359
|
selectedOptions,
|
|
338
|
-
selectedOptionsLabels:
|
|
360
|
+
selectedOptionsLabels: chipDisplay === "root"
|
|
361
|
+
? rootOptions.map(getOptionLabel)
|
|
362
|
+
: chipDisplay === "leaf"
|
|
363
|
+
? leafOptions.map(getOptionLabel)
|
|
364
|
+
: selectedOptions.map(getOptionLabel),
|
|
339
365
|
}));
|
|
340
366
|
onSelect({
|
|
341
367
|
all: { values: [...selectedKeys].map((key) => (0, Value_1.keyToValue)(key)), options: selectedOptions },
|
|
@@ -357,7 +383,7 @@ function TreeSelectFieldBase(props) {
|
|
|
357
383
|
: selectedOptions.length === 0
|
|
358
384
|
? nothingSelectedText
|
|
359
385
|
: "",
|
|
360
|
-
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0)),
|
|
386
|
+
filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
|
|
361
387
|
allowCollapsing: true,
|
|
362
388
|
}));
|
|
363
389
|
}
|
|
@@ -389,10 +415,32 @@ function TreeSelectFieldBase(props) {
|
|
|
389
415
|
});
|
|
390
416
|
positionProps.style = {
|
|
391
417
|
...positionProps.style,
|
|
392
|
-
width: (
|
|
418
|
+
width: (_c = comboBoxRef === null || comboBoxRef === void 0 ? void 0 : comboBoxRef.current) === null || _c === void 0 ? void 0 : _c.clientWidth,
|
|
393
419
|
// Ensures the menu never gets too small.
|
|
394
420
|
minWidth: 200,
|
|
395
421
|
};
|
|
396
422
|
const fieldMaxWidth = (0, utils_2.getFieldWidth)(fullWidth);
|
|
397
423
|
return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw(fieldMaxWidth).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, fullWidth: fullWidth, labelStyle: labelStyle, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, selectedOptionsLabels: fieldState.selectedOptionsLabels, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField, nothingSelectedText: nothingSelectedText, isTree: true }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200, children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, { ...listBoxProps, positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, horizontalLayout: labelStyle === "left", loading: fieldState.optionsLoading, allowCollapsing: fieldState.allowCollapsing, isTree: true }) }))] }));
|
|
398
424
|
}
|
|
425
|
+
function levelOptions(o, level, filtering, collapsedKeys, getOptionValue) {
|
|
426
|
+
var _a;
|
|
427
|
+
// If a user is filtering, then do not provide level to the options as the various paddings may look quite odd.
|
|
428
|
+
const actualLevel = filtering ? 0 : level;
|
|
429
|
+
return [
|
|
430
|
+
[o, actualLevel],
|
|
431
|
+
...(((_a = o.children) === null || _a === void 0 ? void 0 : _a.length) && !collapsedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o)))
|
|
432
|
+
? o.children.flatMap((oc) => levelOptions(oc, actualLevel + 1, filtering, collapsedKeys, getOptionValue))
|
|
433
|
+
: []),
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
// Given an option and the selected keys, it will return the top most selected option in the option's tree.
|
|
437
|
+
// This could be a parent of the option, or the option itself.
|
|
438
|
+
function getTopLevelSelections(o, selectedKeys, getOptionValue) {
|
|
439
|
+
// If this element is already selected, return early. Do not bother looking through children.
|
|
440
|
+
if (selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(o))))
|
|
441
|
+
return [o];
|
|
442
|
+
// If this element has no children, then look through the children for top level selected options.
|
|
443
|
+
if (o.children)
|
|
444
|
+
return [...o.children.flatMap((c) => getTopLevelSelections(c, selectedKeys, getOptionValue))];
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
@@ -42,8 +42,10 @@ export type TreeSelectResponse<O, V extends Value> = {
|
|
|
42
42
|
options: O[];
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
-
/** Finds
|
|
45
|
+
/** Finds first option by Key, and returns it + any parents. */
|
|
46
46
|
export declare function findOption<O, V extends Value>(options: NestedOption<O>[], key: Key, getOptionValue: (o: O) => V): FoundOption<O> | undefined;
|
|
47
|
+
/** Finds all options by Key, and returns it + any parents. */
|
|
48
|
+
export declare function findOptions<O, V extends Value>(options: NestedOption<O>[], key: Key, getOptionValue: (o: O) => V): FoundOption<O>[];
|
|
47
49
|
export declare function flattenOptions<O>(o: NestedOption<O>): NestedOption<O>[];
|
|
48
50
|
export declare function isLeveledOption<O>(option: LeveledOption<O> | any): option is LeveledOption<O>;
|
|
49
51
|
export declare function isLeveledNode<O>(node: Node<O> | Node<LeveledOption<O>>): node is Node<LeveledOption<O>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isLeveledNode = exports.isLeveledOption = exports.flattenOptions = exports.findOption = void 0;
|
|
4
|
-
/** Finds
|
|
3
|
+
exports.isLeveledNode = exports.isLeveledOption = exports.flattenOptions = exports.findOptions = exports.findOption = void 0;
|
|
4
|
+
/** Finds first option by Key, and returns it + any parents. */
|
|
5
5
|
function findOption(options, key, getOptionValue) {
|
|
6
|
-
// This is technically an array of "maybe
|
|
6
|
+
// This is technically an array of "maybe FoundOption"
|
|
7
7
|
const todo = options.map((option) => ({ option, parents: [] }));
|
|
8
8
|
while (todo.length > 0) {
|
|
9
9
|
const curr = todo.pop();
|
|
@@ -18,6 +18,24 @@ function findOption(options, key, getOptionValue) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
exports.findOption = findOption;
|
|
21
|
+
/** Finds all options by Key, and returns it + any parents. */
|
|
22
|
+
function findOptions(options, key, getOptionValue) {
|
|
23
|
+
// This is technically an array of "maybe FoundOption"
|
|
24
|
+
const todo = options.map((option) => ({ option, parents: [] }));
|
|
25
|
+
const found = [];
|
|
26
|
+
while (todo.length > 0) {
|
|
27
|
+
const curr = todo.pop();
|
|
28
|
+
if (getOptionValue(curr.option) === key) {
|
|
29
|
+
found.push(curr);
|
|
30
|
+
}
|
|
31
|
+
else if (curr.option.children) {
|
|
32
|
+
// Search our children and pass along us as the parent
|
|
33
|
+
todo.push(...curr.option.children.map((option) => ({ option, parents: [...curr.parents, curr.option] })));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
exports.findOptions = findOptions;
|
|
21
39
|
function flattenOptions(o) {
|
|
22
40
|
var _a;
|
|
23
41
|
return [o, ...(((_a = o.children) === null || _a === void 0 ? void 0 : _a.length) ? o.children.flatMap((oc) => flattenOptions(oc)) : [])];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homebound/beam",
|
|
3
|
-
"version": "2.354.
|
|
3
|
+
"version": "2.354.4",
|
|
4
4
|
"author": "Homebound",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@homebound/eslint-config": "^1.10.2",
|
|
79
79
|
"@homebound/rtl-react-router-utils": "1.0.3",
|
|
80
80
|
"@homebound/rtl-utils": "^2.65.0",
|
|
81
|
-
"@homebound/truss": "^1.
|
|
81
|
+
"@homebound/truss": "^1.136.0",
|
|
82
82
|
"@homebound/tsconfig": "^1.0.3",
|
|
83
83
|
"@semantic-release/exec": "^6.0.3",
|
|
84
84
|
"@semantic-release/git": "^10.0.1",
|