@homebound/beam 3.1.0-alpha.2 → 3.2.0-alpha.1

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
@@ -5965,6 +5965,7 @@ function dehydrateFilter(filterImpls, value) {
5965
5965
  const filter = filterImpls[key];
5966
5966
  return [
5967
5967
  key,
5968
+ // Let each filter own serialization so persisted state stays stable for non-plain JSON values like PlainDate.
5968
5969
  filter?.dehydrate ? filter.dehydrate(rawValue) : rawValue
5969
5970
  ];
5970
5971
  })
@@ -11121,7 +11122,8 @@ function TreeOption(props) {
11121
11122
  const {
11122
11123
  collapsedKeys,
11123
11124
  setCollapsedKeys,
11124
- getOptionValue
11125
+ getOptionValue,
11126
+ groupKeys
11125
11127
  } = useTreeSelectFieldProvider();
11126
11128
  const {
11127
11129
  optionProps,
@@ -11133,6 +11135,12 @@ function TreeOption(props) {
11133
11135
  shouldSelectOnPressUp: true,
11134
11136
  shouldFocusOnHover: false
11135
11137
  }, state, ref);
11138
+ const isGroup = groupKeys.includes(item.key);
11139
+ const canCollapse = allowCollapsing && !!option.children?.length;
11140
+ function toggleCollapsed() {
11141
+ if (!canCollapse) return;
11142
+ setCollapsedKeys((prevKeys) => collapsedKeys.includes(item.key) ? prevKeys.filter((k) => k !== item.key) : [...prevKeys, item.key]);
11143
+ }
11136
11144
  const isIndeterminate = !isSelected && option.children?.some((o) => hasSelectedChildren(o, state, getOptionValue));
11137
11145
  const listItemStyles = {
11138
11146
  item: {
@@ -11160,7 +11168,12 @@ function TreeOption(props) {
11160
11168
  }]
11161
11169
  }
11162
11170
  };
11163
- return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { ...hoverProps, ...(0, import_runtime36.trussProps)({
11171
+ return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("li", { ...hoverProps, onClick: (e) => {
11172
+ if (!isGroup) return;
11173
+ e.preventDefault();
11174
+ e.stopPropagation();
11175
+ toggleCollapsed();
11176
+ }, ...(0, import_runtime36.trussProps)({
11164
11177
  ...{
11165
11178
  display: "df",
11166
11179
  alignItems: "aic",
@@ -11177,18 +11190,18 @@ function TreeOption(props) {
11177
11190
  lineHeight: "lh_20px"
11178
11191
  },
11179
11192
  ...listItemStyles.item,
11180
- ...isHovered && !isDisabled ? listItemStyles.hover : {},
11181
- ...isFocused ? listItemStyles.focus : {},
11182
- ...isDisabled ? listItemStyles.disabled : {}
11193
+ ...isHovered && (!isDisabled || isGroup) ? listItemStyles.hover : {},
11194
+ ...isFocused && !isGroup ? listItemStyles.focus : {},
11195
+ ...isDisabled && !isGroup ? listItemStyles.disabled : {}
11183
11196
  }), children: [
11184
- 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) => {
11197
+ 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) => {
11185
11198
  e.preventDefault();
11186
11199
  e.stopPropagation();
11187
- setCollapsedKeys((prevKeys) => collapsedKeys.includes(item.key) ? prevKeys.filter((k) => k !== item.key) : [...prevKeys, item.key]);
11200
+ toggleCollapsed();
11188
11201
  return false;
11189
11202
  }, 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 }) }) }),
11190
11203
  /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("span", { className: "df aic gap1 h100 fg1 pt1 pb1 pr2", ref, ...optionProps, "data-label": item.textValue, children: [
11191
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(StyledCheckbox, { isDisabled, isSelected, isIndeterminate, ...tid[item.key.toString()] }),
11204
+ !isGroup && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(StyledCheckbox, { isDisabled, isSelected, isIndeterminate, ...tid[item.key.toString()] }),
11192
11205
  /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "pl1", children: item.rendered })
11193
11206
  ] })
11194
11207
  ] });
