@homebound/beam 3.0.6 → 3.1.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/index.cjs CHANGED
@@ -10952,7 +10952,8 @@ function TreeOption(props) {
10952
10952
  const {
10953
10953
  collapsedKeys,
10954
10954
  setCollapsedKeys,
10955
- getOptionValue
10955
+ getOptionValue,
10956
+ groupKeys
10956
10957
  } = useTreeSelectFieldProvider();
10957
10958
  const {
10958
10959
  optionProps,
@@ -10964,6 +10965,12 @@ function TreeOption(props) {
10964
10965
  shouldSelectOnPressUp: true,
10965
10966
  shouldFocusOnHover: false
10966
10967
  }, state, ref);
10968
+ const isGroup = groupKeys.includes(item.key);
10969
+ const canCollapse = allowCollapsing && !!option.children?.length;
10970
+ function toggleCollapsed() {
10971
+ if (!canCollapse) return;
10972
+ setCollapsedKeys((prevKeys) => collapsedKeys.includes(item.key) ? prevKeys.filter((k) => k !== item.key) : [...prevKeys, item.key]);
10973
+ }
10967
10974
  const isIndeterminate = !isSelected && option.children?.some((o) => hasSelectedChildren(o, state, getOptionValue));
10968
10975
  const listItemStyles = {
10969
10976
  item: {
@@ -10991,7 +10998,12 @@ function TreeOption(props) {
10991
10998
  }]
10992
10999
  }
10993
11000
  };
10994
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { ...hoverProps, ...(0, import_runtime36.trussProps)({
11001
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { ...hoverProps, onClick: (e) => {
11002
+ if (!isGroup) return;
11003
+ e.preventDefault();
11004
+ e.stopPropagation();
11005
+ toggleCollapsed();
11006
+ }, ...(0, import_runtime36.trussProps)({
10995
11007
  ...{
10996
11008
  display: "df",
10997
11009
  alignItems: "aic",
@@ -11008,18 +11020,18 @@ function TreeOption(props) {
11008
11020
  lineHeight: "lh_20px"
11009
11021
  },
11010
11022
  ...listItemStyles.item,
11011
- ...isHovered && !isDisabled ? listItemStyles.hover : {},
11012
- ...isFocused ? listItemStyles.focus : {},
11013
- ...isDisabled ? listItemStyles.disabled : {}
11023
+ ...isHovered && (!isDisabled || isGroup) ? listItemStyles.hover : {},
11024
+ ...isFocused && !isGroup ? listItemStyles.focus : {},
11025
+ ...isDisabled && !isGroup ? listItemStyles.disabled : {}
11014
11026
  }), children: [
11015
- allowCollapsing && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "w_18px fs0 df aic", children: option.children && option.children?.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { onClick: (e) => {
11027
+ allowCollapsing && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { className: "w_18px fs0 df aic", children: canCollapse && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("button", { onClick: (e) => {
11016
11028
  e.preventDefault();
11017
11029
  e.stopPropagation();
11018
- setCollapsedKeys((prevKeys) => collapsedKeys.includes(item.key) ? prevKeys.filter((k) => k !== item.key) : [...prevKeys, item.key]);
11030
+ toggleCollapsed();
11019
11031
  return false;
11020
11032
  }, className: "br4 h_16px w_16px bgTransparent h_bgGray300", ...tid[`collapseToggle_${item.key}`], children: /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(Icon, { icon: collapsedKeys.includes(item.key) ? "triangleRight" : "triangleDown", inc: 2 }) }) }),
11021
11033
  /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "df aic gap1 h100 fg1 pt1 pb1 pr2", ref, ...optionProps, "data-label": item.textValue, children: [
11022
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(StyledCheckbox, { isDisabled, isSelected, isIndeterminate, ...tid[item.key.toString()] }),
11034
+ !isGroup && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(StyledCheckbox, { isDisabled, isSelected, isIndeterminate, ...tid[item.key.toString()] }),
11023
11035
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "pl1", children: item.rendered })
11024
11036
  ] })
11025
11037
  ] });
