@homebound/beam 2.362.1 → 2.363.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.
@@ -4,6 +4,7 @@ export interface TestModalContentProps {
4
4
  withTag?: boolean;
5
5
  withDateField?: boolean;
6
6
  withTextArea?: boolean;
7
+ withTextField?: boolean;
7
8
  }
8
9
  /** A fake modal content component that we share across the modal and superdrawer stories. */
9
10
  export declare function TestModalContent(props: TestModalContentProps): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -24,7 +24,7 @@ function TestModalContent(props) {
24
24
  const [date, setDate] = (0, react_1.useState)(formStateDomain_1.jan1);
25
25
  const [internalValue, setValue] = (0, react_1.useState)("");
26
26
  const { triggerNotice } = (0, Snackbar_1.useSnackbar)();
27
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Modal_1.ModalHeader, { children: props.withTag ? ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.aic.$, children: [(0, jsx_runtime_1.jsx)("span", { children: "Modal Title with Tag" }), (0, jsx_runtime_1.jsx)(Tag_1.Tag, { text: "In progress", type: "info", xss: Css_1.Css.ml1.$ })] })) : props.withTextArea ? ((0, jsx_runtime_1.jsx)(inputs_1.TextAreaField, { label: "Title", placeholder: "Test title", value: internalValue, onChange: (v) => setValue(v), preventNewLines: true, labelStyle: "hidden", borderless: true, xss: Css_1.Css.xl.$ })) : ("The title of the modal that might wrap") }), (0, jsx_runtime_1.jsxs)(Modal_1.ModalBody, { children: [(0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.fdc.aifs.$, children: [(0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.$, children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { label: "More", onClick: () => setNumSentences(numSentences + 2) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Clear", onClick: () => setNumSentences(0) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Primary", onClick: () => setPrimaryDisabled(!primaryDisabled) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Trigger Snackbar", onClick: () => triggerNotice({ message: "Snackbar message" }) }), showLeftAction && ((0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Left Action", onClick: () => setLeftActionDisabled(!leftActionDisabled) }))] }), (0, jsx_runtime_1.jsx)("p", { children: "The body content of the modal. This content can be anything!".repeat(numSentences) })] }), withDateField && (0, jsx_runtime_1.jsx)(inputs_1.DateField, { value: date, label: "Date", onChange: setDate })] }), (0, jsx_runtime_1.jsxs)(Modal_1.ModalFooter, { xss: showLeftAction ? Css_1.Css.jcsb.$ : undefined, children: [showLeftAction && ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Clear", onClick: (0, addon_actions_1.action)("Clear Action"), variant: "tertiary", disabled: leftActionDisabled }) })), (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.$, children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Cancel", onClick: closeModal, variant: "tertiary" }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Apply", onClick: (0, addon_actions_1.action)("Primary action"), disabled: primaryDisabled })] })] })] }));
27
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(Modal_1.ModalHeader, { children: props.withTag ? ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.aic.$, children: [(0, jsx_runtime_1.jsx)("span", { children: "Modal Title with Tag" }), (0, jsx_runtime_1.jsx)(Tag_1.Tag, { text: "In progress", type: "info", xss: Css_1.Css.ml1.$ })] })) : props.withTextField ? ((0, jsx_runtime_1.jsx)(inputs_1.TextField, { label: "Title", placeholder: "Test title", value: internalValue, onChange: (v) => setValue(v), labelStyle: "hidden", onEscapeBubble: true, borderless: true, xss: Css_1.Css.xl.$ })) : props.withTextArea ? ((0, jsx_runtime_1.jsx)(inputs_1.TextAreaField, { label: "Title", placeholder: "Test title", value: internalValue, onChange: (v) => setValue(v), preventNewLines: true, labelStyle: "hidden", borderless: true, xss: Css_1.Css.xl.$ })) : ("The title of the modal that might wrap") }), (0, jsx_runtime_1.jsxs)(Modal_1.ModalBody, { children: [(0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.fdc.aifs.$, children: [(0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.$, children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { label: "More", onClick: () => setNumSentences(numSentences + 2) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Clear", onClick: () => setNumSentences(0) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Primary", onClick: () => setPrimaryDisabled(!primaryDisabled) }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Trigger Snackbar", onClick: () => triggerNotice({ message: "Snackbar message" }) }), showLeftAction && ((0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Left Action", onClick: () => setLeftActionDisabled(!leftActionDisabled) }))] }), (0, jsx_runtime_1.jsx)("p", { children: "The body content of the modal. This content can be anything!".repeat(numSentences) })] }), withDateField && (0, jsx_runtime_1.jsx)(inputs_1.DateField, { value: date, label: "Date", onChange: setDate })] }), (0, jsx_runtime_1.jsxs)(Modal_1.ModalFooter, { xss: showLeftAction ? Css_1.Css.jcsb.$ : undefined, children: [showLeftAction && ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Clear", onClick: (0, addon_actions_1.action)("Clear Action"), variant: "tertiary", disabled: leftActionDisabled }) })), (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.gap1.$, children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Cancel", onClick: closeModal, variant: "tertiary" }), (0, jsx_runtime_1.jsx)(Button_1.Button, { label: "Apply", onClick: (0, addon_actions_1.action)("Primary action"), disabled: primaryDisabled })] })] })] }));
28
28
  }
