@homebound/beam 2.319.1 → 2.320.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.
@@ -15,7 +15,7 @@ class SingleFilter extends BaseFilter_1.BaseFilter {
15
15
  const { label, defaultValue, options: maybeOptions, getOptionLabel, getOptionValue, nothingSelectedText, ...props } = this.props;
16
16
  const options = Array.isArray(maybeOptions)
17
17
  ? [allOption, ...maybeOptions]
18
- : { ...maybeOptions, initial: [allOption, ...maybeOptions.initial] };
18
+ : { ...maybeOptions, current: maybeOptions.current };
19
19
  return ((0, jsx_runtime_1.jsx)(SelectField_1.SelectField, { ...props, options: options, getOptionValue: (o) => (o === allOption ? undefined : getOptionValue(o)), getOptionLabel: (o) => (o === allOption ? nothingSelectedText !== null && nothingSelectedText !== void 0 ? nothingSelectedText : "All" : getOptionLabel(o)), compact: !vertical, value: value, label: this.label, labelStyle: inModal ? "hidden" : !inModal && !vertical ? "inline" : "above", sizeToContent: !inModal && !vertical, nothingSelectedText: nothingSelectedText !== null && nothingSelectedText !== void 0 ? nothingSelectedText : "All", onSelect: (value) => setValue(value || undefined), ...this.testId(tid) }));
20
20
  }
21
21
  }
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SelectField = void 0;
4
4
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
+ const react_1 = require("react");
5
6
  const ComboBoxBase_1 = require("./internal/ComboBoxBase");