@@ -11102,9 +11114,14 @@ function VirtualizedOptions(props) {
11102
11114
  totalListHeightChanged: onListHeightChange,
11103
11115
  totalCount: items.length,
11104
11116
  ...process.env.NODE_ENV === "test" ? {
11105
- // We don't really need to set this, but it's handy for tests, which would
11106
- // otherwise render just 1 row. A better way to do this would be to jest.mock
11107
- // out Virtuoso with an impl that just rendered everything, but doing this for now.
11117
+ // In tests, we render all rows so assertions can see expands/async-loaded items. However,
11118
+ // the `initialItemCount` (next prop) is only applied on amount, so we set `key={items.length}`
11119
+ // to force a remount when our list changes -- and we only want/need this in tests, b/c otherwise
11120
+ // in production a Virtuoso remount causes visible flashing.
11121
+ key: items.length,
11122
+ // We don't really need to set this, but it's handy for tests, which would otherwise render
11123
+ // just 1 row. A better way to do this would be to jest.mock out Virtuoso with an impl that
11124
+ // just rendered everything, but doing this for now.
11108
11125
  initialItemCount: items.length
11109
11126
  } : {
11110
11127
  // Ensure the selected item is visible when the list renders
@@ -11144,8 +11161,7 @@ function VirtualizedOptions(props) {
11144
11161
  components: !loading ? {} : {
11145
11162
  Footer: typeof loading === "function" ? loading : () => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(LoadingDots, { contrast })
11146
11163
  }
11147
- },
11148
- items.length
11164
+ }
11149
11165
  );
11150
11166
  }
11151
11167
 
@@ -11318,6 +11334,7 @@ function TreeSelectField(props) {
11318
11334
  ...otherProps
11319
11335
  } = props;
11320
11336
  const [collapsedKeys, setCollapsedKeys] = (0, import_react44.useState)([]);
11337
+ const groupKeys = (0, import_react44.useMemo)(() => props.groupOptions?.map((option) => valueToKey(option)) ?? [], [props.groupOptions]);
11321
11338
  (0, import_react44.useEffect)(() => {
11322
11339
  setCollapsedKeys(!Array.isArray(options) ? [] : defaultCollapsed ? options.map((o) => getOptionValue(o)) : options.flatMap(flattenOptions).filter((o) => o.defaultCollapsed).map((o) => getOptionValue(o)));
11323
11340
  }, [options, defaultCollapsed]);
@@ -11325,11 +11342,12 @@ function TreeSelectField(props) {
11325
11342
  () => ({
11326
11343
  collapsedKeys,
11327
11344
  setCollapsedKeys,
11328
- getOptionValue
11345
+ getOptionValue,
11346
+ groupKeys
11329
11347
  }),
11330
11348
  // 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
11331
11349
  // eslint-disable-next-line react-hooks/exhaustive-deps
11332
- [collapsedKeys, setCollapsedKeys]
11350
+ [collapsedKeys, setCollapsedKeys, groupKeys]
11333
11351
  );