@@ -11271,9 +11284,14 @@ function VirtualizedOptions(props) {
11271
11284
  totalListHeightChanged: onListHeightChange,
11272
11285
  totalCount: items.length,
11273
11286
  ...process.env.NODE_ENV === "test" ? {
11274
- // We don't really need to set this, but it's handy for tests, which would
11275
- // otherwise render just 1 row. A better way to do this would be to jest.mock
11276
- // out Virtuoso with an impl that just rendered everything, but doing this for now.
11287
+ // In tests, we render all rows so assertions can see expands/async-loaded items. However,
11288
+ // the `initialItemCount` (next prop) is only applied on amount, so we set `key={items.length}`
11289
+ // to force a remount when our list changes -- and we only want/need this in tests, b/c otherwise
11290
+ // in production a Virtuoso remount causes visible flashing.
11291
+ key: items.length,
11292
+ // We don't really need to set this, but it's handy for tests, which would otherwise render
11293
+ // just 1 row. A better way to do this would be to jest.mock out Virtuoso with an impl that
11294
+ // just rendered everything, but doing this for now.
11277
11295
  initialItemCount: items.length
11278
11296
  } : {
11279
11297
  // Ensure the selected item is visible when the list renders
@@ -11313,8 +11331,7 @@ function VirtualizedOptions(props) {
11313
11331
  components: !loading ? {} : {
11314
11332
  Footer: typeof loading === "function" ? loading : () => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)(LoadingDots, { contrast })
11315
11333
  }
11316
- },
11317
- items.length
11334
+ }
11318
11335
  );
11319
11336
  }
11320
11337
 
@@ -11487,6 +11504,7 @@ function TreeSelectField(props) {
11487
11504
  ...otherProps
11488
11505
  } = props;
11489
11506
  const [collapsedKeys, setCollapsedKeys] = (0, import_react44.useState)([]);
11507
+ const groupKeys = (0, import_react44.useMemo)(() => props.groupOptions?.map((option) => valueToKey(option)) ?? [], [props.groupOptions]);
11490
11508
  (0, import_react44.useEffect)(() => {
11491
11509
  setCollapsedKeys(!Array.isArray(options) ? [] : defaultCollapsed ? options.map((o) => getOptionValue(o)) : options.flatMap(flattenOptions).filter((o) => o.defaultCollapsed).map((o) => getOptionValue(o)));
11492
11510
  }, [options, defaultCollapsed]);
@@ -11494,11 +11512,12 @@ function TreeSelectField(props) {
11494
11512
  () => ({
11495
11513
  collapsedKeys,
11496
11514
  setCollapsedKeys,
11497
- getOptionValue
11515
+ getOptionValue,
11516
+ groupKeys
11498
11517
  }),
11499
11518
  // 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
11500
11519
  // eslint-disable-next-line react-hooks/exhaustive-deps
11501
- [collapsedKeys, setCollapsedKeys]
11520
+ [collapsedKeys, setCollapsedKeys, groupKeys]
11502
11521
  );