29
29
  exports.TestModalContent = TestModalContent;
30
30
  function TestModalFilterTable() {
@@ -6,6 +6,16 @@ export interface TextFieldProps<X> extends BeamTextFieldProps<X> {
6
6
  clearable?: boolean;
7
7
  api?: MutableRefObject<TextFieldApi | undefined>;
8
8
  onEnter?: VoidFunction;
9
+ /**
10
+ * Allows a TextField to opt-in to bubbling up the escape key event to its parent.
11
+ *
12
+ * Usually this is a bad idea, because escape-in-a-modal might lose the user's WIP (without
13
+ * sufficient "are you sure" checking), and so instead we let callers opt-in to this.
14
+ *
15
+ * Note that react-aria's `useSearchField` / `useComboBox` seems to have this built-in:
16
+ * https://github.com/adobe/react-spectrum/issues/5480
17
+ */
18
+ onEscapeBubble?: boolean;
9
19
  endAdornment?: ReactNode;
10
20
  startAdornment?: ReactNode;
11
21
  hideErrorMessage?: boolean;
@@ -8,7 +8,7 @@ const components_1 = require("../components");
8
8
  const TextFieldBase_1 = require("./TextFieldBase");
9
9
  const utils_1 = require("../utils");
10
10
  function TextField(props) {
11
- const { disabled = false, readOnly = false, required, errorMsg, value = "", onBlur, onFocus, api, onEnter, hideErrorMessage, ...otherProps } = props;
11
+ const { disabled = false, readOnly = false, required, errorMsg, value = "", onBlur, onFocus, api, onEnter, onEscapeBubble, hideErrorMessage, ...otherProps } = props;
12
12
  const isDisabled = !!disabled;
13
13
  const isReadOnly = !!readOnly;
14
14
  const textFieldProps = {
@@ -28,6 +28,10 @@ function TextField(props) {
28
28
  (0, utils_1.maybeCall)(onEnter);
29
29
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
30
30
  }
31
+ else if (e.key === "Escape" && onEscapeBubble) {
32
+ // Allow closing modals from within text fields...
33
+ e.continuePropagation();
34
+ }
31
35
  },
32
36
  }, inputRef);
33
37
  // Construct our TextFieldApi to give access to some imperative methods
@@ -42,7 +42,19 @@ function TreeSelectField(props) {
42
42
  const { getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
43
43
  getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
44
44
  options, onSelect, values, defaultCollapsed = false, ...otherProps } = props;
45
- const [collapsedKeys, setCollapsedKeys] = (0, react_1.useState)(Array.isArray(options) && defaultCollapsed ? options.map((o) => getOptionValue(o)) : []);
45
+ const [collapsedKeys, setCollapsedKeys] = (0, react_1.useState)([]);
46
+ (0, react_1.useEffect)(() => {
47
+ setCollapsedKeys(!Array.isArray(options)
48
+ ? []
49
+ : defaultCollapsed
50
+ ? options.map((o) => getOptionValue(o))
51
+ : options
52
+ .flatMap(utils_1.flattenOptions)
53
+ .filter((o) => o.defaultCollapsed)
54
+ .map((o) => getOptionValue(o)));
55
+ // Explicitly ignoring `getOptionValue` as it typically isn't memo'd
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [options, defaultCollapsed]);
46
58
  const contextValue = (0, react_1.useMemo)(() => ({ collapsedKeys, setCollapsedKeys, getOptionValue }),
47
59
  // 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
48
60
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -84,14 +96,17 @@ function TreeSelectFieldBase(props) {
84
96
  // Find the options that matches the value. These could be parents or a children.
85
97
  const foundOptions = (0, utils_1.findOptions)(initialOptions, (0, Value_1.valueToKey)(v), getOptionValue);
86
98
  // Go through the `foundOptions` and get the keys of the options and its children if it has any.
87
- return foundOptions.flatMap(({ option }) => {
88
- var _a, _b;
89
- return [
90
- (0, Value_1.valueToKey)(getOptionValue(option)),
91
- ...((_b = (_a = option.children) === null || _a === void 0 ? void 0 : _a.flatMap((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _b !== void 0 ? _b : []),
92
- ];
93
- });
99
+ return foundOptions.flatMap(({ option }) => selectOptionAndAllChildren(option));
94
100
  }));
101
+ function selectOptionAndAllChildren(maybeParent) {
102
+ var _a, _b;
103
+ // Check if the maybeParent has children, if so, return those as selected keys
104
+ // Do in a recursive way so that children may have children
105
+ return [
106
+ (0, Value_1.valueToKey)(getOptionValue(maybeParent)),
107
+ ...((_b = (_a = maybeParent.children) === null || _a === void 0 ? void 0 : _a.flatMap(selectOptionAndAllChildren)) !== null && _b !== void 0 ? _b : []),
108
+ ];
109
+ }
95
110
  // It is possible that all the children of a parent were considered selected `values`, but the parent wasn't included in the `values` array.
96
111
  // In this case, the parent also should be considered a selected option.
97
112
  function areAllChildrenSelected(maybeParent) {
@@ -146,17 +161,27 @@ function TreeSelectFieldBase(props) {
146
161
  getOptionLabel,
147
162
  isReadOnly,
148
163
  nothingSelectedText,
149
- collapsedKeys,
150
164
  getOptionValue,
165
+ collapsedKeys,
151
166
  ]);
152
167
  // Initialize the TreeFieldState
153
168
  const [fieldState, setFieldState] = (0, react_1.useState)(() => initTreeFieldState());
169
+ (0, react_1.useEffect)(() => {
170
+ // We don't want to do this if initialOptions is not an array, because we would be lazy loading `allOptions`
171
+ if (Array.isArray(options)) {
172
+ setFieldState((prevState) => ({ ...prevState, allOptions: options }));
173
+ }
174
+ }, [options]);
154
175
  // Reset the TreeFieldState if the values array changes and doesn't match the selectedOptions
155
176
  (0, react_1.useEffect)(() => {
156
177
  // if the values does not match the values in the fieldState, then update the fieldState
157
178
  const selectedKeys = fieldState.selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
158
- if (values &&
159
- (values.length !== selectedKeys.length || !values.every((v) => selectedKeys.includes((0, Value_1.valueToKey)(v))))) {
179
+ if (
180
+ // If the values were cleared
181
+ (values === undefined && selectedKeys.length !== 0) ||
182
+ // Or values were set, but they don't match the selected keys
183
+ (values && (values.length !== selectedKeys.length || !values.every((v) => selectedKeys.includes((0, Value_1.valueToKey)(v)))))) {
184
+ // Then reinitialize
160
185
  setFieldState(initTreeFieldState());
161
186
  }
162
187
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -218,7 +243,7 @@ function TreeSelectFieldBase(props) {
218
243
  setFieldState((prevState) => ({
219
244
  ...prevState,
220
245
  inputValue: "",
221
- filteredOptions: initialOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
246
+ filteredOptions: prevState.allOptions.flatMap((o) => levelOptions(o, 0, false, collapsedKeys, getOptionValue)),
222
247
  }));
223
248
  }
224
249
  }
@@ -417,10 +442,10 @@ function TreeSelectFieldBase(props) {
417
442
  ...positionProps.style,
418
443
  width: (_c = comboBoxRef === null || comboBoxRef === void 0 ? void 0 : comboBoxRef.current) === null || _c === void 0 ? void 0 : _c.clientWidth,
419
444
  // Ensures the menu never gets too small.
420
- minWidth: 200,
445
+ minWidth: 320,
421
446
  };
422
447
  const fieldMaxWidth = (0, utils_2.getFieldWidth)(fullWidth);
423
- return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw(fieldMaxWidth).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, fullWidth: fullWidth, labelStyle: labelStyle, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, selectedOptionsLabels: fieldState.selectedOptionsLabels, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField, nothingSelectedText: nothingSelectedText, isTree: true }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200, children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, { ...listBoxProps, positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, horizontalLayout: labelStyle === "left", loading: fieldState.optionsLoading, allowCollapsing: fieldState.allowCollapsing, isTree: true }) }))] }));
448
+ return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw(fieldMaxWidth).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, fullWidth: fullWidth, labelStyle: labelStyle, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, selectedOptionsLabels: fieldState.selectedOptionsLabels, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField, nothingSelectedText: nothingSelectedText, isTree: true }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 320, 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 }) }))] }));
424
449
  }
425
450
  function levelOptions(o, level, filtering, collapsedKeys, getOptionValue) {
426
451
  var _a;
@@ -7,6 +7,7 @@ type FoundOption<O> = {
7
7
  };
8
8
  export type NestedOption<O> = O & {
9
9
  children?: NestedOption<O>[];
10
+ defaultCollapsed?: boolean;
10
11
  };
11
12
  export type NestedOptionsOrLoad<O> = NestedOption<O>[] | {
12
13
  current: NestedOption<O>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.362.1",
3
+ "version": "2.363.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",