11334
11352
  return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(CollapsedContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(TreeSelectFieldBase, { ...otherProps, options, getOptionLabel, getOptionValue, values, onSelect: ({
11335
11353
  all,
@@ -11350,7 +11368,8 @@ var CollapsedContext = import_react44.default.createContext({
11350
11368
  collapsedKeys: [],
11351
11369
  setCollapsedKeys: () => {
11352
11370
  },
11353
- getOptionValue: () => ({})
11371
+ getOptionValue: () => ({}),
11372
+ groupKeys: []
11354
11373
  });
11355
11374
  function TreeSelectFieldBase(props) {
11356
11375
  const {
@@ -11369,13 +11388,15 @@ function TreeSelectFieldBase(props) {
11369
11388
  contrast = false,
11370
11389
  nothingSelectedText = "",
11371
11390
  onSelect,
11372
- defaultCollapsed = false,
11391
+ defaultCollapsed: _defaultCollapsed = false,
11373
11392
  placeholder,
11374
11393
  fullWidth = fieldProps?.fullWidth ?? false,
11375
11394
  chipDisplay = "root",
11376
11395
  disabledOptions,
11396
+ groupOptions: _groupOptions,
11377
11397
  ...otherProps
11378
11398
  } = props;
11399
+ void _defaultCollapsed;
11379
11400
  const isDisabled = !!disabled;
11380
11401
  const isReadOnly = !!readOnly;
11381
11402
  const initialOptions = Array.isArray(options) ? options : options.current;
@@ -11387,7 +11408,10 @@ function TreeSelectFieldBase(props) {
11387
11408
  const {
11388
11409
  collapsedKeys
11389
11410
  } = useTreeSelectFieldProvider();
11411
+ const groupKeys = (0, import_react44.useMemo)(() => _groupOptions?.map((option) => valueToKey(option)) ?? [], [_groupOptions]);
11412
+ const groupKeySet = (0, import_react44.useMemo)(() => new Set(groupKeys), [groupKeys]);
11390
11413
  const disabledOptionsWithReasons = Object.fromEntries(disabledOptions?.map(disabledOptionToKeyedTuple) ?? []);
11414
+ const disabledKeys = [.../* @__PURE__ */ new Set([...Object.keys(disabledOptionsWithReasons), ...groupKeys])];
11391
11415
  const initTreeFieldState = (0, import_react44.useCallback)(() => {
11392
11416
  const selectedKeys = new Set(values?.flatMap((v) => {
11393
11417
  const foundOptions = findOptions(initialOptions, valueToKey(v), getOptionValue);
@@ -11396,14 +11420,16 @@ function TreeSelectFieldBase(props) {
11396
11420
  }) => selectOptionAndAllChildren(option));
11397
11421
  }));
11398
11422
  function selectOptionAndAllChildren(maybeParent) {
11399
- return [valueToKey(getOptionValue(maybeParent)), ...maybeParent.children?.flatMap(selectOptionAndAllChildren) ?? []];
11423
+ const key = valueToKey(getOptionValue(maybeParent));
11424
+ return [...groupKeySet.has(key) ? [] : [key], ...maybeParent.children?.flatMap(selectOptionAndAllChildren) ?? []];
11400
11425
  }
11401
11426
  function areAllChildrenSelected(maybeParent) {
11402
- const isSelected = selectedKeys.has(valueToKey(getOptionValue(maybeParent)));
11427
+ const key = valueToKey(getOptionValue(maybeParent));
11428
+ const isSelected = selectedKeys.has(key);
11403
11429
  if (isSelected || !maybeParent.children || maybeParent.children.length === 0) return isSelected;
11404
11430
  const areAllSelected = maybeParent.children.every(areAllChildrenSelected);
11405
- if (areAllSelected) {
11406
- selectedKeys.add(valueToKey(getOptionValue(maybeParent)));
11431
+ if (areAllSelected && !groupKeySet.has(key)) {
11432
+ selectedKeys.add(key);
11407
11433
  }
11408
11434
  return areAllSelected;
11409
11435
  }
@@ -11414,18 +11440,17 @@ function TreeSelectFieldBase(props) {
11414
11440
  return [maybeOption.option];
11415
11441
  });
11416
11442
  const selectedOptionsLabels = chipDisplay === "root" ? initialOptions.flatMap((o) => getTopLevelSelections(o, selectedKeys, getOptionValue)).map(getOptionLabel) : chipDisplay === "leaf" ? selectedOptions.filter((o) => !o.children || o.children.length === 0).map(getOptionLabel) : selectedOptions.map(getOptionLabel);
11417
- const filteredOptions = initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue));
11418
11443
  return {
11419
11444
  selectedKeys: [...selectedKeys],
11445
+ searchValue: void 0,
11420
11446
  inputValue: selectedOptions.length === 1 ? getOptionLabel([...selectedOptions][0]) : isReadOnly && selectedOptions.length > 0 ? selectedOptionsLabels.join(", ") : selectedOptions.length === 0 ? nothingSelectedText : "",
11421
- filteredOptions,
11422
11447
  selectedOptions,
11423
11448
  allOptions: initialOptions,
11424
11449
  selectedOptionsLabels,
11425
11450
  optionsLoading: false,
11426
11451
  allowCollapsing: true
11427
11452
  };
11428
- }, [initialOptions, values, chipDisplay, getOptionLabel, isReadOnly, nothingSelectedText, getOptionValue, collapsedKeys]);
11453
+ }, [initialOptions, values, chipDisplay, getOptionLabel, isReadOnly, nothingSelectedText, getOptionValue, groupKeySet]);
11429
11454
  const [fieldState, setFieldState] = (0, import_react44.useState)(() => initTreeFieldState());
11430
11455
  (0, import_react44.useEffect)(() => {
11431
11456
  if (Array.isArray(options)) {
@@ -11445,75 +11470,54 @@ function TreeSelectFieldBase(props) {
11445
11470
  setFieldState(initTreeFieldState());
11446
11471
  }
11447
11472
  }, [getOptionValue, initTreeFieldState, values]);
11448
- const reactToCollapse = (0, import_react44.useRef)(false);
11449
- (0, import_react44.useEffect)(
11450
- () => {
11451
- if (reactToCollapse.current) {
11452
- setFieldState(({
11453
- allOptions,
11454
- inputValue,
11455
- ...others
11456
- }) => ({
11457
- allOptions,
11458
- inputValue,
11459
- ...others,
11460
- filteredOptions: allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue)))
11461
- }));
11462
- }
11463
- reactToCollapse.current = true;
11464
- },
11465
- // Only react to collapseKey changes. Other deps should be stable (`contains`, `getOptionLabel`, `getOptionValue`).
11466
- // eslint-disable-next-line react-hooks/exhaustive-deps
11467
- [collapsedKeys]
11468
- );
11473
+ const filteredOptions = (0, import_react44.useMemo)(() => getFilteredOptions(fieldState.allOptions, fieldState.searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue), [fieldState.allOptions, fieldState.searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue]);
11469
11474
  const onInputChange = (0, import_react44.useCallback)((inputValue) => {
11470
11475
  setFieldState((prevState) => {
11471
11476
  return {
11472
11477
  ...prevState,
11473
11478
  inputValue,
11474
- allowCollapsing: inputValue.length === 0,
11475
- filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue)))
11479
+ searchValue: inputValue.length === 0 ? void 0 : inputValue,
11480
+ allowCollapsing: inputValue.length === 0
11476
11481
  };
11477
11482
  });