11503
11522
  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: ({
11504
11523
  all,
@@ -11519,7 +11538,8 @@ var CollapsedContext = import_react44.default.createContext({
11519
11538
  collapsedKeys: [],
11520
11539
  setCollapsedKeys: () => {
11521
11540
  },
11522
- getOptionValue: () => ({})
11541
+ getOptionValue: () => ({}),
11542
+ groupKeys: []
11523
11543
  });
11524
11544
  function TreeSelectFieldBase(props) {
11525
11545
  const {
@@ -11538,13 +11558,15 @@ function TreeSelectFieldBase(props) {
11538
11558
  contrast = false,
11539
11559
  nothingSelectedText = "",
11540
11560
  onSelect,
11541
- defaultCollapsed = false,
11561
+ defaultCollapsed: _defaultCollapsed = false,
11542
11562
  placeholder,
11543
11563
  fullWidth = fieldProps?.fullWidth ?? false,
11544
11564
  chipDisplay = "root",
11545
11565
  disabledOptions,
11566
+ groupOptions: _groupOptions,
11546
11567
  ...otherProps
11547
11568
  } = props;
11569
+ void _defaultCollapsed;
11548
11570
  const isDisabled = !!disabled;
11549
11571
  const isReadOnly = !!readOnly;
11550
11572
  const initialOptions = Array.isArray(options) ? options : options.current;
@@ -11556,7 +11578,10 @@ function TreeSelectFieldBase(props) {
11556
11578
  const {
11557
11579
  collapsedKeys
11558
11580
  } = useTreeSelectFieldProvider();
11581
+ const groupKeys = (0, import_react44.useMemo)(() => _groupOptions?.map((option) => valueToKey(option)) ?? [], [_groupOptions]);
11582
+ const groupKeySet = (0, import_react44.useMemo)(() => new Set(groupKeys), [groupKeys]);
11559
11583
  const disabledOptionsWithReasons = Object.fromEntries(disabledOptions?.map(disabledOptionToKeyedTuple) ?? []);
11584
+ const disabledKeys = [.../* @__PURE__ */ new Set([...Object.keys(disabledOptionsWithReasons), ...groupKeys])];
11560
11585
  const initTreeFieldState = (0, import_react44.useCallback)(() => {
11561
11586
  const selectedKeys = new Set(values?.flatMap((v) => {
11562
11587
  const foundOptions = findOptions(initialOptions, valueToKey(v), getOptionValue);
@@ -11565,14 +11590,16 @@ function TreeSelectFieldBase(props) {
11565
11590
  }) => selectOptionAndAllChildren(option));
11566
11591
  }));
11567
11592
  function selectOptionAndAllChildren(maybeParent) {
11568
- return [valueToKey(getOptionValue(maybeParent)), ...maybeParent.children?.flatMap(selectOptionAndAllChildren) ?? []];
11593
+ const key = valueToKey(getOptionValue(maybeParent));
11594
+ return [...groupKeySet.has(key) ? [] : [key], ...maybeParent.children?.flatMap(selectOptionAndAllChildren) ?? []];
11569
11595
  }
11570
11596
  function areAllChildrenSelected(maybeParent) {
11571
- const isSelected = selectedKeys.has(valueToKey(getOptionValue(maybeParent)));
11597
+ const key = valueToKey(getOptionValue(maybeParent));
11598
+ const isSelected = selectedKeys.has(key);
11572
11599
  if (isSelected || !maybeParent.children || maybeParent.children.length === 0) return isSelected;
11573
11600
  const areAllSelected = maybeParent.children.every(areAllChildrenSelected);
11574
- if (areAllSelected) {
11575
- selectedKeys.add(valueToKey(getOptionValue(maybeParent)));
11601
+ if (areAllSelected && !groupKeySet.has(key)) {
11602
+ selectedKeys.add(key);
11576
11603
  }
11577
11604
  return areAllSelected;
11578
11605
  }
@@ -11583,18 +11610,17 @@ function TreeSelectFieldBase(props) {
11583
11610
  return [maybeOption.option];
11584
11611
  });
11585
11612
  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);
11586
- const filteredOptions = initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue));
11587
11613
  return {
11588
11614
  selectedKeys: [...selectedKeys],
11615
+ searchValue: void 0,
11589
11616
  inputValue: selectedOptions.length === 1 ? getOptionLabel([...selectedOptions][0]) : isReadOnly && selectedOptions.length > 0 ? selectedOptionsLabels.join(", ") : selectedOptions.length === 0 ? nothingSelectedText : "",
11590
- filteredOptions,
11591
11617
  selectedOptions,
11592
11618
  allOptions: initialOptions,
11593
11619
  selectedOptionsLabels,
11594
11620
  optionsLoading: false,
11595
11621
  allowCollapsing: true
11596
11622
  };
11597
- }, [initialOptions, values, chipDisplay, getOptionLabel, isReadOnly, nothingSelectedText, getOptionValue, collapsedKeys]);
11623
+ }, [initialOptions, values, chipDisplay, getOptionLabel, isReadOnly, nothingSelectedText, getOptionValue, groupKeySet]);
11598
11624
  const [fieldState, setFieldState] = (0, import_react44.useState)(() => initTreeFieldState());
