@homebound/beam 2.263.0 → 2.265.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.
@@ -77,6 +77,10 @@ export declare const Icons: {
77
77
  arrowUp: import("@emotion/react/jsx-runtime").JSX.Element;
78
78
  arrowDown: import("@emotion/react/jsx-runtime").JSX.Element;
79
79
  arrowRight: import("@emotion/react/jsx-runtime").JSX.Element;
80
+ triangleLeft: import("@emotion/react/jsx-runtime").JSX.Element;
81
+ triangleRight: import("@emotion/react/jsx-runtime").JSX.Element;
82
+ triangleUp: import("@emotion/react/jsx-runtime").JSX.Element;
83
+ triangleDown: import("@emotion/react/jsx-runtime").JSX.Element;
80
84
  menuClose: import("@emotion/react/jsx-runtime").JSX.Element;
81
85
  menuOpen: import("@emotion/react/jsx-runtime").JSX.Element;
82
86
  arrowFromLeft: import("@emotion/react/jsx-runtime").JSX.Element;
@@ -84,6 +84,10 @@ exports.Icons = {
84
84
  arrowUp: ((0, jsx_runtime_1.jsx)("path", { d: "M12.793 21.207L12.793 6.62097L18.086 11.914L19.5 10.5L11.793 2.79297L4.086 10.5L5.5 11.914L10.793 6.62097L10.793 21.207L12.793 21.207Z" })),
85
85
  arrowDown: ((0, jsx_runtime_1.jsx)("path", { d: "M10.7929 2.79303L10.7929 17.379L5.49994 12.086L4.08594 13.5L11.7929 21.207L19.4999 13.5L18.0859 12.086L12.7929 17.379L12.7929 2.79303L10.7929 2.79303Z" })),
86
86
  arrowRight: ((0, jsx_runtime_1.jsx)("path", { d: "M2.586 13L17.172 13L11.879 18.293L13.293 19.707L21 12L13.293 4.29303L11.879 5.70703L17.172 11L2.586 11L2.586 13Z" })),
87
+ triangleLeft: (0, jsx_runtime_1.jsx)("path", { d: "M15 18L9 12L15 6L15 18Z" }),
88
+ triangleRight: (0, jsx_runtime_1.jsx)("path", { d: "M9 6L15 12L9 18L9 6Z" }),
89
+ triangleUp: (0, jsx_runtime_1.jsx)("path", { d: "M6 15L12 9L18 15L6 15Z" }),
90
+ triangleDown: (0, jsx_runtime_1.jsx)("path", { d: "M18 9L12 15L6 9H18Z" }),
87
91
  menuClose: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("path", { d: "M8 6H24V8H8V6ZM8 11H24V13H8V11ZM8 16H24V18H8V16Z" }), (0, jsx_runtime_1.jsx)("path", { d: "M5 8.94L1.94667 12L5 15.06L4.06 16L0.0599999 12L4.06 8L5 8.94Z" })] })),
88
92
  menuOpen: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("path", { d: "M8 6H24V8H8V6ZM8 11H24V13H8V11ZM8 16H24V18H8V16Z" }), (0, jsx_runtime_1.jsx)("path", { d: "M0.0600583 15.06L3.11339 12L0.0600586 8.94L1.00006 8L5.00006 12L1.00006 16L0.0600583 15.06Z" })] })),
89
93
  arrowFromLeft: ((0, jsx_runtime_1.jsx)("path", { d: "M4 6H6V18H4V6ZM8 13H16.586L12.293 17.293L13.707 18.707L20.414 12L13.707 5.293L12.293 6.707L16.586 11H8V13Z" })),
@@ -21,4 +21,11 @@ export interface CheckboxBaseProps extends BeamFocusableProps {
21
21
  helperText?: string | ReactNode;
22
22
  }
23
23
  export declare function CheckboxBase(props: CheckboxBaseProps): import("@emotion/react/jsx-runtime").JSX.Element;
24
+ interface StyledCheckboxProps {
25
+ isDisabled?: boolean;
26
+ isIndeterminate?: boolean;
27
+ isSelected?: boolean;
28
+ isFocusVisible?: boolean;
29
+ }
30
+ export declare function StyledCheckbox(props: StyledCheckboxProps): import("@emotion/react/jsx-runtime").JSX.Element;
24
31
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CheckboxBase = void 0;
3
+ exports.StyledCheckbox = exports.CheckboxBase = void 0;
4
4
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const react_aria_1 = require("react-aria");
@@ -15,7 +15,6 @@ function CheckboxBase(props) {
15
15
  const { isFocusVisible, focusProps } = (0, react_aria_1.useFocusRing)(ariaProps);
16
16
  const { hoverProps, isHovered } = (0, react_aria_1.useHover)({ isDisabled });
17
17
  const tid = (0, utils_1.useTestIds)(props, (0, defaultTestId_1.defaultTestId)(label));
18
- const markIcon = isIndeterminate ? dashSmall : isSelected ? checkmarkSmall : "";
19
18
  return ((0, jsx_runtime_1.jsxs)("label", { css: Css_1.Css.df.cursorPointer.relative
20
19
  // Prevents accidental checkbox clicks due to label width being longer
21
20
  // than the content.
@@ -23,16 +22,7 @@ function CheckboxBase(props) {
23
22
  .maxw((0, Css_1.px)(320))
24
23
  .if(description !== undefined)
25
24
  .maxw((0, Css_1.px)(344))
26
- .if(isDisabled).cursorNotAllowed.$, "aria-label": label, children: [(0, jsx_runtime_1.jsx)(react_aria_1.VisuallyHidden, { children: (0, jsx_runtime_1.jsx)("input", { ref: ref, ...(0, react_aria_1.mergeProps)(inputProps, focusProps), ...tid, "data-indeterminate": isIndeterminate }) }), (0, jsx_runtime_1.jsx)("span", { ...hoverProps, css: {
27
- ...baseStyles,
28
- ...(((isSelected && !isDisabled) || isIndeterminate) && filledBoxStyles),
29
- ...(((isSelected && !isDisabled) || isIndeterminate) && isHovered && filledBoxHoverStyles),
30
- ...(isDisabled && disabledBoxStyles),
31
- ...(isDisabled && isSelected && disabledSelectedBoxStyles),
32
- ...(isFocusVisible && focusRingStyles),
33
- ...(isHovered && hoverBorderStyles),
34
- ...markStyles,
35
- }, "aria-hidden": "true", children: markIcon }), !checkboxOnly && (
25
+ .if(isDisabled).cursorNotAllowed.$, "aria-label": label, children: [(0, jsx_runtime_1.jsx)(react_aria_1.VisuallyHidden, { children: (0, jsx_runtime_1.jsx)("input", { ref: ref, ...(0, react_aria_1.mergeProps)(inputProps, focusProps), ...tid, "data-indeterminate": isIndeterminate }) }), (0, jsx_runtime_1.jsx)(StyledCheckbox, { ...props, isFocusVisible: isFocusVisible }), !checkboxOnly && (
36
26
  // Use a mtPx(-2) to better align the label with the checkbox.
37
27
  // Not using align-items: center as the checkbox would align with all content below, where we really want it to stay only aligned with the label
38
28
  (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.ml1.mtPx(-2).$, children: [label && (0, jsx_runtime_1.jsx)("div", { css: { ...labelStyles, ...(isDisabled && disabledColor) }, children: label }), description && (0, jsx_runtime_1.jsx)("div", { css: { ...descStyles, ...(isDisabled && disabledColor) }, children: description }), errorMsg && (0, jsx_runtime_1.jsx)(ErrorMessage_1.ErrorMessage, { errorMsg: errorMsg, ...tid.errorMsg }), helperText && (0, jsx_runtime_1.jsx)(HelperText_1.HelperText, { helperText: helperText, ...tid.helperText })] }))] }));
@@ -49,5 +39,22 @@ const hoverBorderStyles = Css_1.Css.bLightBlue900.$;
49
39
  const markStyles = { svg: Css_1.Css.absolute.topPx(-1).leftPx(-1).$ };
50
40
  const labelStyles = Css_1.Css.smMd.$;
51
41
  const descStyles = Css_1.Css.sm.gray700.$;
42
+ function StyledCheckbox(props) {
43
+ const { isDisabled = false, isIndeterminate = false, isSelected, isFocusVisible } = props;
44
+ const { hoverProps, isHovered } = (0, react_aria_1.useHover)({ isDisabled });
45
+ const markIcon = isIndeterminate ? dashSmall : isSelected ? checkmarkSmall : "";
46
+ const tid = (0, utils_1.useTestIds)(props);
47
+ return ((0, jsx_runtime_1.jsx)("span", { ...hoverProps, css: {
48
+ ...baseStyles,
49
+ ...(((isSelected && !isDisabled) || isIndeterminate) && filledBoxStyles),
50
+ ...(((isSelected && !isDisabled) || isIndeterminate) && isHovered && filledBoxHoverStyles),
51
+ ...(isDisabled && disabledBoxStyles),
52
+ ...(isDisabled && isSelected && disabledSelectedBoxStyles),
53
+ ...(isFocusVisible && focusRingStyles),
54
+ ...(isHovered && hoverBorderStyles),
55
+ ...markStyles,
56
+ }, "aria-hidden": "true", "data-checked": isSelected ? true : isIndeterminate ? "mixed" : false, ...tid.checkbox, children: markIcon }));
57
+ }
58
+ exports.StyledCheckbox = StyledCheckbox;
52
59
  const checkmarkSmall = ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", children: (0, jsx_runtime_1.jsx)("path", { d: "M6.66669 10.3907L4.47135 8.19533L3.52869 9.138L6.66669 12.276L13.138 5.80467L12.1954 4.862L6.66669 10.3907Z", fill: Css_1.Palette.White }) }));
53
60
  const dashSmall = ((0, jsx_runtime_1.jsx)("svg", { width: "16", height: "16", children: (0, jsx_runtime_1.jsx)("rect", { x: "4", y: "7.5", width: "8", height: "1.35", fill: Css_1.Palette.White }) }));
@@ -36,7 +36,9 @@ function SelectField(props) {
36
36
  },
37
37
  // Read Only does not apply to `select` fields, instead we'll add in disabled for tests to verify.
38
38
  disabled: !!(disabled || readOnly), "data-error": !!errorMsg, "data-errormsg": errorMsg, "data-readonly": readOnly, children: [(0, jsx_runtime_1.jsx)("option", { disabled: true, value: "" }), options.map((option, i) => {
39
- return ((0, jsx_runtime_1.jsx)("option", { value: `${getOptionValue(option)}`, disabled: disabledOptions.includes(getOptionValue(option)), children: getOptionLabel(option) }, i));
39
+ return ((0, jsx_runtime_1.jsx)("option", { value: `${getOptionValue(option)}`, disabled: disabledOptions.some((dOption) => typeof dOption === "object"
40
+ ? dOption.value === getOptionValue(option)
41
+ : dOption === getOptionValue(option)), children: getOptionLabel(option) }, i));
40
42
  })] }), helperText && (0, jsx_runtime_1.jsx)("div", { ...tid.helperText, children: helperText })] }));
