@homebound/beam 2.320.0 → 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.
@@ -72,7 +72,8 @@ export type OptionsOrLoad<O> = O[] | {
72
72
  /** The full list of options, after load() has been fired. */
73
73
  options: O[] | undefined;
74
74
  };
75
- 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[];
76
77
  /** A marker option to automatically add an "Unset" option to the start of options. */
77
78
  export declare const unsetOption: {};
78
79
  export declare function disabledOptionToKeyedTuple(disabledOption: Value | {
@@ -25,27 +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, getOptionLabel: propOptionLabel, getOptionValue: propOptionValue, getOptionMenuLabel: propOptionMenuLabel, ...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
31
  const getOptionLabel = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? unsetLabel : propOptionLabel(o)), [propOptionLabel, unsetLabel]);
34
32
  const getOptionValue = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? undefined : propOptionValue(o)), [propOptionValue, unsetLabel]);
35
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
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ Array.isArray(propOptions) ? [propOptions, unsetLabel] : [propOptions.current, propOptions.options, unsetLabel]);
36
40
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
37
41
  const isDisabled = !!disabled;
38
42
  const isReadOnly = !!readOnly;
43
+ // Do a one-time initialize of fieldState
39
44
  const [fieldState, setFieldState] = (0, react_1.useState)(() => {
40
45
  var _a;
41
- const initOptions = Array.isArray(maybeOptions) ? maybeOptions : asArray(maybeOptions.current);
42
- const selectedOptions = initOptions.filter((o) => values.includes(getOptionValue(o)));
46
+ const selectedOptions = options.filter((o) => values.includes(getOptionValue(o)));
43
47
  return {
44
48
  selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
45
- inputValue: getInputValue(initOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText),
46
- filteredOptions: initOptions,
47
- allOptions: initOptions,
48
- 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,
49
53
  optionsLoading: false,
50
54
  };
51
55
  });
@@ -112,9 +116,9 @@ function ComboBoxBase(props) {
112
116
  }
113
117
  }
114
118
  async function maybeInitLoad() {
115
- if (!Array.isArray(maybeOptions)) {
119
+ if (!Array.isArray(propOptions)) {
116
120
  setFieldState((prevState) => ({ ...prevState, optionsLoading: true }));
117
- await maybeOptions.load();
121
+ await propOptions.load();
118
122
  setFieldState((prevState) => ({ ...prevState, optionsLoading: false }));
119
123
  }
120
124
  }
@@ -198,41 +202,29 @@ function ComboBoxBase(props) {
198
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
199
203
  // eslint-disable-next-line react-hooks/exhaustive-deps
200
204
  [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;
205
+ // Re-sync fieldState.allOptions
209
206
  (0, react_1.useEffect)(() => {
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) {
214
- setFieldState((prevState) => {
215
- var _a;
216
- const selectedOptions = maybeUpdatedArray.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
217
- return {
218
- ...prevState,
219
- selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
220
- inputValue: selectedOptions.length === 1
221
- ? getOptionLabel(selectedOptions[0])
222
- : multiselect && selectedOptions.length === 0
223
- ? nothingSelectedText
224
- : "",
225
- selectedOptions: selectedOptions,
226
- filteredOptions: maybeUpdatedArray,
227
- allOptions: maybeUpdatedArray,
228
- };
229
- });
230
- }
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
+ });
231
223
  },
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...
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
234
226
  // eslint-disable-next-line react-hooks/exhaustive-deps
235
- [maybeUpdatedOptions, getOptionLabel, getOptionValue]);
227
+ [options]);
236
228
  // For the most part, the returned props contain `aria-*` and `id` attributes for accessibility purposes.
237
229
  const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
238
230
  ...comboBoxProps,
@@ -269,19 +261,32 @@ function getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSele
269
261
  ? nothingSelectedText
270
262
  : "";
271
263
  }
272
- function initializeOptions(options, unsetLabel) {
273
- if (!unsetLabel) {
274
- 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);
275
269
  }
276
- if (Array.isArray(options)) {
277
- return getOptionsWithUnset(unsetLabel, options);
270
+ if (Array.isArray(optionsOrLoad)) {
271
+ opts.push(...optionsOrLoad);
278
272
  }
279
- return { ...options, options: getOptionsWithUnset(unsetLabel, options.options) };
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;
280
288
  }
281
289
  exports.initializeOptions = initializeOptions;
282
- function getOptionsWithUnset(unsetLabel, options) {
283
- return [exports.unsetOption, ...(options ? options : [])];
284
- }
285
290
  /** A marker option to automatically add an "Unset" option to the start of options. */
286
291
  exports.unsetOption = {};
287
292
  function disabledOptionToKeyedTuple(disabledOption) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.320.0",
3
+ "version": "2.320.1",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",