11599
11625
  (0, import_react44.useEffect)(() => {
11600
11626
  if (Array.isArray(options)) {
@@ -11614,75 +11640,54 @@ function TreeSelectFieldBase(props) {
11614
11640
  setFieldState(initTreeFieldState());
11615
11641
  }
11616
11642
  }, [getOptionValue, initTreeFieldState, values]);
11617
- const reactToCollapse = (0, import_react44.useRef)(false);
11618
- (0, import_react44.useEffect)(
11619
- () => {
11620
- if (reactToCollapse.current) {
11621
- setFieldState(({
11622
- allOptions,
11623
- inputValue,
11624
- ...others
11625
- }) => ({
11626
- allOptions,
11627
- inputValue,
11628
- ...others,
11629
- filteredOptions: allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue)))
11630
- }));
11631
- }
11632
- reactToCollapse.current = true;
11633
- },
11634
- // Only react to collapseKey changes. Other deps should be stable (`contains`, `getOptionLabel`, `getOptionValue`).
11635
- // eslint-disable-next-line react-hooks/exhaustive-deps
11636
- [collapsedKeys]
11637
- );
11643
+ const filteredOptions = (0, import_react44.useMemo)(() => getFilteredOptions(fieldState.allOptions, fieldState.searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue), [fieldState.allOptions, fieldState.searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue]);
11638
11644
  const onInputChange = (0, import_react44.useCallback)((inputValue) => {
11639
11645
  setFieldState((prevState) => {
11640
11646
  return {
11641
11647
  ...prevState,
11642
11648
  inputValue,
11643
- allowCollapsing: inputValue.length === 0,
11644
- filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), inputValue)))
11649
+ searchValue: inputValue.length === 0 ? void 0 : inputValue,
11650
+ allowCollapsing: inputValue.length === 0
11645
11651
  };
11646
11652
  });
11647
- }, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
11648
- const maybeInitLoad = (0, import_react44.useCallback)(async (options2, fieldState2, setFieldState2) => {
11653
+ }, []);
11654
+ const maybeInitLoad = (0, import_react44.useCallback)(async (options2, setFieldState2) => {
11649
11655
  if (!Array.isArray(options2)) {
11650
11656
  setFieldState2((prevState) => ({
11651
11657
  ...prevState,
11652
11658
  optionsLoading: true
11653
11659
  }));
11654
11660
  const loadedOptions = (await options2.load()).options;
11655
- const filteredOptions = loadedOptions.flatMap((o) => levelOptions(o, 0, fieldState2.inputValue.length > 0, collapsedKeys, getOptionValue).filter(([option]) => contains(getOptionLabel(option), fieldState2.inputValue)));
11656
11661
  setFieldState2((prevState) => ({
11657
11662
  ...prevState,
11658
- filteredOptions,
11659
11663
  allOptions: loadedOptions,
11660
11664
  optionsLoading: false
11661
11665
  }));
11662
11666
  }
11663
- }, [collapsedKeys, contains, getOptionLabel, getOptionValue]);
11667
+ }, []);
11664
11668
  const firstOpen = (0, import_react44.useRef)(true);
11665
11669
  function onOpenChange(isOpen) {
11666
11670
  if (firstOpen.current && isOpen) {
11667
- maybeInitLoad(options, fieldState, setFieldState);
11671
+ maybeInitLoad(options, setFieldState);
11668
11672
  firstOpen.current = false;
11669
11673
  }
11670
11674
  if (isOpen) {
11671
11675
  setFieldState((prevState) => ({
11672
11676
  ...prevState,
11673
11677
  inputValue: "",
11674
- filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue))
11678
+ searchValue: void 0,
11679
+ allowCollapsing: true
11675
11680
  }));
11676
11681
  }
11677
11682
  }
11678
11683
  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]);
