@homebound/beam 2.319.1 → 2.320.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.
@@ -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,18 @@ 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
- export declare function initializeOptions<O>(options: OptionsOrLoad<O>, unsetLabel: string | undefined): OptionsOrLoad<O>;
75
+ /** Transforms/simplifies `optionsOrLoad` into just options, with unsetLabel maybe added. */
76
+ export declare function initializeOptions<O, V extends Value>(optionsOrLoad: OptionsOrLoad<O>, getOptionValue: (opt: O) => V, unsetLabel: string | undefined): O[];
77
+ /** A marker option to automatically add an "Unset" option to the start of options. */
72
78
  export declare const unsetOption: {};
73
79
  export declare function disabledOptionToKeyedTuple(disabledOption: Value | {
74
80
  value: Value;
@@ -25,38 +25,31 @@ 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: propOptions, 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
- // Call `initializeOptions` to prepend the `unset` option if the `unsetLabel` was provided.
31
- const maybeOptions = (0, react_1.useMemo)(() => initializeOptions(options, unsetLabel), [options, unsetLabel]);
32
30
  // 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
31
+ const getOptionLabel = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? unsetLabel : propOptionLabel(o)), [propOptionLabel, unsetLabel]);
32
+ const getOptionValue = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? undefined : propOptionValue(o)), [propOptionValue, unsetLabel]);
33
+ const getOptionMenuLabel = (0, react_1.useCallback)((o) => propOptionMenuLabel ? propOptionMenuLabel(o, Boolean(unsetLabel) && o === exports.unsetOption) : getOptionLabel(o), [propOptionMenuLabel, unsetLabel, getOptionLabel]);
34
+ // Call `initializeOptions` to prepend the `unset` option if the `unsetLabel` was provided.
35
+ const options = (0, react_1.useMemo)(() => initializeOptions(propOptions, getOptionValue, unsetLabel),
36
+ // If the caller is using { current, load, options }, memoize on only `current` and `options` values.
37
+ // ...and don't bother on memoizing on getOptionValue b/c it's basically always a lambda
45
38
  // eslint-disable-next-line react-hooks/exhaustive-deps
46
- [props.getOptionValue, unsetLabel, getOptionLabel]);
39
+ Array.isArray(propOptions) ? [propOptions, unsetLabel] : [propOptions.current, propOptions.options, unsetLabel]);
47
40
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
48
41
  const isDisabled = !!disabled;
49
42
  const isReadOnly = !!readOnly;
43
+ // Do a one-time initialize of fieldState
50
44
  const [fieldState, setFieldState] = (0, react_1.useState)(() => {
51
45
  var _a;
52
- const initOptions = Array.isArray(maybeOptions) ? maybeOptions : maybeOptions.initial;
53
- const selectedOptions = initOptions.filter((o) => values.includes(getOptionValue(o)));
46
+ const selectedOptions = options.filter((o) => values.includes(getOptionValue(o)));
54
47
  return {
55
48
  selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
56
- inputValue: getInputValue(initOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText),
57
- filteredOptions: initOptions,
58
- allOptions: initOptions,
59
- selectedOptions: selectedOptions,
49
+ inputValue: getInputValue(options.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText),
50
+ filteredOptions: options,
51
+ allOptions: options,
52
+ selectedOptions,
60
53
  optionsLoading: false,
61
54
  };
62
55
  });
@@ -123,17 +116,10 @@ function ComboBoxBase(props) {
123
116
  }
124
117
  }
125
118
  async function maybeInitLoad() {
126
- if (!Array.isArray(maybeOptions)) {
119
+ if (!Array.isArray(propOptions)) {
127
120
  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
- }));
121
+ await propOptions.load();
122
+ setFieldState((prevState) => ({ ...prevState, optionsLoading: false }));
137
123
  }
138
124
  }
139
125
  const firstOpen = (0, react_1.useRef)(true);
@@ -216,37 +202,29 @@ function ComboBoxBase(props) {
216
202
  // 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
203
  // eslint-disable-next-line react-hooks/exhaustive-deps
218
204
  [values]);