11478
- }, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
11479
- const maybeInitLoad = (0, import_react44.useCallback)(async (options2, fieldState2, setFieldState2) => {
11483
+ }, []);
11484
+ const maybeInitLoad = (0, import_react44.useCallback)(async (options2, setFieldState2) => {
11480
11485
  if (!Array.isArray(options2)) {
11481
11486
  setFieldState2((prevState) => ({
11482
11487
  ...prevState,
11483
11488
  optionsLoading: true
11484
11489
  }));
11485
11490
  const loadedOptions = (await options2.load()).options;
11486
- const filteredOptions = loadedOptions.flatMap((o) => levelOptions(o, 0, fieldState2.inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), fieldState2.inputValue)));
11487
11491
  setFieldState2((prevState) => ({
11488
11492
  ...prevState,
11489
- filteredOptions,
11490
11493
  allOptions: loadedOptions,
11491
11494
  optionsLoading: false
11492
11495
  }));
11493
11496
  }
11494
- }, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
11497
+ }, []);
11495
11498
  const firstOpen = (0, import_react44.useRef)(true);
11496
11499
  function onOpenChange(isOpen) {
11497
11500
  if (firstOpen.current && isOpen) {
11498
- maybeInitLoad(options, fieldState, setFieldState);
11501
+ maybeInitLoad(options, setFieldState);
11499
11502
  firstOpen.current = false;
11500
11503
  }
11501
11504
  if (isOpen) {
11502
11505
  setFieldState((prevState) => ({
11503
11506
  ...prevState,
11504
11507
  inputValue: "",
11505
- filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue))
11508
+ searchValue: void 0,
11509
+ allowCollapsing: true
11506
11510
  }));
11507
11511
  }
11508
11512
  }
11509
11513
  const comboBoxChildren = (0, import_react44.useCallback)(([item]) => /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_react_stately5.Item, { textValue: getOptionLabel(item), children: getOptionMenuLabel(item) }, valueToKey(getOptionValue(item))), [getOptionValue, getOptionLabel, getOptionMenuLabel]);