11679
11684
  const comboBoxProps = {
11680
11685
  ...otherProps,
11681
- disabledKeys: Object.keys(disabledOptionsWithReasons),
11686
+ disabledKeys,
11682
11687
  placeholder: !values || values.length === 0 ? placeholder : "",
11683
11688
  label: props.label,
11684
11689
  inputValue: fieldState.inputValue,
11685
- items: fieldState.filteredOptions,
11690
+ items: filteredOptions,
11686
11691
  isDisabled,
11687
11692
  isReadOnly,
11688
11693
  onInputChange,
@@ -11707,6 +11712,8 @@ function TreeSelectFieldBase(props) {
11707
11712
  setFieldState((prevState) => ({
11708
11713
  ...prevState,
11709
11714
  inputValue: nothingSelectedText,
11715
+ searchValue: void 0,
11716
+ allowCollapsing: true,
11710
11717
  selectedKeys: [],
11711
11718
  selectedOptions: []
11712
11719
  }));
@@ -11737,15 +11744,16 @@ function TreeSelectFieldBase(props) {
11737
11744
  const childrenKeys = option.children.flatMap(flattenOptions).map((o) => valueToKey(getOptionValue(o))).filter((childKey) => {
11738
11745
  return !state.disabledKeys.has(childKey);
11739
11746
  });
11740
- [key, ...childrenKeys].forEach(addedKeys.add, addedKeys);
11747
+ [...groupKeySet.has(key) ? [] : [key], ...childrenKeys].forEach(addedKeys.add, addedKeys);
11741
11748
  }
11742
- for (const parent of parents.reverse()) {
11743
- const allChecked = parent.children?.every((child) => {
11744
- const childKey = valueToKey(getOptionValue(child));
11745
- return addedKeys.has(childKey) || existingKeys.has(childKey) || state.disabledKeys.has(childKey);
11746
- });
11747
- if (allChecked) {
11748
- addedKeys.add(valueToKey(getOptionValue(parent)));
11749
+ const selectionKeys = /* @__PURE__ */ new Set([...existingKeys, ...addedKeys]);
11750
+ for (const parent of [...parents].reverse()) {
11751
+ const parentKey = valueToKey(getOptionValue(parent));
11752
+ if (isOptionFullySelected(parent, selectionKeys, state.disabledKeys, groupKeySet, getOptionValue)) {
11753
+ if (!groupKeySet.has(parentKey)) {
11754
+ addedKeys.add(parentKey);
11755
+ selectionKeys.add(parentKey);
11756
+ }
11749
11757
  }
11750
11758
  }
11751
11759
  }
@@ -11778,7 +11786,7 @@ function TreeSelectFieldBase(props) {
11778
11786
  ...prevState,
11779
11787
  // Since we reset the list of options upon selection changes, then set the `inputValue` to empty string to reflect that.
11780
11788
  inputValue: "",
11781
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
11789
+ searchValue: void 0,
11782
11790
  selectedKeys: [...selectedKeys],
11783
11791
  selectedOptions,
11784
11792
  selectedOptionsLabels: chipDisplay === "root" ? rootOptions.map(getOptionLabel) : chipDisplay === "leaf" ? leafOptions.map(getOptionLabel) : selectedOptions.map(getOptionLabel)
@@ -11809,7 +11817,7 @@ function TreeSelectFieldBase(props) {
11809
11817
  setFieldState((prevState) => ({
11810
11818
  ...prevState,
11811
11819
  inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : selectedOptions.length === 0 ? nothingSelectedText : "",
11812
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
11820
+ searchValue: void 0,
11813
11821
  allowCollapsing: true
11814
11822
  }));
11815
11823
  }
@@ -11898,6 +11906,18 @@ function getTopLevelSelections(o, selectedKeys, getOptionValue) {
11898
11906
  if (o.children) return [...o.children.flatMap((c) => getTopLevelSelections(c, selectedKeys, getOptionValue))];
11899
11907
  return [];
11900
11908
  }
11909
+ function getFilteredOptions(allOptions, searchValue, collapsedKeys, contains, getOptionLabel, getOptionValue) {
11910
+ return allOptions.flatMap((option) => levelOptions(option, 0, !!searchValue, collapsedKeys, getOptionValue).filter(([nestedOption]) => searchValue ? contains(getOptionLabel(nestedOption), searchValue) : true));
11911
+ }
11912
+ function isOptionFullySelected(option, selectedKeys, disabledKeys, groupKeys, getOptionValue) {
11913
+ const key = valueToKey(getOptionValue(option));
11914
+ if (groupKeys.has(key)) {
11915
+ return option.children?.length ? option.children.every((child) => isOptionFullySelected(child, selectedKeys, disabledKeys, groupKeys, getOptionValue)) : false;
11916
+ }
11917
+ if (selectedKeys.has(key) || disabledKeys.has(key)) return true;
11918
+ if (!option.children || option.children.length === 0) return false;
11919
+ return option.children.every((child) => isOptionFullySelected(child, selectedKeys, disabledKeys, groupKeys, getOptionValue));
11920
+ }
11901
11921
 
11902
11922
  // src/inputs/internal/ComboBoxInput.tsx
11903
11923
  var import_jsx_runtime55 = require("react/jsx-runtime");