6
7
  function SelectField(props) {
7
8
  const { getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
8
9
  getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
9
10
  options, onSelect, value, ...otherProps } = props;
10
- return ((0, jsx_runtime_1.jsx)(ComboBoxBase_1.ComboBoxBase, { ...otherProps, options: options, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, values: [value], onSelect: (values, options) => {
11
+ const values = (0, react_1.useMemo)(() => [value], [value]);
12
+ return ((0, jsx_runtime_1.jsx)(ComboBoxBase_1.ComboBoxBase, { ...otherProps, options: options, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, values: values, onSelect: (values, options) => {
11
13
  // If the user used `unsetLabel`, then values will be `[undefined]` and options `[unsetOption]`
12
14
  if (values.length > 0 && options.length > 0) {
13
15
  const option = options[0];
@@ -64,7 +64,7 @@ function TreeSelectFieldBase(props) {
64
64
  const { values, options, getOptionValue, getOptionLabel, getOptionMenuLabel = getOptionLabel, disabled, readOnly, labelStyle, borderless, contrast = false, nothingSelectedText = "", onSelect, defaultCollapsed = false, placeholder, ...otherProps } = props;
65
65
  const isDisabled = !!disabled;
66
66
  const isReadOnly = !!readOnly;
67
- const initialOptions = Array.isArray(options) ? options : options.initial;
67
+ const initialOptions = Array.isArray(options) ? options : options.current;
68
68
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
69
69
  const { collapsedKeys } = useTreeSelectFieldProvider();
70
70
  function levelOptions(o, level, filtering) {
@@ -9,7 +9,7 @@ export type NestedOption<O> = O & {
9
9
  children?: NestedOption<O>[];
10
10
  };
11
11
  export type NestedOptionsOrLoad<O> = NestedOption<O>[] | {
12
- initial: NestedOption<O>[];
12
+ current: NestedOption<O>[];
13
13
  load: () => Promise<{
14
14
  options: NestedOption<O>[];
15
15
  }>;
@@ -2,6 +2,7 @@ import React, { ReactNode } from "react";
2
2
  import { PresentationFieldProps } from "../../components/PresentationContext";
3
3
  import { Value } from "../Value";
4
4
  import { BeamFocusableProps } from "../../interfaces";
5
+ /** Base props for either `SelectField` or `MultiSelectField`. */
5
6
  export interface ComboBoxBaseProps<O, V extends Value> extends BeamFocusableProps, PresentationFieldProps {
6
7
  /** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. `isUnsetOpt` is only defined for single SelectField */
7
8
  getOptionMenuLabel?: (opt: O, isUnsetOpt?: boolean) => string | ReactNode;
@@ -62,13 +63,17 @@ export interface ComboBoxBaseProps<O, V extends Value> extends BeamFocusableProp
62
63
  * and so we cannot easily change them.
63
64
  */
64
65
  export declare function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>): JSX.Element;
66
+ /** Allows lazy-loading select fields, which is useful for pages w/lots of fields the user may not actually use. */
65
67
  export type OptionsOrLoad<O> = O[] | {
66
- initial: O[];
67
- load: () => Promise<{
68
- options: O[];
69
- }>;
68
+ /** The initial option to show before the user interacts with the dropdown. */
69
+ current: O | undefined;
70
+ /** Fired when the user interacts with the dropdown, to load the real options. */
71
+ load: () => Promise<unknown>;
72
+ /** The full list of options, after load() has been fired. */
73
+ options: O[] | undefined;
70
74
  };
71
75
  export declare function initializeOptions<O>(options: OptionsOrLoad<O>, unsetLabel: string | undefined): OptionsOrLoad<O>;
76
+ /** A marker option to automatically add an "Unset" option to the start of options. */
72
77
  export declare const unsetOption: {};
73
78
  export declare function disabledOptionToKeyedTuple(disabledOption: Value | {
74
79
  value: Value;
@@ -25,31 +25,20 @@ const utils_1 = require("../../utils");
25
25
  function ComboBoxBase(props) {
26
26
  var _a, _b, _c, _d;
27
27
  const { fieldProps } = (0, PresentationContext_1.usePresentationContext)();
28
- const { disabled, readOnly, onSelect, options, multiselect = false, values = [], nothingSelectedText = "", contrast, disabledOptions, borderless, unsetLabel, ...otherProps } = props;
28
+ const { disabled, readOnly, onSelect, options, multiselect = false, values = [], nothingSelectedText = "", contrast, disabledOptions, borderless, unsetLabel, getOptionLabel: propOptionLabel, getOptionValue: propOptionValue, getOptionMenuLabel: propOptionMenuLabel, ...otherProps } = props;
29
29
  const labelStyle = (_b = (_a = otherProps.labelStyle) !== null && _a !== void 0 ? _a : fieldProps === null || fieldProps === void 0 ? void 0 : fieldProps.labelStyle) !== null && _b !== void 0 ? _b : "above";
30
30
  // Call `initializeOptions` to prepend the `unset` option if the `unsetLabel` was provided.
31
31
  const maybeOptions = (0, react_1.useMemo)(() => initializeOptions(options, unsetLabel), [options, unsetLabel]);
32
32
  // Memoize the callback functions and handle the `unset` option if provided.
33
- const getOptionLabel = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? unsetLabel : props.getOptionLabel(o)),
34
- // 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
35
- // eslint-disable-next-line react-hooks/exhaustive-deps
36
- [props.getOptionLabel, unsetLabel]);
37
- const getOptionValue = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? undefined : props.getOptionValue(o)),
38
- // 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
39
- // eslint-disable-next-line react-hooks/exhaustive-deps
40
- [props.getOptionValue, unsetLabel]);
41
- const getOptionMenuLabel = (0, react_1.useCallback)((o) => props.getOptionMenuLabel
42
- ? props.getOptionMenuLabel(o, Boolean(unsetLabel) && o === exports.unsetOption)
43
- : getOptionLabel(o),
44
- // 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
45
- // eslint-disable-next-line react-hooks/exhaustive-deps
46
- [props.getOptionValue, unsetLabel, getOptionLabel]);
33
+ const getOptionLabel = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? unsetLabel : propOptionLabel(o)), [propOptionLabel, unsetLabel]);
34
+ const getOptionValue = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? undefined : propOptionValue(o)), [propOptionValue, unsetLabel]);
35
+ const getOptionMenuLabel = (0, react_1.useCallback)((o) => propOptionMenuLabel ? propOptionMenuLabel(o, Boolean(unsetLabel) && o === exports.unsetOption) : getOptionLabel(o), [propOptionMenuLabel, unsetLabel, getOptionLabel]);
47
36
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
48
37
  const isDisabled = !!disabled;
49
38
  const isReadOnly = !!readOnly;
50
39
  const [fieldState, setFieldState] = (0, react_1.useState)(() => {
51
40
  var _a;
52
- const initOptions = Array.isArray(maybeOptions) ? maybeOptions : maybeOptions.initial;
41
+ const initOptions = Array.isArray(maybeOptions) ? maybeOptions : asArray(maybeOptions.current);
53
42
  const selectedOptions = initOptions.filter((o) => values.includes(getOptionValue(o)));
54
43
  return {
55
44
  selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
@@ -125,15 +114,8 @@ function ComboBoxBase(props) {
125
114
  async function maybeInitLoad() {
126
115
  if (!Array.isArray(maybeOptions)) {
127
116
  setFieldState((prevState) => ({ ...prevState, optionsLoading: true }));
128
- const loadedOptions = (await maybeOptions.load()).options;
129
- // Ensure the `unset` option is prepended to the top of the list if `unsetLabel` was provided
130
- const options = !unsetLabel ? loadedOptions : getOptionsWithUnset(unsetLabel, loadedOptions);
131
- setFieldState((prevState) => ({
132
- ...prevState,
133
- filteredOptions: options,
134
- allOptions: options,
135
- optionsLoading: false,
136
- }));
117
+ await maybeOptions.load();
118
+ setFieldState((prevState) => ({ ...prevState, optionsLoading: false }));
137
119
  }
138
120
  }
139
121
  const firstOpen = (0, react_1.useRef)(true);
@@ -216,19 +198,22 @@ function ComboBoxBase(props) {
216
198
  // 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
217
199
  // eslint-disable-next-line react-hooks/exhaustive-deps
218
200
  [values]);
201
+ // When options are an array, then use them as-is.
202
+ // If options are an object, then use the `initial` array if the menu has not been opened
203
+ // Otherwise, use the current fieldState array options.
204
+ const maybeUpdatedOptions = Array.isArray(maybeOptions)
205
+ ? maybeOptions
206
+ : firstOpen.current === false && !fieldState.optionsLoading
207
+ ? maybeOptions.options
208
+ : maybeOptions.current;
219
209
  (0, react_1.useEffect)(() => {
220
- // When options are an array, then use them as-is.
221
- // If options are an object, then use the `initial` array if the menu has not been opened
222
- // Otherwise, use the current fieldState array options.
223
- const maybeUpdatedOptions = Array.isArray(maybeOptions)
224
- ? maybeOptions
225
- : firstOpen.current === false
226
- ? fieldState.allOptions
227
- : maybeOptions.initial;
228
- if (maybeUpdatedOptions !== fieldState.allOptions) {
210
+ // We leave `maybeOptions.initial` as a non-array so that it's stable, but now that we're inside the
211
+ // useEffect, array-ize it if needed.
212
+ const maybeUpdatedArray = asArray(maybeUpdatedOptions);
213
+ if (maybeUpdatedArray !== fieldState.allOptions) {
229
214
  setFieldState((prevState) => {
230
215
  var _a;
231
- const selectedOptions = maybeUpdatedOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
216
+ const selectedOptions = maybeUpdatedArray.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
232
217
  return {
233
218
  ...prevState,
234
219
  selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
@@ -238,15 +223,16 @@ function ComboBoxBase(props) {
238
223
  ? nothingSelectedText
239
224
  : "",
240
225
  selectedOptions: selectedOptions,
241
- filteredOptions: maybeUpdatedOptions,
242
- allOptions: maybeUpdatedOptions,
226
+ filteredOptions: maybeUpdatedArray,
227
+ allOptions: maybeUpdatedArray,
243
228
  };
244
229
  });
245
230
  }
246
231
  },
247
- // 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
232
+ // I started working on fixing this deps array, but seems like `getOptionLabel` & friends
233
+ // would very rarely be stable anyway, so going to hold off on further fixes for now...
248
234
  // eslint-disable-next-line react-hooks/exhaustive-deps
249
- [maybeOptions]);
235
+ [maybeUpdatedOptions, getOptionLabel, getOptionValue]);
250
236
  // For the most part, the returned props contain `aria-*` and `id` attributes for accessibility purposes.
251
237
  const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
252
238
  ...comboBoxProps,
@@ -290,12 +276,13 @@ function initializeOptions(options, unsetLabel) {
290
276
  if (Array.isArray(options)) {
291
277
  return getOptionsWithUnset(unsetLabel, options);
292
278
  }
293
- return { ...options, initial: getOptionsWithUnset(unsetLabel, options.initial) };
279
+ return { ...options, options: getOptionsWithUnset(unsetLabel, options.options) };
294
280
  }
295
281
  exports.initializeOptions = initializeOptions;
296
282
  function getOptionsWithUnset(unsetLabel, options) {
297
- return [exports.unsetOption, ...options];
283
+ return [exports.unsetOption, ...(options ? options : [])];
298
284
  }
285
+ /** A marker option to automatically add an "Unset" option to the start of options. */
299
286
  exports.unsetOption = {};
300
287
  function disabledOptionToKeyedTuple(disabledOption) {
301
288
  if (typeof disabledOption === "object" && disabledOption !== null) {
@@ -306,3 +293,6 @@ function disabledOptionToKeyedTuple(disabledOption) {
306
293
  }
307
294
  }
308
295
  exports.disabledOptionToKeyedTuple = disabledOptionToKeyedTuple;
296
+ function asArray(arrayOrElement) {
297
+ return Array.isArray(arrayOrElement) ? arrayOrElement : arrayOrElement ? [arrayOrElement] : [];
298
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.319.1",
3
+ "version": "2.320.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",