41
43
  }
42
44
  exports.SelectField = SelectField;
@@ -0,0 +1,12 @@
1
+ import { Node } from "@react-types/shared";
2
+ import { ListState } from "react-stately";
3
+ import { LeveledOption } from "./utils";
4
+ interface TreeOptionProps<O> {
5
+ item: Node<LeveledOption<O>>;
6
+ state: ListState<O>;
7
+ contrast?: boolean;
8
+ allowCollapsing?: boolean;
9
+ }
10
+ /** Represents a single option within a ListBox - used by SelectField, MultiSelectField, and TreeSelectField */
11
+ export declare function TreeOption<O>(props: TreeOptionProps<O>): import("@emotion/react/jsx-runtime").JSX.Element | null;
12
+ export {};
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TreeOption = void 0;
4
+ const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_aria_1 = require("react-aria");
7
+ const components_1 = require("../../components");
8
+ const Css_1 = require("../../Css");
9
+ const CheckboxBase_1 = require("../CheckboxBase");
10
+ const TreeSelectField_1 = require("./TreeSelectField");
11
+ const Value_1 = require("../Value");
12
+ const utils_1 = require("../../utils");
13
+ /** Represents a single option within a ListBox - used by SelectField, MultiSelectField, and TreeSelectField */
14
+ function TreeOption(props) {
15
+ var _a, _b;
16
+ const { item, state, contrast = false, allowCollapsing = true } = props;
17
+ const leveledOption = item.value;
18
+ if (!leveledOption)
19
+ return null;
20
+ const [option, level] = leveledOption;
21
+ const ref = (0, react_1.useRef)(null);
22
+ const { hoverProps, isHovered } = (0, react_aria_1.useHover)({});
23
+ const tid = (0, utils_1.useTestIds)(props, "treeOption");
24
+ const { collapsedKeys, setCollapsedKeys, getOptionValue } = (0, TreeSelectField_1.useTreeSelectFieldProvider)();
25
+ const { optionProps, isDisabled, isFocused, isSelected } = (0, react_aria_1.useOption)({ key: item.key, shouldSelectOnPressUp: true, shouldFocusOnHover: false }, state, ref);
26
+ // If this item is not selected, then determine if some of its children are selected to show the indeterminate state.
27
+ // Note: If `isSelected` will be true if all of the children were selected. That auto-parent-selection happens in the `onSelect` callback in TreeSelectField.
28
+ const isIndeterminate = !isSelected && ((_a = option.children) === null || _a === void 0 ? void 0 : _a.some((o) => hasSelectedChildren(o, state, getOptionValue)));
29
+ const listItemStyles = {
30
+ item: Css_1.Css.gray900.if(contrast).white.$,
31
+ hover: Css_1.Css.bgGray100.if(contrast).bgGray600.$,
32
+ disabled: Css_1.Css.cursorNotAllowed.gray400.if(contrast).gray500.$,
33
+ focus: Css_1.Css.add("boxShadow", `inset 0 0 0 1px ${!contrast ? Css_1.Palette.LightBlue700 : Css_1.Palette.LightBlue500}`).$,
34
+ };
35
+ return ((0, jsx_runtime_1.jsxs)("li", { ...hoverProps, css: {
36
+ ...Css_1.Css.df.aic.jcsb.gap1.pl2.mh("42px").outline0.cursorPointer.sm.plPx(16 + level * 8).$,
37
+ ...listItemStyles.item,
38
+ ...(isHovered && !isDisabled ? listItemStyles.hover : {}),
39
+ ...(isFocused ? listItemStyles.focus : {}),
40
+ ...(isDisabled ? listItemStyles.disabled : {}),
41
+ }, children: [allowCollapsing && ((0, jsx_runtime_1.jsx)("span", { css: Css_1.Css.wPx(18).fs0.df.aic.$, children: option.children && ((_b = option.children) === null || _b === void 0 ? void 0 : _b.length) > 0 && ((0, jsx_runtime_1.jsx)("button", { onClick: (e) => {
42
+ e.preventDefault();
43
+ e.stopPropagation();
44
+ setCollapsedKeys((prevKeys) => collapsedKeys.includes(item.key) ? prevKeys.filter((k) => k !== item.key) : [...prevKeys, item.key]);
45
+ return false;
46
+ }, css: Css_1.Css.br4.hPx(16).wPx(16).bgTransparent.onHover.bgGray300.$, ...tid[`collapseToggle_${item.key}`], children: (0, jsx_runtime_1.jsx)(components_1.Icon, { icon: collapsedKeys.includes(item.key) ? "triangleRight" : "triangleDown", inc: 2 }) })) })), (0, jsx_runtime_1.jsxs)("span", { css: Css_1.Css.df.aic.gap1.h100.fg1.py1.pr2.$, ref: ref, ...optionProps, children: [(0, jsx_runtime_1.jsx)(CheckboxBase_1.StyledCheckbox, { isDisabled: isDisabled, isSelected: isSelected, isIndeterminate: isIndeterminate, ...tid[item.key.toString()] }), (0, jsx_runtime_1.jsx)("div", { css: Css_1.Css.pl1.$, children: item.rendered })] })] }));
47
+ }
48
+ exports.TreeOption = TreeOption;
49
+ function hasSelectedChildren(childOption, state, getOptionValue) {
50
+ if (childOption.children && childOption.children.length > 0) {
51
+ return childOption.children.some((child) => hasSelectedChildren(child, state, getOptionValue));
52
+ }
53
+ return state.selectionManager.isSelected((0, Value_1.valueToKey)(getOptionValue(childOption)));
54
+ }
@@ -0,0 +1,49 @@
1
+ import React, { Dispatch, Key, ReactNode, SetStateAction } from "react";
2
+ import { PresentationFieldProps } from "../../components/PresentationContext";
3
+ import { Value } from "../index";
4
+ import { NestedOptionsOrLoad, TreeSelectResponse } from "./utils";
5
+ import { BeamFocusableProps } from "../../interfaces";
6
+ import { HasIdAndName, Optional } from "../../types";
7
+ export interface TreeSelectFieldProps<O, V extends Value> extends BeamFocusableProps, PresentationFieldProps {
8
+ /** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. `isUnsetOpt` is only defined for single SelectField */
9
+ getOptionMenuLabel?: (opt: O, isUnsetOpt?: boolean) => string | ReactNode;
10
+ getOptionValue: (opt: O) => V;
11
+ getOptionLabel: (opt: O) => string;
12
+ /** The current value; it can be `undefined`, even if `V` cannot be. */
13
+ values: V[] | undefined;
14
+ onSelect: (options: TreeSelectResponse<O, V>) => void;
15
+ options: NestedOptionsOrLoad<O>;
16
+ /** Whether the field is disabled. If a ReactNode, it's treated as a "disabled reason" that's shown in a tooltip. */
17
+ disabled?: boolean | ReactNode;
18
+ required?: boolean;
19
+ errorMsg?: string;
20
+ helperText?: string | ReactNode;
21
+ /** Allow placing an icon/decoration within the input field. */
22
+ fieldDecoration?: (opt: O) => ReactNode;
23
+ /** Sets the form field label. */
24
+ label: string;
25
+ readOnly?: boolean | ReactNode;
26
+ onBlur?: () => void;
27
+ onFocus?: () => void;
28
+ sizeToContent?: boolean;
29
+ /** The text to show when nothing is selected, i.e. could be "All" for filters. */
30
+ nothingSelectedText?: string;
31
+ /** When set the SelectField is expected to be put on a darker background */
32
+ contrast?: boolean;
33
+ /** Placeholder content */
34
+ placeholder?: string;
35
+ hideErrorMessage?: boolean;
36
+ /** Whether to have all groups collapsed on initial load. Can also be configured individually, which overrides this behavior.
37
+ * @default false */
38
+ defaultCollapsed?: boolean;
39
+ }
40
+ export declare function TreeSelectField<O, V extends Value>(props: TreeSelectFieldProps<O, V>): JSX.Element;
41
+ export declare function TreeSelectField<O extends HasIdAndName<V>, V extends Value>(props: Optional<TreeSelectFieldProps<O, V>, "getOptionValue" | "getOptionLabel">): JSX.Element;
42
+ export declare function useTreeSelectFieldProvider<O, V extends Value>(): CollapsedChildrenState<any, any>;
43
+ interface CollapsedChildrenState<O, V extends Value> {
44
+ collapsedKeys: Key[];
45
+ setCollapsedKeys: Dispatch<SetStateAction<Key[]>>;
46
+ getOptionValue: (opt: O) => V;
47
+ }
48
+ export declare const CollapsedContext: React.Context<CollapsedChildrenState<any, any>>;
49
+ export {};
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.CollapsedContext = exports.useTreeSelectFieldProvider = exports.TreeSelectField = void 0;
27
+ const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
28
+ const react_1 = __importStar(require("react"));
29
+ const react_aria_1 = require("react-aria");
30
+ const react_stately_1 = require("react-stately");
31
+ const components_1 = require("../../components");
32
+ const internal_1 = require("../../components/internal");
33
+ const Css_1 = require("../../Css");
34
+ const ComboBoxInput_1 = require("../internal/ComboBoxInput");
35
+ const ListBox_1 = require("../internal/ListBox");
36
+ const utils_1 = require("./utils");
37
+ const Value_1 = require("../Value");
38
+ function TreeSelectField(props) {
39
+ const { getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
40
+ getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
41
+ options, onSelect, values, defaultCollapsed = false, ...otherProps } = props;
42
+ const [collapsedKeys, setCollapsedKeys] = (0, react_1.useState)(Array.isArray(options) && defaultCollapsed ? options.map((o) => getOptionValue(o)) : []);
43
+ const contextValue = (0, react_1.useMemo)(() => ({ collapsedKeys, setCollapsedKeys, getOptionValue }), [collapsedKeys, setCollapsedKeys]);
44
+ return ((0, jsx_runtime_1.jsx)(exports.CollapsedContext.Provider, { value: contextValue, children: (0, jsx_runtime_1.jsx)(TreeSelectFieldBase, { ...otherProps, options: options, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, values: values, onSelect: ({ all, leaf, root }) => {
45
+ onSelect({ all, leaf, root });
46
+ } }) }));
47
+ }
48
+ exports.TreeSelectField = TreeSelectField;
49
+ function useTreeSelectFieldProvider() {
50
+ return (0, react_1.useContext)(exports.CollapsedContext);
51
+ }
52
+ exports.useTreeSelectFieldProvider = useTreeSelectFieldProvider;
53
+ // create the context to hold the collapsed state, default should be false
54
+ exports.CollapsedContext = react_1.default.createContext({
55
+ collapsedKeys: [],
56
+ setCollapsedKeys: () => { },
57
+ getOptionValue: () => ({}),
58
+ });
59
+ function TreeSelectFieldBase(props) {
60
+ var _a;
61
+ const { values, options, getOptionValue, getOptionLabel, getOptionMenuLabel = getOptionLabel, disabled, readOnly, labelStyle, borderless, contrast = false, nothingSelectedText = "", onSelect, defaultCollapsed = false, placeholder, ...otherProps } = props;
62
+ const isDisabled = !!disabled;
63
+ const isReadOnly = !!readOnly;
64
+ const initialOptions = Array.isArray(options) ? options : options.initial;
65
+ const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
66
+ const { collapsedKeys } = useTreeSelectFieldProvider();
67
+ function levelOptions(o, level, filtering) {
68
+ var _a;
69
+ // If a user is filtering, then do not provide level to the options as the various paddings may look quite odd.
70
+ const actualLevel = filtering ? 0 : level;
71
+ return [
72
+ [o, actualLevel],
73
+ ...(((_a = o.children) === null || _a === void 0 ? void 0 : _a.length) && !collapsedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o)))
74
+ ? o.children.flatMap((oc) => levelOptions(oc, actualLevel + 1, filtering))
75
+ : []),
76
+ ];
77
+ }
78
+ // Initialize the TreeFieldState
79
+ const [fieldState, setFieldState] = (0, react_1.useState)(() => {
80
+ var _a;
81
+ const filteredOptions = initialOptions.flatMap((o) => levelOptions(o, 0));
82
+ const selectedOptions = (_a = values === null || values === void 0 ? void 0 : values.flatMap((v) => {
83
+ var _a, _b;
84
+ const maybeOption = (0, utils_1.findOption)(initialOptions, (0, Value_1.valueToKey)(v), getOptionValue);
85
+ if (!maybeOption)
86
+ return [];
87
+ const { option } = maybeOption;
88
+ // If the selected key has children then all children should also be considered selected.
89
+ return [option, ...((_b = (_a = option.children) === null || _a === void 0 ? void 0 : _a.flatMap(utils_1.flattenOptions)) !== null && _b !== void 0 ? _b : [])];
90
+ })) !== null && _a !== void 0 ? _a : [];
91
+ const selectedKeys = selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
92
+ // It is possible that all the children of a parent were considered selected `values`, but the parent wasn't.
93
+ // In this case, the parent also should be considered a selected option.
94
+ function areAllChildrenSelected(maybeParent) {
95
+ const isSelected = selectedKeys.includes((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
96
+ // if this key is already selected, then return true
97
+ if (isSelected)
98
+ return true;
99
+ // If we do not have children, then return the state of this leaf node.
100
+ if (!maybeParent.children)
101
+ return isSelected;
102
+ // If we do have children, then check if all children are selected.
103
+ // if all are selected, then push this parent to the selectedKeys and selectedOptions
104
+ const areAllSelected = maybeParent.children.every(areAllChildrenSelected);
105
+ if (areAllSelected) {
106
+ selectedKeys.push((0, Value_1.valueToKey)(getOptionValue(maybeParent)));
107
+ selectedOptions.push(maybeParent);
108
+ }
109
+ return areAllSelected;
110
+ }
111
+ initialOptions.forEach(areAllChildrenSelected);
112
+ return {
113
+ selectedKeys,
114
+ inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : "",
115
+ filteredOptions,
116
+ selectedOptions,
117
+ allOptions: initialOptions,
118
+ optionsLoading: false,
119
+ allowCollapsing: true,
120
+ };
121
+ });
122
+ // React to collapsed keys and update the filtered options
123
+ const reactToCollapse = (0, react_1.useRef)(false);
124
+ (0, react_1.useEffect)(() => {
125
+ // Do not run this effect on first render. Otherwise we'd be triggering a re-render on first render.
126
+ if (reactToCollapse.current) {
127
+ setFieldState(({ allOptions, inputValue, ...others }) => ({
128
+ allOptions,
129
+ inputValue,
130
+ ...others,
131
+ filteredOptions: allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), inputValue))),
132
+ }));
133
+ }
134
+ reactToCollapse.current = true;
135
+ }, [collapsedKeys]);
136
+ // Update the filtered options when the input value changes
137
+ const onInputChange = (0, react_1.useCallback)((inputValue) => {
138
+ setFieldState((prevState) => {
139
+ return {
140
+ ...prevState,
141
+ inputValue,
142
+ allowCollapsing: inputValue.length === 0,
143
+ filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), inputValue))),
144
+ };
145
+ });
146
+ }, [setFieldState]);
147
+ // Handle loading of the options in the case that they are loaded via a Promise.
148
+ const maybeInitLoad = (0, react_1.useCallback)(async (options, fieldState, setFieldState) => {
149
+ if (!Array.isArray(options)) {
150
+ setFieldState((prevState) => ({ ...prevState, optionsLoading: true }));
151
+ const loadedOptions = (await options.load()).options;
152
+ const filteredOptions = loadedOptions.flatMap((o) => levelOptions(o, 0, fieldState.inputValue.length > 0).filter(([option]) => contains(getOptionLabel(option), fieldState.inputValue)));
153
+ // Ensure the `unset` option is prepended to the top of the list if `unsetLabel` was provided
154
+ setFieldState((prevState) => ({
155
+ ...prevState,
156
+ filteredOptions,
157
+ allOptions: loadedOptions,
158
+ optionsLoading: false,
159
+ }));
160
+ }
161
+ }, []);
162
+ // Only on the first open of the listbox, we want to load the options if they haven't been loaded yet.
163
+ const firstOpen = (0, react_1.useRef)(true);
164
+ function onOpenChange(isOpen) {
165
+ if (firstOpen.current && isOpen) {
166
+ maybeInitLoad(options, fieldState, setFieldState);
167
+ firstOpen.current = false;
168
+ }
169
+ }
170
+ // This is _always_ going to appear new. Maybe `useMemo`?
171
+ const comboBoxProps = {
172
+ ...otherProps,
173
+ placeholder: !values || values.length === 0 ? placeholder : "",
174
+ label: props.label,
175
+ inputValue: fieldState.inputValue,
176
+ // where we might want to do flatmap and return diff kind of array (children ? add level prop) inside children callback - can put markup wrapper div adds padding
177
+ // so we're not doing it multiple places
178
+ items: fieldState.filteredOptions,
179
+ isDisabled,
180
+ isReadOnly,
181
+ onInputChange,
182
+ onOpenChange,
183
+ children: ([item]) => (
184
+ // 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
185
+ (0, jsx_runtime_1.jsx)(react_stately_1.Item, { textValue: getOptionLabel(item), children: getOptionMenuLabel(item) }, (0, Value_1.valueToKey)(getOptionValue(item)))),
186
+ };
187
+ const state = (0, react_stately_1.useComboBoxState)({
188
+ ...comboBoxProps,
189
+ allowsEmptyCollection: true,
190
+ });
191
+ // @ts-ignore - `selectionManager.state` exists, but not according to the types. We are tricking the ComboBox state to support multiple selections.
192
+ state.selectionManager.state = (0, react_stately_1.useMultipleSelectionState)({
193
+ selectionMode: "multiple",
194
+ selectedKeys: fieldState.selectedKeys,
195
+ onSelectionChange: (newKeys) => {
196
+ if (newKeys === "all") {
197
+ // We do not support an "All" option
198
+ return;
199
+ }
200
+ // First figure out which keys changed so we can correctly determine which affiliated options may need to be updated as well.
201
+ const existingKeys = state.selectionManager.selectedKeys;
202
+ const addedKeys = new Set([...newKeys].filter((x) => !existingKeys.has(x)));
203
+ const removedKeys = new Set([...existingKeys].filter((x) => !newKeys.has(x)));
204
+ // Make sure there are changes before we do anything.
205
+ if (addedKeys.size > 0 || removedKeys.size > 0) {
206
+ // Quickly return out of this if all selections are removed
207
+ if (newKeys.size === 0) {
208
+ setFieldState((prevState) => ({ ...prevState, inputValue: "", selectedKeys: [], selectedOptions: [] }));
209
+ onSelect({
210
+ all: { values: [], options: [] },
211
+ leaf: { values: [], options: [] },
212
+ root: { values: [], options: [] },
213
+ });
214
+ return;
215
+ }
216
+ // `onSelectionChange` is only ever going to be adding or removing 1 key at a time.
217
+ // The below logic for adding/removing using a forEach loop is just to make TS happy.
218
+ // This may look like a lot of logic, but in the execution it will only ever be adding/removing 1 key at a time, so it really isn't as bad as it looks.
219
+ // For added keys, we need to see if any other options should be added as well.
220
+ [...addedKeys].forEach((key) => {
221
+ var _a;
222
+ const maybeOption = (0, utils_1.findOption)(fieldState.allOptions, key, getOptionValue);
223
+ if (!maybeOption)
224
+ return;
225
+ const { option, parents } = maybeOption;
226
+ // If the newly added option has children, then consider the children to be newly added keys as well.
227
+ if (option && option.children && option.children.length > 0) {
228
+ const childrenKeys = option.children.flatMap(utils_1.flattenOptions).map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
229
+ [key, ...childrenKeys].forEach(addedKeys.add, addedKeys);
230
+ }
231
+ // If this was a child that was selected, then see if every other child is also selected, and if so, consider the parent selected.
232
+ // Walk up the parents and determine their state.
233
+ for (const parent of parents.reverse()) {
234
+ const allChecked = (_a = parent.children) === null || _a === void 0 ? void 0 : _a.every((child) => {
235
+ const childKey = (0, Value_1.valueToKey)(getOptionValue(child));
236
+ return addedKeys.has(childKey) || existingKeys.has(childKey);
237
+ });
238
+ if (allChecked) {
239
+ addedKeys.add((0, Value_1.valueToKey)(getOptionValue(parent)));
240
+ }
241
+ }
242
+ });
243
+ // For removed keys, we need to also unselect any children and parents of the removed key
244
+ [...removedKeys].forEach((key) => {
245
+ // Grab the option and parents of the option that was removed
246
+ const maybeOption = (0, utils_1.findOption)(fieldState.allOptions, key, getOptionValue);
247
+ if (!maybeOption)
248
+ return;
249
+ const { option, parents } = maybeOption;
250
+ // If the option has children, then we need to unselect those children as well
251
+ if (option.children && option.children.length > 0) {
252
+ const childrenKeys = option.children.flatMap(utils_1.flattenOptions).map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
253
+ [key, ...childrenKeys].forEach(removedKeys.add, removedKeys);
254
+ }
255
+ // If the option has parents, then we need to unselect those parents as well
256
+ if (parents.length > 0) {
257
+ const parentKeys = parents.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
258
+ [key, ...parentKeys].forEach(removedKeys.add, removedKeys);
259
+ }
260
+ });
261
+ // Create the lists to update our TreeState with
262
+ const selectedKeys = new Set([...existingKeys, ...addedKeys].filter((x) => !removedKeys.has(x)));
263
+ const selectedOptions = fieldState.allOptions
264
+ .flatMap(utils_1.flattenOptions)
265
+ .filter((o) => selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(o))));
266
+ // Our `onSelect` callback provides three things:
267
+ // 1. ALL selected values + options
268
+ // 2. Only the top level selected values + options
269
+ // 3. Only the leaf selected values + options
270
+ // For the top level and leaf selections, we need to do some extra work to determine which options are the top level and leaf.
271
+ function getTopLevelSelections(o) {
272
+ // If this element is already selected, return early. Do not bother looking through children.
273
+ if (selectedKeys.has((0, Value_1.valueToKey)(getOptionValue(o))))
274
+ return [o];
275
+ // If this element has no children, then look through the children for top level selected options.
276
+ if (o.children)
277
+ return [...o.children.flatMap(getTopLevelSelections)];
278
+ return [];
279
+ }
280
+ const rootOptions = fieldState.allOptions.flatMap(getTopLevelSelections);
281
+ const rootValues = rootOptions.map(getOptionValue);
282
+ // Finally get the list of options that are just the "leaf" options, meaning they have no children.
283
+ const leafOptions = selectedOptions.filter((o) => !o.children || o.children.length === 0);
284
+ const leafValues = leafOptions.map(getOptionValue);
285
+ setFieldState((prevState) => ({
286
+ ...prevState,
287
+ selectedKeys: [...selectedKeys],
288
+ selectedOptions,
289
+ }));
290
+ onSelect({
291
+ all: { values: [...selectedKeys].map((key) => (0, Value_1.keyToValue)(key)), options: selectedOptions },
292
+ leaf: { values: leafValues, options: leafOptions },
293
+ root: { values: rootValues, options: rootOptions },
294
+ });
295
+ }
296
+ },
297
+ });
298
+ // Resets the TreeFieldState when the 'blur' event is triggered on the input.
299
+ function resetField() {
300
+ const { inputValue, selectedOptions } = fieldState;
301
+ if (inputValue !== "" || (selectedOptions.length === 1 && inputValue !== getOptionLabel(selectedOptions[0]))) {
302
+ setFieldState((prevState) => ({
303
+ ...prevState,
304
+ inputValue: selectedOptions.length === 1 ? getOptionLabel(selectedOptions[0]) : "",
305
+ filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0)),
306
+ allowCollapsing: true,
307
+ }));
308
+ }
309
+ }
310
+ const comboBoxRef = (0, react_1.useRef)(null);
311
+ const triggerRef = (0, react_1.useRef)(null);
312
+ const inputRef = (0, react_1.useRef)(null);
313
+ const inputWrapRef = (0, react_1.useRef)(null);
314
+ const listBoxRef = (0, react_1.useRef)(null);
315
+ const popoverRef = (0, react_1.useRef)(null);
316
+ const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
317
+ ...comboBoxProps,
318
+ inputRef,
319
+ buttonRef: triggerRef,
320
+ listBoxRef,
321
+ popoverRef,
322
+ }, state);
323
+ const { buttonProps } = (0, react_aria_1.useButton)({ ...triggerProps, isDisabled: isDisabled || isReadOnly }, triggerRef);
324
+ // useOverlayPosition moves the overlay to the top of the DOM to avoid any z-index issues. Uses the `targetRef` to DOM placement
325
+ const { overlayProps: positionProps } = (0, react_aria_1.useOverlayPosition)({
326
+ targetRef: inputWrapRef,
327
+ overlayRef: popoverRef,
328
+ scrollRef: listBoxRef,
329
+ shouldFlip: true,
330
+ isOpen: state.isOpen,
331
+ onClose: state.close,
332
+ placement: "bottom left",
333
+ offset: borderless ? 8 : 4,
334
+ });
335
+ positionProps.style = {
336
+ ...positionProps.style,
337
+ width: (_a = comboBoxRef === null || comboBoxRef === void 0 ? void 0 : comboBoxRef.current) === null || _a === void 0 ? void 0 : _a.clientWidth,
338
+ // Ensures the menu never gets too small.
339
+ minWidth: 200,
340
+ };
341
+ return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, labelStyle: labelStyle, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField, nothingSelectedText: nothingSelectedText, typeToFilter: true, 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 }) }))] }));
342
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./TreeSelectField";
2
+ export type { NestedOption, NestedOptionsOrLoad } from "./utils";
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./TreeSelectField"), exports);
@@ -0,0 +1,46 @@
1
+ import { Node } from "@react-types/shared";
2
+ import { Key } from "react";
3
+ import { Value } from "../Value";
4
+ type FoundOption<O> = {
5
+ option: NestedOption<O>;
6
+ parents: NestedOption<O>[];
7
+ };
8
+ export type NestedOption<O> = O & {
9
+ children?: NestedOption<O>[];
10
+ };
11
+ export type NestedOptionsOrLoad<O> = NestedOption<O>[] | {
12
+ initial: NestedOption<O>[];
13
+ load: () => Promise<{
14
+ options: NestedOption<O>[];
15
+ }>;
16
+ };
17
+ export type LeveledOption<O> = [NestedOption<O>, number];
18
+ export type TreeFieldState<O> = {
19
+ inputValue: string;
20
+ filteredOptions: LeveledOption<O>[];
21
+ selectedKeys: Key[];
22
+ selectedOptions: NestedOption<O>[];
23
+ allOptions: NestedOption<O>[];
24
+ optionsLoading: boolean;
25
+ allowCollapsing: boolean;
26
+ };
27
+ export type TreeSelectResponse<O, V extends Value> = {
28
+ all: {
29
+ values: V[];
30
+ options: O[];
31
+ };
32
+ leaf: {
33
+ values: V[];
34
+ options: O[];
35
+ };
36
+ root: {
37
+ values: V[];
38
+ options: O[];
39
+ };
40
+ };
41
+ /** Finds an option by Key, and returns it + any parents. */
42
+ export declare function findOption<O, V extends Value>(options: NestedOption<O>[], key: Key, getOptionValue: (o: O) => V): FoundOption<O> | undefined;
43
+ export declare function flattenOptions<O>(o: NestedOption<O>): NestedOption<O>[];
44
+ export declare function isLeveledOption<O>(option: LeveledOption<O> | any): option is LeveledOption<O>;
45
+ export declare function isLeveledNode<O>(node: Node<O> | Node<LeveledOption<O>>): node is Node<LeveledOption<O>>;
46
+ export {};
@@ -0,0 +1,33 @@
1
+ "use strict";
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. */
5
+ function findOption(options, key, getOptionValue) {
6
+ // This is technically an array of "maybe FoundRow"
7
+ const todo = options.map((option) => ({ option, parents: [] }));
8
+ while (todo.length > 0) {
9
+ const curr = todo.pop();
10
+ if (getOptionValue(curr.option) === key) {
11
+ return curr;
12
+ }
13
+ else if (curr.option.children) {
14
+ // Search our children and pass along us as the parent
15
+ todo.push(...curr.option.children.map((option) => ({ option, parents: [...curr.parents, curr.option] })));
16
+ }
17
+ }
18
+ return undefined;
19
+ }
20
+ exports.findOption = findOption;
21
+ function flattenOptions(o) {
22
+ var _a;
23
+ return [o, ...(((_a = o.children) === null || _a === void 0 ? void 0 : _a.length) ? o.children.flatMap((oc) => flattenOptions(oc)) : [])];
24
+ }
25
+ exports.flattenOptions = flattenOptions;
26
+ function isLeveledOption(option) {
27
+ return Array.isArray(option) && option.length === 2 && typeof option[1] === "number";
28
+ }
29
+ exports.isLeveledOption = isLeveledOption;
30
+ function isLeveledNode(node) {
31
+ return isLeveledOption(node.value);
32
+ }
33
+ exports.isLeveledNode = isLeveledNode;
@@ -17,4 +17,5 @@ export * from "./TextField";
17
17
  export type { TextFieldApi } from "./TextField";
