@homebound/beam 2.354.2 → 2.354.3

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.
@@ -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
- function levelOptions(o, level, filtering) {
74
- var _a;
75
- // If a user is filtering, then do not provide level to the options as the various paddings may look quite odd.
76
- const actualLevel = filtering ? 0 : level;
77
- return [
78
- [o, actualLevel],
79
- ...(((_a = o.children) === null || _a === void 0 ? void 0 : _a.length) && !collapsedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o)))
80
- ? o.children.flatMap((oc) => levelOptions(oc, actualLevel + 1, filtering))
81
- : []),
82
- ];
83
- }
84
- // Initialize the TreeFieldState
85
- const [fieldState, setFieldState] = (0, react_1.useState)(() => {
86
- var _a;
87
- const filteredOptions = initialOptions.flatMap((o) => levelOptions(o, 0));
88
- const selectedOptions = (_a = values === null || values === void 0 ? void 0 : values.flatMap((v) => {
89
- var _a, _b;
90
- const maybeOption = (0, utils_1.findOption)(initialOptions, (0, Value_1.valueToKey)(v), getOptionValue);
91
- if (!maybeOption)
92
- return [];
93
- const { option } = maybeOption;
94
- // If the selected key has children then all children should also be considered selected.
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.includes((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
102
- // if this key is already selected, then return true
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 and selectedOptions
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.push((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
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
- // Given a child option, determine if the parent is selected.
119
- const isParentSelected = (option) => {
120
- var _a;
121
- const parents = (_a = (0, utils_1.findOption)(initialOptions, (0, Value_1.valueToKey)(getOptionValue(option)), getOptionValue)) === null || _a === void 0 ? void 0 : _a.parents;
122
- if (!parents)
123
- return false;
124
- return parents.some((parent) => selectedKeys.includes((0, Value_1.valueToKey)(getOptionValue(parent))));
125
- };
126
- const selectedOptionsLabels = selectedOptions.filter((o) => !isParentSelected(o)).map(getOptionLabel);
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
- // 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
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 - dont have to pass down to tree option - filtered options is where we're flat mapping
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 maybeOption = (0, utils_1.findOption)(fieldState.allOptions, key, getOptionValue);
270
- if (!maybeOption)
284
+ const maybeOptions = (0, utils_1.findOptions)(fieldState.allOptions, key, getOptionValue);
285
+ if (maybeOptions.length === 0)
271
286
  return;
272
- const { option, parents } = maybeOption;
273
- // If the newly added option has children, then consider the children to be newly added keys as well.
274
- if (option && option.children && option.children.length > 0) {
275
- const childrenKeys = option.children.flatMap(utils_1.flattenOptions).map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
276
- [key, ...childrenKeys].forEach(addedKeys.add, addedKeys);
277
- }
278
- // If this was a child that was selected, then see if every other child is also selected, and if so, consider the parent selected.
279
- // Walk up the parents and determine their state.
280
- for (const parent of parents.reverse()) {
281
- const allChecked = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.every((child) => {
282
- const childKey = (0, Value_1.valueToKey)(getOptionValue(child));
283
- return addedKeys.has(childKey) || existingKeys.has(childKey);
284
- });
285
- if (allChecked) {
286
- addedKeys.add((0, Value_1.valueToKey)(getOptionValue(parent)));
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 maybeOption = (0, utils_1.findOption)(fieldState.allOptions, key, getOptionValue);
294
- if (!maybeOption)
315
+ const maybeOptions = (0, utils_1.findOptions)(fieldState.allOptions, key, getOptionValue);
316
+ if (maybeOptions.length === 0)
295
317
  return;
296
- const { option, parents } = maybeOption;
297
- // If the option has children, then we need to unselect those children as well
298
- if (option.children && option.children.length > 0) {
299
- const childrenKeys = option.children.flatMap(utils_1.flattenOptions).map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
300
- [key, ...childrenKeys].forEach(removedKeys.add, removedKeys);
301
- }
302
- // If the option has parents, then we need to unselect those parents as well
303
- if (parents.length > 0) {
304
- const parentKeys = parents.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
305
- [key, ...parentKeys].forEach(removedKeys.add, removedKeys);
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 = fieldState.allOptions
311
- .flatMap(utils_1.flattenOptions)
312
- .filter((o) => selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(o))));
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
- function getTopLevelSelections(o) {
319
- // If this element is already selected, return early. Do not bother looking through children.
320
- if (selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(o))))
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: nothingSelectedText,
335
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0)),
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: rootOptions.map(getOptionLabel),
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: (_b = comboBoxRef === null || comboBoxRef === void 0 ? void 0 : comboBoxRef.current) === null || _b === void 0 ? void 0 : _b.clientWidth,
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 an option by Key, and returns it + any parents. */
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 an option by Key, and returns it + any parents. */
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 FoundRow"
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.2",
3
+ "version": "2.354.3",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",