11510
11514
  const comboBoxProps = {
11511
11515
  ...otherProps,
11512
- disabledKeys: Object.keys(disabledOptionsWithReasons),
11516
+ disabledKeys,
11513
11517
  placeholder: !values || values.length === 0 ? placeholder : "",
11514
11518
  label: props.label,
11515
11519
  inputValue: fieldState.inputValue,
11516
- items: fieldState.filteredOptions,
11520
+ items: filteredOptions,
11517
11521
  isDisabled,
11518
11522
  isReadOnly,
11519
11523
  onInputChange,
@@ -11538,6 +11542,8 @@ function TreeSelectFieldBase(props) {
11538
11542
  setFieldState((prevState) => ({
11539
11543
  ...prevState,
11540
11544
  inputValue: nothingSelectedText,
11545
+ searchValue: void 0,
11546
+ allowCollapsing: true,
11541
11547
  selectedKeys: [],
11542
11548
  selectedOptions: []
11543
11549
  }));
@@ -11568,15 +11574,16 @@ function TreeSelectFieldBase(props) {
11568
11574
  const childrenKeys = option.children.flatMap(flattenOptions).map((o) => valueToKey(getOptionValue(o))).filter((childKey) => {
11569
11575
  return !state.disabledKeys.has(childKey);
11570
11576
  });
11571
- [key, ...childrenKeys].forEach(addedKeys.add, addedKeys);
11577
+ [...groupKeySet.has(key) ? [] : [key], ...childrenKeys].forEach(addedKeys.add, addedKeys);
11572
11578
  }
11573
- for (const parent of parents.reverse()) {
11574
- const allChecked = parent.children?.every((child) => {
11575
- const childKey = valueToKey(getOptionValue(child));
11576
- return addedKeys.has(childKey) || existingKeys.has(childKey) || state.disabledKeys.has(childKey);
11577
- });
11578
- if (allChecked) {
11579
- addedKeys.add(valueToKey(getOptionValue(parent)));
11579
+ const selectionKeys = /* @__PURE__ */ new Set([...existingKeys, ...addedKeys]);
11580
+ for (const parent of [...parents].reverse()) {
11581
+ const parentKey = valueToKey(getOptionValue(parent));
11582
+ if (isOptionFullySelected(parent, selectionKeys, state.disabledKeys, groupKeySet, getOptionValue)) {
11583
+ if (!groupKeySet.has(parentKey)) {
11584
+ addedKeys.add(parentKey);
11585
+ selectionKeys.add(parentKey);
11586
+ }
11580
11587
  }
11581
11588
  }
11582
11589
  }
@@ -11609,7 +11616,7 @@ function TreeSelectFieldBase(props) {
11609
11616
  ...prevState,
11610
11617
  // Since we reset the list of options upon selection changes, then set the `inputValue` to empty string to reflect that.
11611
11618
  inputValue: "",
11612
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
11619
+ searchValue: void 0,
11613
11620
  selectedKeys: [...selectedKeys],
11614
11621
  selectedOptions,
11615
11622
  selectedOptionsLabels: chipDisplay === "root" ? rootOptions.map(getOptionLabel) : chipDisplay === "leaf" ? leafOptions.map(getOptionLabel) : selectedOptions.map(getOptionLabel)
@@ -11640,7 +11647,7 @@ function TreeSelectFieldBase(props) {
11640
11647
  setFieldState((prevState) => ({
11641
11648
  ...prevState,
11642
11649
  inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : selectedOptions.length === 0 ? nothingSelectedText : "",
11643
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
11650
+ searchValue: void 0,
11644
11651
  allowCollapsing: true
11645
11652
  }));
11646
11653
  }
@@ -11729,6 +11736,18 @@ function getTopLevelSelections(o, selectedKeys, getOptionValue) {
11729
11736
  if (o.children) return [...o.children.flatMap((c) => getTopLevelSelections(c, selectedKeys, getOptionValue))];
11730
11737
  return [];
11731
11738
  }
11739
+ function getFilteredOptions(allOptions, searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue) {
11740
+ return allOptions.flatMap((option) => levelOptions(option, 0, !!searchValue, collapsedKeys, getOptionValue).filter(([nestedOption]) => searchValue ? contains(getOptionLabel(nestedOption), searchValue) : true));
11741
+ }
11742
+ function isOptionFullySelected(option, selectedKeys, disabledKeys, groupKeys, getOptionValue) {
11743
+ const key = valueToKey(getOptionValue(option));
11744
+ if (groupKeys.has(key)) {
11745
+ return option.children?.length ? option.children.every((child) => isOptionFullySelected(child, selectedKeys, disabledKeys, groupKeys, getOptionValue)) : false;
11746
+ }
11747
+ if (selectedKeys.has(key) || disabledKeys.has(key)) return true;
11748
+ if (!option.children || option.children.length === 0) return false;
11749
+ return option.children.every((child) => isOptionFullySelected(child, selectedKeys, disabledKeys, groupKeys, getOptionValue));
11750
+ }
11732
11751
 
11733
11752
  // src/inputs/internal/ComboBoxInput.tsx
11734
11753
  var import_jsx_runtime55 = require("react/jsx-runtime");