18
18
  export * from "./ToggleButton";
19
19
  export * from "./ToggleChipGroup";
20
+ export * from "./TreeSelectField";
20
21
  export type { Value } from "./Value";
@@ -32,3 +32,4 @@ __exportStar(require("./TextAreaField"), exports);
32
32
  __exportStar(require("./TextField"), exports);
33
33
  __exportStar(require("./ToggleButton"), exports);
34
34
  __exportStar(require("./ToggleChipGroup"), exports);
35
+ __exportStar(require("./TreeSelectField"), exports);
@@ -2,12 +2,13 @@ import { InputHTMLAttributes, LabelHTMLAttributes, MutableRefObject, ReactNode }
2
2
  import { ComboBoxState } from "react-stately";
3
3
  import { PresentationFieldProps } from "../../components/PresentationContext";
4
4
  import { Value } from "../Value";
5
- interface SelectFieldInputProps<O, V extends Value> extends PresentationFieldProps {
5
+ interface ComboBoxInputProps<O, V extends Value> extends PresentationFieldProps {
6
6
  buttonProps: any;
7
7
  buttonRef: MutableRefObject<HTMLButtonElement | null>;
8
8
  inputProps: InputHTMLAttributes<HTMLInputElement>;
9
9
  inputRef: MutableRefObject<HTMLInputElement | null>;
10
10
  inputWrapRef: MutableRefObject<HTMLDivElement | null>;
11
+ listBoxRef?: MutableRefObject<HTMLDivElement | null>;
11
12
  state: ComboBoxState<O>;
12
13
  fieldDecoration?: (opt: O) => ReactNode;
13
14
  errorMsg?: string;
@@ -27,6 +28,7 @@ interface SelectFieldInputProps<O, V extends Value> extends PresentationFieldPro
27
28
  resetField: VoidFunction;
28
29
  hideErrorMessage?: boolean;
29
30
  typeToFilter: boolean;
31
+ isTree?: boolean;
30
32
  }
31
- export declare function ComboBoxInput<O, V extends Value>(props: SelectFieldInputProps<O, V>): import("@emotion/react/jsx-runtime").JSX.Element;
33
+ export declare function ComboBoxInput<O, V extends Value>(props: ComboBoxInputProps<O, V>): import("@emotion/react/jsx-runtime").JSX.Element;
32
34
  export {};
@@ -7,25 +7,50 @@ const react_aria_1 = require("react-aria");
7
7
  const components_1 = require("../../components");
8
8
  const Css_1 = require("../../Css");
9
9
  const TextFieldBase_1 = require("../TextFieldBase");
10
- const utils_1 = require("../../utils");
10
+ const TreeSelectField_1 = require("../TreeSelectField/TreeSelectField");
11
+ const utils_1 = require("../TreeSelectField/utils");
12
+ const utils_2 = require("../../utils");
11
13
  function ComboBoxInput(props) {
12
- const { inputProps, buttonProps, buttonRef, errorMsg, state, fieldDecoration, onBlur, onFocus, selectedOptions, getOptionValue, getOptionLabel, sizeToContent = false, contrast = false, nothingSelectedText, resetField, ...otherProps } = props;
14
+ const { inputProps, buttonProps, buttonRef, errorMsg, state, fieldDecoration, onBlur, onFocus, selectedOptions, getOptionValue, getOptionLabel, sizeToContent = false, contrast = false, nothingSelectedText, resetField, isTree, listBoxRef, ...otherProps } = props;
15
+ const { collapsedKeys, setCollapsedKeys } = (0, TreeSelectField_1.useTreeSelectFieldProvider)();
13
16
  const [isFocused, setIsFocused] = (0, react_1.useState)(false);
14
17
  const isMultiSelect = state.selectionManager.selectionMode === "multiple";
15
18
  const showNumSelection = isMultiSelect && state.selectionManager.selectedKeys.size > 1;
16
19
  // For MultiSelect only show the `fieldDecoration` when input is not in focus.
17
20
  const showFieldDecoration = (!isMultiSelect || (isMultiSelect && !isFocused)) && fieldDecoration && selectedOptions.length === 1;
18
- return ((0, jsx_runtime_1.jsx)(TextFieldBase_1.TextFieldBase, { ...otherProps, errorMsg: errorMsg, contrast: contrast, xss: otherProps.labelStyle !== "inline" && !inputProps.readOnly ? Css_1.Css.fw5.$ : {}, startAdornment: (showNumSelection && ((0, jsx_runtime_1.jsx)("span", { css: Css_1.Css.wPx(16).hPx(16).fs0.br100.bgLightBlue700.white.tinySb.df.aic.jcc.$, children: state.selectionManager.selectedKeys.size }))) ||
21
+ return ((0, jsx_runtime_1.jsx)(TextFieldBase_1.TextFieldBase, { ...otherProps, errorMsg: errorMsg, contrast: contrast, xss: otherProps.labelStyle !== "inline" && !inputProps.readOnly ? Css_1.Css.fw5.$ : {}, startAdornment: (showNumSelection && ((0, jsx_runtime_1.jsx)("span", { css: Css_1.Css.wPx(16).hPx(16).fs0.br100.bgLightBlue700.white.tinySb.df.aic.jcc.$, "data-testid": "selectedOptionsCount", children: state.selectionManager.selectedKeys.size }))) ||
19
22
  (showFieldDecoration && fieldDecoration(selectedOptions[0])), endAdornment: !inputProps.readOnly && ((0, jsx_runtime_1.jsx)("button", { ...buttonProps, disabled: inputProps.disabled, ref: buttonRef, css: {
20
23
  ...Css_1.Css.br4.outline0.gray700.if(contrast).gray400.$,
21
24
  ...(inputProps.disabled ? Css_1.Css.cursorNotAllowed.gray400.if(contrast).gray600.$ : {}),
22
- }, children: (0, jsx_runtime_1.jsx)(components_1.Icon, { icon: state.isOpen ? "chevronUp" : "chevronDown" }) })), inputProps: {
25
+ }, "data-testid": "toggleListBox", children: (0, jsx_runtime_1.jsx)(components_1.Icon, { icon: state.isOpen ? "chevronUp" : "chevronDown" }) })), inputProps: {
23
26
  ...(0, react_aria_1.mergeProps)(inputProps, { "aria-invalid": Boolean(errorMsg), onInput: () => state.open() }),
24
27
  // Not merging the following as we want them to overwrite existing events
25
28
  ...{
26
29
  onKeyDown: (e) => {
27
30
  // We need to do some custom logic when using MultiSelect, as react-aria/stately Combobox doesn't support multiselect out of the box.
28
31
  if (isMultiSelect) {
32
+ if (isTree) {
33
+ const item = state.collection.getItem(state.selectionManager.focusedKey);
34
+ if (item && (e.key === "ArrowRight" || e.key === "ArrowLeft")) {
35
+ if (!(0, utils_1.isLeveledNode)(item))
36
+ return;
37
+ const leveledOption = item.value;
38
+ if (!leveledOption)
39
+ return;
40
+ const [option] = leveledOption;
41
+ e.stopPropagation();
42
+ e.preventDefault();
43
+ if (option && option.children && option.children.length > 0) {
44
+ if (collapsedKeys.includes(item.key) && e.key === "ArrowRight") {
45
+ setCollapsedKeys((prevKeys) => prevKeys.filter((k) => k !== item.key));
46
+ }
47
+ else if (!collapsedKeys.includes(item.key) && e.key === "ArrowLeft") {
48
+ setCollapsedKeys((prevKeys) => [...prevKeys, item.key]);
49
+ }
50
+ }
51
+ return;
52
+ }
53
+ }
29
54
  // Enter should toggle the focused item.
30
55
  if (e.key === "Enter") {
31
56
  // Prevent form submissions if menu is open.
@@ -54,9 +79,11 @@ function ComboBoxInput(props) {
54
79
  inputProps.onKeyDown && inputProps.onKeyDown(e);
55
80
  },
56
81
  onBlur: (e) => {
57
- // Do not call onBlur if readOnly or interacting within the input wrapper (such as the menu trigger button).
82
+ var _a;
83
+ // Do not call onBlur if readOnly or interacting within the input wrapper (such as the menu trigger button), or anything within the listbox.
58
84
  if (inputProps.readOnly ||
59
- (props.inputWrapRef.current && props.inputWrapRef.current.contains(e.relatedTarget))) {
85
+ (props.inputWrapRef.current && props.inputWrapRef.current.contains(e.relatedTarget)) ||
86
+ (((_a = props.listBoxRef) === null || _a === void 0 ? void 0 : _a.current) && props.listBoxRef.current.contains(e.relatedTarget))) {
60
87
  return;
61
88
  }
62
89
  // We purposefully override onBlur here instead of using mergeProps, b/c inputProps.onBlur
@@ -64,7 +91,7 @@ function ComboBoxInput(props) {
64
91
  // detects a) there is no props.selectedKey (b/c we don't pass it), and b) there is an
65
92
  // `inputValue`, so it thinks it needs to call `resetInputValue()`.
66
93
  setIsFocused(false);
67
- (0, utils_1.maybeCall)(onBlur);
94
+ (0, utils_2.maybeCall)(onBlur);
68
95
  state.close();
69
96
  // Always call `resetField` onBlur, this ensures the field's `input.value` resets
70
97
  // to what it should be in case it doesn't currently match.
@@ -74,7 +101,7 @@ function ComboBoxInput(props) {
74
101
  if (inputProps.readOnly)
75
102
  return;
76
103
  setIsFocused(true);
77
- (0, utils_1.maybeCall)(onFocus);
104
+ (0, utils_2.maybeCall)(onFocus);
78
105
  },
79
106
  onClick: () => {
80
107
  var _a;
@@ -89,7 +116,7 @@ function ComboBoxInput(props) {
89
116
  // 3. Use `nothingSelectedText`
90
117
  // 4. Default to "1"
91
118
  // And do not allow it to grow past a size of 20.
92
- // TODO: Combine logic to determine the input's value. Similar logic is used in SelectFieldBase, though it is intertwined with other state logic. Such as when to open/close menu, or filter the options within that menu, etc...
119
+ // TODO: Combine logic to determine the input's value. Similar logic is used in ComboBoxBase, though it is intertwined with other state logic. Such as when to open/close menu, or filter the options within that menu, etc...
93
120
  sizeToContent
94
121
  ? Math.min(String(inputProps.value ||
95
122
  (isMultiSelect && selectedOptions.length === 1 && getOptionLabel(selectedOptions[0])) ||
@@ -11,6 +11,8 @@ interface ListBoxProps<O, V extends Key> {
11
11
  positionProps: React.HTMLAttributes<Element>;
12
12
  loading?: boolean | (() => JSX.Element);
13
13
  disabledOptionsWithReasons?: Record<string, string | undefined>;
14
+ isTree?: boolean;
15
+ allowCollapsing?: boolean;
14
16
  }
15
17
  /** A ListBox is an internal component used by SelectField and MultiSelectField to display the list of options */
16
18
  export declare function ListBox<O, V extends Key>(props: ListBoxProps<O, V>): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ const VirtualizedOptions_1 = require("./VirtualizedOptions");
12
12
  /** A ListBox is an internal component used by SelectField and MultiSelectField to display the list of options */
13
13
  function ListBox(props) {
14
14
  var _a;
15
- const { state, listBoxRef, selectedOptions = [], getOptionLabel, getOptionValue, contrast = false, positionProps, horizontalLayout = false, loading, disabledOptionsWithReasons = {}, } = props;
15
+ const { state, listBoxRef, selectedOptions = [], getOptionLabel, getOptionValue, contrast = false, positionProps, horizontalLayout = false, loading, disabledOptionsWithReasons = {}, isTree, allowCollapsing, } = props;
16
16
  const { listBoxProps } = (0, react_aria_1.useListBox)({ disallowEmptySelection: true, ...props }, state, listBoxRef);
17
17
  const positionMaxHeight = (_a = positionProps.style) === null || _a === void 0 ? void 0 : _a.maxHeight;
18
18
  // The popoverMaxHeight will be based on the value defined by the positionProps returned from `useOverlayPosition` (which will always be a defined as a `number` based on React-Aria's `calculatePosition`).
@@ -57,11 +57,11 @@ function ListBox(props) {
57
57
  // Add `w50` in that case to ensure the ListBox is only the width of the field. If the width definitions ever change, we need to update here as well.
58
58
  ...Css_1.Css.bgWhite.br4.w100.bshBasic.hPx(popoverHeight).df.fdc.if(contrast).bgGray700.if(horizontalLayout).w50.$,
59
59
  "&:hover": Css_1.Css.bshHover.$,
60
- }, ref: listBoxRef, ...listBoxProps, children: [isMultiSelect && state.selectionManager.selectedKeys.size > 0 && ((0, jsx_runtime_1.jsx)("ul", { css: Css_1.Css.listReset.pt2.pl2.pb1.pr1.df.bb.bGray200.add("flexWrap", "wrap").maxh("50%").overflowAuto.$, ref: selectedList, children: selectedOptions.map((o) => ((0, jsx_runtime_1.jsx)(ListBoxToggleChip_1.ListBoxToggleChip, { state: state, option: o, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, disabled: state.disabledKeys.has(getOptionValue(o)) }, getOptionValue(o)))) })), (0, jsx_runtime_1.jsx)("ul", { css: Css_1.Css.listReset.fg1.$, children: hasSections ? ([...state.collection].map((section) => ((0, jsx_runtime_1.jsx)(ListBoxSection_1.ListBoxSection, { section: section, state: state, contrast: contrast, onListHeightChange: onListHeightChange, popoverHeight: popoverHeight,
60
+ }, ref: listBoxRef, ...listBoxProps, children: [isMultiSelect && !isTree && state.selectionManager.selectedKeys.size > 0 && ((0, jsx_runtime_1.jsx)("ul", { css: Css_1.Css.listReset.pt2.pl2.pb1.pr1.df.bb.bGray200.add("flexWrap", "wrap").maxh("50%").overflowAuto.$, ref: selectedList, children: selectedOptions.map((o) => ((0, jsx_runtime_1.jsx)(ListBoxToggleChip_1.ListBoxToggleChip, { state: state, option: o, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, disabled: state.disabledKeys.has(getOptionValue(o)) }, getOptionValue(o)))) })), (0, jsx_runtime_1.jsx)("ul", { css: Css_1.Css.listReset.fg1.$, children: hasSections ? ([...state.collection].map((section) => ((0, jsx_runtime_1.jsx)(ListBoxSection_1.ListBoxSection, { section: section, state: state, contrast: contrast, onListHeightChange: onListHeightChange, popoverHeight: popoverHeight,
61
61
  // Only scroll on focus if using VirtualFocus (used for ComboBoxState (SelectField), but not SelectState (ChipSelectField))
62
62
  scrollOnFocus: props.shouldUseVirtualFocus, disabledOptionsWithReasons: disabledOptionsWithReasons }, section.key)))) : ((0, jsx_runtime_1.jsx)(VirtualizedOptions_1.VirtualizedOptions, { state: state, items: [...state.collection], onListHeightChange: onListHeightChange, contrast: contrast,
63
63
  // Only scroll on focus if using VirtualFocus (used for ComboBoxState (SelectField), but not SelectState (ChipSelectField))
64
- scrollOnFocus: props.shouldUseVirtualFocus, loading: loading, disabledOptionsWithReasons: disabledOptionsWithReasons })) })] }));
64
+ scrollOnFocus: props.shouldUseVirtualFocus, loading: loading, disabledOptionsWithReasons: disabledOptionsWithReasons, isTree: isTree, allowCollapsing: allowCollapsing })) })] }));
65
65
  }
66
66
  exports.ListBox = ListBox;
67
67
  // UX specified maximum height for a ListBox (in pixels)
@@ -22,28 +22,10 @@ function Option(props) {
22
22
  // Get props for the option element.
23
23
  // Prevent options from receiving browser focus via shouldUseVirtualFocus.
24
24
  const { optionProps, isDisabled, isFocused, isSelected } = (0, react_aria_1.useOption)({ key: item.key, shouldSelectOnPressUp: true, shouldFocusOnHover: false }, state, ref);
25
- // Additional onKeyDown logic to ensure the the virtualized list (in <VirtualizedOptions />) scrolls to keep the "focused" option in view
26
- const onKeyDown = (0, react_1.useCallback)((e) => {
27
- if (!scrollToIndex || !(e.key === "ArrowDown" || e.key === "ArrowUp")) {
28
- return;
29
- }
30
- const toKey = e.key === "ArrowDown" ? item.nextKey : item.prevKey;
31
- if (!toKey) {
32
- return;
33
- }
34
- const toItem = state.collection.getItem(toKey);
35
- // Only scroll the "options" (`state.collection` is a flat list of sections and items - we want to avoid scrolling to a "section" as it is not shown in the UI)
36
- if (toItem &&
37
- // Ensure we are only ever scrolling to an "option".
38
- (toItem.parentKey === "options" || (!toItem.parentKey && toItem.type === "item")) &&
39
- toItem.index !== undefined) {
40
- scrollToIndex(toItem.index);
41
- }
42
- }, [scrollToIndex, state]);
43
25
  return (0, components_1.maybeTooltip)({
44
26
  title: disabledReason,
45
27
  placement: "right",
46
- children: ((0, jsx_runtime_1.jsxs)("li", { ...(0, react_aria_1.mergeProps)(optionProps, hoverProps, { onKeyDown }), ref: ref, css: {
28
+ children: ((0, jsx_runtime_1.jsxs)("li", { ...(0, react_aria_1.mergeProps)(optionProps, hoverProps), ref: ref, css: {
47
29
  ...Css_1.Css.df.aic.jcsb.py1.px2.mh("42px").outline0.cursorPointer.sm.$,
48
30
  // Assumes only one Persistent Item per list - will need to change to utilize Sections if that assumption is incorrect.
49
31
  ...((0, ChipSelectField_1.isPersistentKey)(item.key) ? Css_1.Css.bt.bGray200.$ : {}),
@@ -1,14 +1,17 @@
1
1
  /// <reference types="react" />
2
2
  import { Node } from "@react-types/shared";
3
3
  import { SelectState } from "react-stately";
4
+ import { LeveledOption } from "../TreeSelectField/utils";
4
5
  interface VirtualizedOptionsProps<O> {
5
6
  state: SelectState<O>;
6
- items: Node<O>[];
7
+ items: Node<O>[] | Node<LeveledOption<O>>[];
7
8
  onListHeightChange: (height: number) => void;
8
9
  contrast: boolean;
9
10
  scrollOnFocus?: boolean;
10
11
  loading?: boolean | (() => JSX.Element);
11
12
  disabledOptionsWithReasons: Record<string, string | undefined>;
13
+ isTree?: boolean;
14
+ allowCollapsing?: boolean;
12
15
  }
13
16
  export declare function VirtualizedOptions<O>(props: VirtualizedOptionsProps<O>): import("@emotion/react/jsx-runtime").JSX.Element;
14
17
  export {};
@@ -6,9 +6,11 @@ const react_1 = require("react");
6
6
  const react_virtuoso_1 = require("react-virtuoso");
7
7
  const LoadingDots_1 = require("./LoadingDots");
8
8
  const Option_1 = require("./Option");
9
+ const TreeOption_1 = require("../TreeSelectField/TreeOption");
10
+ const utils_1 = require("../TreeSelectField/utils");
9
11
  // Displays ListBox options in a virtualized container for performance reasons
10
12
  function VirtualizedOptions(props) {
11
- const { state, items, onListHeightChange, contrast, scrollOnFocus, loading, disabledOptionsWithReasons } = props;
13
+ const { state, items, onListHeightChange, contrast, scrollOnFocus, loading, disabledOptionsWithReasons, isTree, allowCollapsing, } = props;
12
14
  const virtuosoRef = (0, react_1.useRef)(null);
13
15
  const focusedItem = state.collection.getItem(state.selectionManager.focusedKey);
14
16
  const selectedItem = state.selectionManager.selectedKeys.size > 0
@@ -26,13 +28,20 @@ function VirtualizedOptions(props) {
26
28
  // We don't really need to set this, but it's handy for tests, which would
27
29
  // otherwise render just 1 row. A better way to do this would be to jest.mock
28
30
  // out Virtuoso with an impl that just rendered everything, but doing this for now.
29
- initialItemCount: 5, itemContent: (idx) => {
31
+ initialItemCount: 10, itemContent: (idx) => {
30
32
  var _a;
31
33
  const item = items[idx];
32
34
  if (item) {
33
- return ((0, jsx_runtime_1.jsx)(Option_1.Option, { item: item, state: state, contrast: contrast,
34
- // Only send scrollToIndex functionality forward if we are not auto-scrolling on focus.
35
- scrollToIndex: scrollOnFocus ? undefined : (_a = virtuosoRef.current) === null || _a === void 0 ? void 0 : _a.scrollToIndex, disabledReason: disabledOptionsWithReasons[item.key] }, item.key));
35
+ if (isTree && (0, utils_1.isLeveledNode)(item)) {
36
+ return ((0, jsx_runtime_1.jsx)(TreeOption_1.TreeOption, { item: item, state: state, contrast: contrast,
37
+ // scrollToIndex={scrollOnFocus ? undefined : virtuosoRef.current?.scrollToIndex}
38
+ allowCollapsing: allowCollapsing }, item.key));
39
+ }
40
+ if (!(0, utils_1.isLeveledNode)(item)) {
41
+ return ((0, jsx_runtime_1.jsx)(Option_1.Option, { item: item, state: state, contrast: contrast,
42
+ // Only send scrollToIndex functionality forward if we are not auto-scrolling on focus.
43
+ scrollToIndex: scrollOnFocus ? undefined : (_a = virtuosoRef.current) === null || _a === void 0 ? void 0 : _a.scrollToIndex, disabledReason: disabledOptionsWithReasons[item.key] }, item.key));
44
+ }
36
45
  }
37
46
  }, components: !loading
38
47
  ? {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.263.0",
3
+ "version": "2.265.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",