205
+ // Re-sync fieldState.allOptions
219
206
  (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) {
229
- setFieldState((prevState) => {
230
- var _a;
231
- const selectedOptions = maybeUpdatedOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
232
- return {
233
- ...prevState,
234
- selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
235
- inputValue: selectedOptions.length === 1
236
- ? getOptionLabel(selectedOptions[0])
237
- : multiselect && selectedOptions.length === 0
238
- ? nothingSelectedText
239
- : "",
240
- selectedOptions: selectedOptions,
241
- filteredOptions: maybeUpdatedOptions,
242
- allOptions: maybeUpdatedOptions,
243
- };
244
- });
245
- }
207
+ setFieldState((prevState) => {
208
+ var _a;
209
+ const selectedOptions = options.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
210
+ return {
211
+ ...prevState,
212
+ selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
213
+ inputValue: selectedOptions.length === 1
214
+ ? getOptionLabel(selectedOptions[0])
215
+ : multiselect && selectedOptions.length === 0
216
+ ? nothingSelectedText
217
+ : "",
218
+ selectedOptions: selectedOptions,
219
+ filteredOptions: options,
220
+ allOptions: options,
221
+ };
222
+ });
246
223
  },
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
224
+ // We're primarily only re-setting `allOptions`, and so recalc selected as well, but we don't
225
+ // want to depend on values/etc., b/c we'll defer to their useEffects to update their state
248
226
  // eslint-disable-next-line react-hooks/exhaustive-deps
249
- [maybeOptions]);
227
+ [options]);
250
228
  // For the most part, the returned props contain `aria-*` and `id` attributes for accessibility purposes.
251
229
  const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
252
230
  ...comboBoxProps,
@@ -283,19 +261,33 @@ function getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSele
283
261
  ? nothingSelectedText
284
262
  : "";
285
263
  }
286
- function initializeOptions(options, unsetLabel) {
287
- if (!unsetLabel) {
288
- return options;
264
+ /** Transforms/simplifies `optionsOrLoad` into just options, with unsetLabel maybe added. */
265
+ function initializeOptions(optionsOrLoad, getOptionValue, unsetLabel) {
266
+ const opts = [];
267
+ if (unsetLabel) {
268
+ opts.push(exports.unsetOption);
289
269
  }
290
- if (Array.isArray(options)) {
291
- return getOptionsWithUnset(unsetLabel, options);
270
+ if (Array.isArray(optionsOrLoad)) {
271
+ opts.push(...optionsOrLoad);
292
272
  }
293
- return { ...options, initial: getOptionsWithUnset(unsetLabel, options.initial) };
273
+ else {
274
+ const { options, current } = optionsOrLoad;
275
+ if (options) {
276
+ opts.push(...options);
277
+ }
278
+ // Even if the SelectField has lazy-loaded options, make sure the current value is really in there
279
+ if (current) {
280
+ const value = getOptionValue(current);
281
+ const found = options && options.find((o) => getOptionValue(o) === value);
282
+ if (!found) {
283
+ opts.push(current);
284
+ }
285
+ }
286
+ }
287
+ return opts;
294
288
  }
295
289
  exports.initializeOptions = initializeOptions;
296
- function getOptionsWithUnset(unsetLabel, options) {
297
- return [exports.unsetOption, ...options];
298
- }
290
+ /** A marker option to automatically add an "Unset" option to the start of options. */
299
291
  exports.unsetOption = {};
300
292
  function disabledOptionToKeyedTuple(disabledOption) {
301
293
  if (typeof disabledOption === "object" && disabledOption !== null) {
@@ -306,3 +298,6 @@ function disabledOptionToKeyedTuple(disabledOption) {
306
298
  }
307
299
  }
308
300
  exports.disabledOptionToKeyedTuple = disabledOptionToKeyedTuple;
301
+ function asArray(arrayOrElement) {
302
+ return Array.isArray(arrayOrElement) ? arrayOrElement : arrayOrElement ? [arrayOrElement] : [];
303
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.319.1",
3
+ "version": "2.320.1",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",