@homebound/beam 2.96.3 → 2.97.3

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.
@@ -16,8 +16,35 @@ exports.CssReset = CssReset;
16
16
  // The is primarily for navigation, like breadcrumb links or tab links.
17
17
  exports.navLink = "navLink";
18
18
  const ourReset = (0, react_1.css) `
19
- a:not(.${exports.navLink}) { color: ${Css_1.Palette.LightBlue700} },
20
- a:visited:not(.${exports.navLink}) { color: ${Css_1.Palette.LightBlue500} },
19
+ a:not(.${exports.navLink}) {
20
+ color: ${Css_1.Palette.LightBlue700};
21
+ }
22
+
23
+ a:visited:not(.${exports.navLink}) {
24
+ color: ${Css_1.Palette.LightBlue500};
25
+ }
26
+
27
+ /**
28
+ * Beam animations
29
+ */
30
+ @keyframes loadingDots {
31
+ 0% {
32
+ background-color: ${Css_1.Palette.Gray600};
33
+ }
34
+ 50%,
35
+ 100% {
36
+ background-color: ${Css_1.Palette.Gray300};
37
+ }
38
+ }
39
+ @keyframes loadingDotsContrast {
40
+ 0% {
41
+ background-color: ${Css_1.Palette.Gray200};
42
+ }
43
+ 50%,
44
+ 100% {
45
+ background-color: ${Css_1.Palette.Gray500};
46
+ }
47
+ }
21
48
  `;
22
49
  // Copy/pasted from TW which uses this as their base reset.
23
50
  const modernNormalizeReset = (0, react_1.css) `
@@ -12,11 +12,10 @@ exports.singleFilter = singleFilter;
12
12
  const allOption = {};
13
13
  class SingleFilter extends BaseFilter_1.BaseFilter {
14
14
  render(value, setValue, tid, inModal, vertical) {
15
- const { label, defaultValue, options, getOptionLabel, getOptionValue, ...props } = this.props;
16
- return ((0, jsx_runtime_1.jsx)(SelectField_1.SelectField, Object.assign({}, props, { options: [
17
- // We always add "All" as the 1st option, to allow unselecting with a click
18
- allOption,
19
- ...options,
20
- ], getOptionValue: (o) => (o === allOption ? undefined : getOptionValue(o)), getOptionLabel: (o) => (o === allOption ? "All" : getOptionLabel(o)), compact: !vertical, value: value, label: this.label, inlineLabel: !inModal && !vertical, hideLabel: inModal, sizeToContent: !inModal && !vertical, nothingSelectedText: "All", onSelect: (value) => setValue(value || undefined) }, this.testId(tid)), void 0));
15
+ const { label, defaultValue, options: maybeOptions, getOptionLabel, getOptionValue, ...props } = this.props;
16
+ const options = Array.isArray(maybeOptions)
17
+ ? [allOption, ...maybeOptions]
18
+ : { ...maybeOptions, initial: [allOption, ...maybeOptions.initial] };
19
+ return ((0, jsx_runtime_1.jsx)(SelectField_1.SelectField, Object.assign({}, props, { options: options, getOptionValue: (o) => (o === allOption ? undefined : getOptionValue(o)), getOptionLabel: (o) => (o === allOption ? "All" : getOptionLabel(o)), compact: !vertical, value: value, label: this.label, inlineLabel: !inModal && !vertical, hideLabel: inModal, sizeToContent: !inModal && !vertical, nothingSelectedText: "All", onSelect: (value) => setValue(value || undefined) }, this.testId(tid)), void 0));
21
20
  }
22
21
  }
@@ -1,4 +1,5 @@
1
1
  import { ReactNode } from "react";
2
+ import { Modifier } from "react-day-picker";
2
3
  import { TextFieldBaseProps } from "./TextFieldBase";
3
4
  import "./DateField.css";
4
5
  export interface DateFieldProps extends Pick<TextFieldBaseProps<{}>, "borderless" | "visuallyDisabled" | "hideLabel" | "compact"> {
@@ -19,6 +20,11 @@ export interface DateFieldProps extends Pick<TextFieldBaseProps<{}>, "borderless
19
20
  placeholder?: string;
20
21
  format?: keyof typeof dateFormats;
21
22
  iconLeft?: boolean;
23
+ /**
24
+ * Set custom logic for individual dates or date ranges to be disabled in the picker
25
+ * exposed from `react-day-picker`: https://react-day-picker.js.org/api/DayPicker#modifiers
26
+ */
27
+ disabledDays?: Modifier;
22
28
  }
23
29
  export declare function DateField(props: DateFieldProps): import("@emotion/react/jsx-runtime").JSX.Element;
24
30
  declare const dateFormats: {
@@ -16,7 +16,7 @@ const utils_1 = require("../utils");
16
16
  const defaultTestId_1 = require("../utils/defaultTestId");
17
17
  require("./DateField.css");
18
18
  function DateField(props) {
19
- const { label, disabled, required, value, onChange, onFocus, onBlur, errorMsg, helperText, inlineLabel = false, readOnly = false, format = "short", iconLeft = false, ...others } = props;
19
+ const { label, disabled, required, value, onChange, onFocus, onBlur, errorMsg, helperText, inlineLabel = false, readOnly = false, format = "short", iconLeft = false, disabledDays, ...others } = props;
20
20
  const inputRef = (0, react_1.useRef)(null);
21
21
  const inputWrapRef = (0, react_1.useRef)(null);
22
22
  const buttonRef = (0, react_1.useRef)(null);
@@ -123,7 +123,7 @@ function DateField(props) {
123
123
  }, endAdornment: !iconLeft && calendarButton, startAdornment: iconLeft && calendarButton, tooltip: isDisabled && typeof disabled !== "boolean" ? disabled : undefined }, others), void 0), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, Object.assign({ triggerRef: inputWrapRef, popoverRef: overlayRef, positionProps: { ...overlayProps, ...positionProps }, onClose: state.close, isOpen: state.isOpen }, { children: (0, jsx_runtime_1.jsx)(DatePickerOverlay_1.DatePickerOverlay, Object.assign({ state: state, value: value, positionProps: positionProps, onChange: (d) => {
124
124
  setInputValue(formatDate(d, dateFormat));
125
125
  onChange(d);
126
- } }, tid.datePicker), void 0) }), void 0))] }, void 0));
126
+ }, disabledDays: disabledDays }, tid.datePicker), void 0) }), void 0))] }, void 0));
127
127
  }
128
128
  exports.DateField = DateField;
129
129
  function formatDate(date, format) {
@@ -14,6 +14,6 @@ function DateField(props) {
14
14
  const { value } = e.target;
15
15
  setValue(value);
16
16
  onChange((0, date_fns_1.parse)(value, "MM/dd/yy", new Date()));
17
- }, onBlur: () => (0, utils_1.maybeCall)(onBlur), onFocus: () => (0, utils_1.maybeCall)(onFocus), disabled: !!props.disabled, readOnly: props.readOnly }), void 0));
17
+ }, onBlur: () => (0, utils_1.maybeCall)(onBlur), onFocus: () => (0, utils_1.maybeCall)(onFocus), disabled: !!props.disabled, readOnly: props.readOnly, "data-disabled-days": JSON.stringify(props.disabledDays) }), void 0));
18
18
  }
19
19
  exports.DateField = DateField;
@@ -1,16 +1,10 @@
1
- import { ReactNode } from "react";
2
1
  import { Value } from "./";
3
2
  import { BeamSelectFieldBaseProps } from "./internal/SelectFieldBase";
4
3
  import { HasIdAndName, Optional } from "../types";
5
- export interface SelectFieldProps<O, V extends Value> extends BeamSelectFieldBaseProps<O, V> {
6
- /** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. */
7
- getOptionMenuLabel?: (opt: O) => string | ReactNode;
8
- getOptionValue: (opt: O) => V;
9
- getOptionLabel: (opt: O) => string;
4
+ export interface SelectFieldProps<O, V extends Value> extends Omit<BeamSelectFieldBaseProps<O, V>, "values" | "onSelect"> {
10
5
  /** The current value; it can be `undefined`, even if `V` cannot be. */
11
6
  value: V | undefined;
12
7
  onSelect: (value: V, opt: O) => void;
13
- options: O[];
14
8
  }
15
9
  /**
16
10
  * Provides a non-native select/dropdown widget.
@@ -7,10 +7,9 @@ function SelectField(props) {
7
7
  const { getOptionValue = (opt) => opt.id, // if unset, assume O implements HasId
8
8
  getOptionLabel = (opt) => opt.name, // if unset, assume O implements HasName
9
9
  options, onSelect, value, ...otherProps } = props;
10
- return ((0, jsx_runtime_1.jsx)(SelectFieldBase_1.SelectFieldBase, Object.assign({}, otherProps, { options: options, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, values: [value], onSelect: (values) => {
11
- if (values.length > 0) {
12
- const selectedOption = options.find((o) => getOptionValue(o) === values[0]);
13
- onSelect && selectedOption && onSelect(getOptionValue(selectedOption), selectedOption);
10
+ return ((0, jsx_runtime_1.jsx)(SelectFieldBase_1.SelectFieldBase, Object.assign({}, otherProps, { options: options, getOptionLabel: getOptionLabel, getOptionValue: getOptionValue, values: [value], onSelect: (values, options) => {
11
+ if (values.length > 0 && options.length > 0) {
12
+ onSelect && onSelect(values[0], options[0]);
14
13
  }
15
14
  } }), void 0));
16
15
  }
@@ -1,4 +1,4 @@
1
1
  import { Key } from "react";
2
2
  import { SelectFieldProps } from "./";
3
3
  /** Mocks out `SelectField` as a `<select>` field. */
4
- export declare function SelectField<T extends object, V extends Key>(props: SelectFieldProps<T, V>): import("@emotion/react/jsx-runtime").JSX.Element;
4
+ export declare function SelectField<O extends object, V extends Key>(props: SelectFieldProps<O, V>): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -2,20 +2,31 @@
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 utils_1 = require("../utils");
6
7
  /** Mocks out `SelectField` as a `<select>` field. */
7
8
  function SelectField(props) {
8
9
  const { getOptionValue = (o) => o.id, // if unset, assume O implements HasId
9
10
  getOptionLabel = (o) => o.name, // if unset, assume O implements HasName
10
- value, options, onSelect, readOnly = false, errorMsg, onBlur, onFocus, disabled, disabledOptions = [], } = props;
11
+ value, options: maybeOptions, onSelect, readOnly = false, errorMsg, onBlur, onFocus, disabled, disabledOptions = [], } = props;
11
12
  const tid = (0, utils_1.useTestIds)(props, "select");
13
+ const [options, setOptions] = (0, react_1.useState)(Array.isArray(maybeOptions) ? maybeOptions : maybeOptions.initial);
12
14
  const currentOption = options.find((o) => getOptionValue(o) === value) || options[0];
15
+ (0, react_1.useEffect)(() => {
16
+ if (Array.isArray(maybeOptions) && maybeOptions !== options) {
17
+ setOptions(maybeOptions);
18
+ }
19
+ }, [maybeOptions]);
13
20
  return ((0, jsx_runtime_1.jsxs)("select", Object.assign({}, tid, { value:
14
21
  // @ts-ignore - allow `value` to be seen as a string
15
22
  value !== undefined && value !== "" && currentOption ? getOptionValue(currentOption) : "", onChange: (e) => {
16
23
  const option = options.find((o) => `${getOptionValue(o)}` === e.target.value) || options[0];
17
24
  onSelect(getOptionValue(option), option);
18
- }, onFocus: () => {
25
+ }, onFocus: async () => {
26
+ if (!Array.isArray(maybeOptions)) {
27
+ const result = await maybeOptions.load();
28
+ setOptions(result.options);
29
+ }
19
30
  if (!readOnly && onFocus)
20
31
  onFocus();
21
32
  }, onBlur: () => {
@@ -1,10 +1,12 @@
1
1
  import React from "react";
2
+ import { Modifier } from "react-day-picker";
2
3
  import { OverlayTriggerState } from "react-stately";
3
4
  interface DatePickerOverlayProps {
4
5
  value: Date | undefined;
5
6
  state: OverlayTriggerState;
6
7
  positionProps: React.HTMLAttributes<Element>;
7
8
  onChange: (value: Date) => void;
9
+ disabledDays: Modifier;
8
10
  }
9
11
  export declare function DatePickerOverlay(props: DatePickerOverlayProps): import("@emotion/react/jsx-runtime").JSX.Element;
10
12
  export {};
@@ -11,7 +11,7 @@ const Css_1 = require("../../Css");
11
11
  const utils_1 = require("../../utils");
12
12
  function DatePickerOverlay(props) {
13
13
  var _a;
14
- const { value, state, positionProps, onChange } = props;
14
+ const { value, state, positionProps, onChange, disabledDays } = props;
15
15
  // We define some spacing between the Calendar overlay and the trigger element, and depending on where the overlay renders (above or below) we need to adjust the spacing.
16
16
  // We can determine if the position was flipped based on what style is defined, `top` (for positioned below the trigger), and `bottom` (for above the trigger).
17
17
  // The above assumption regarding `top` and `bottom` is true as long as we use `bottom` as our default `OverlayPosition.placement` (set in DateField).
@@ -35,11 +35,17 @@ function DatePickerOverlay(props) {
35
35
  "& .DayPicker-Day:active": Css_1.Css.bgGray400.$,
36
36
  // Make the month title, i.e. "May 2021", match figma; pyPx nudge matches the NavbarElement nudging
37
37
  "& .DayPicker-Caption > div": Css_1.Css.base.pyPx(2).$,
38
- } }, tid, { children: (0, jsx_runtime_1.jsx)(react_day_picker_1.default, { navbarElement: NavbarElement, weekdayElement: Weekday, selectedDays: [value], initialMonth: value !== null && value !== void 0 ? value : new Date(), onDayClick: (day) => {
38
+ // For days that are disabled via `disabledDays`,
39
+ "& .DayPicker-Day--disabled": Css_1.Css.cursorNotAllowed.$,
40
+ // Override `.DayPicker-Day:active` background when the day is disabled
41
+ "& .DayPicker-Day--disabled:active": Css_1.Css.bgWhite.$,
42
+ } }, tid, { children: (0, jsx_runtime_1.jsx)(react_day_picker_1.default, { navbarElement: NavbarElement, weekdayElement: Weekday, selectedDays: [value], initialMonth: value !== null && value !== void 0 ? value : new Date(), onDayClick: (day, modifiers) => {
43
+ if (modifiers.disabled)
44
+ return;
39
45
  // Set the day value, and close the picker.
40
46
  onChange(day);
41
47
  state.close();
42
- } }, void 0) }), void 0));
48
+ }, disabledDays: disabledDays }, void 0) }), void 0));
43
49
  }
44
50
  exports.DatePickerOverlay = DatePickerOverlay;
45
51
  /** Customize the prev/next button to be our SVG icons. */
@@ -8,6 +8,7 @@ interface ListBoxProps<O, V extends Key> {
8
8
  getOptionValue: (opt: O) => V;
9
9
  contrast?: boolean;
10
10
  positionProps: React.HTMLAttributes<Element>;
11
+ loading?: boolean | (() => JSX.Element);
11
12
  }
12
13
  /** A ListBox is an internal component used by SelectField and MultiSelectField to display the list of options */
13
14
  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, } = props;
15
+ const { state, listBoxRef, selectedOptions = [], getOptionLabel, getOptionValue, contrast = false, positionProps, loading, } = 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`).
@@ -37,7 +37,7 @@ function ListBox(props) {
37
37
  // Only scroll on focus if using VirtualFocus (used for ComboBoxState (SelectField), but not SelectState (ChipSelectField))
38
38
  scrollOnFocus: props.shouldUseVirtualFocus }, section.key)))) : ((0, jsx_runtime_1.jsx)(VirtualizedOptions_1.VirtualizedOptions, { state: state, items: [...state.collection], onListHeightChange: onListHeightChange, contrast: contrast,
39
39
  // Only scroll on focus if using VirtualFocus (used for ComboBoxState (SelectField), but not SelectState (ChipSelectField))
40
- scrollOnFocus: props.shouldUseVirtualFocus }, void 0)) }), void 0)] }), void 0));
40
+ scrollOnFocus: props.shouldUseVirtualFocus, loading: loading }, void 0)) }), void 0)] }), void 0));
41
41
  }
42
42
  exports.ListBox = ListBox;
43
43
  // UX specified maximum height for a ListBox (in pixels)
@@ -0,0 +1,3 @@
1
+ export declare function LoadingDots({ contrast }: {
2
+ contrast: boolean;
3
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LoadingDots = void 0;
4
+ const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
+ const Css_1 = require("../../Css");
6
+ const utils_1 = require("../../utils");
7
+ function LoadingDots({ contrast }) {
8
+ const circleCss = Css_1.Css.hPx(8)
9
+ .wPx(8)
10
+ .br4.bgColor(contrast ? Css_1.Palette.Gray500 : Css_1.Palette.Gray300)
11
+ .add("animationName", contrast ? "loadingDotsContrast" : "loadingDots")
12
+ .add("animationDuration", "800ms")
13
+ .add("animationIterationCount", "infinite")
14
+ .add("animationTimingFunction", "linear")
15
+ .add("animationDirection", "alternate").$;
16
+ const tid = (0, utils_1.useTestIds)({});
17
+ return ((0, jsx_runtime_1.jsx)("div", Object.assign({ css: Css_1.Css.py2.df.jcc.$ }, tid.loadingDots, { children: (0, jsx_runtime_1.jsx)("div", { "aria-label": "Loading", css: {
18
+ ...circleCss,
19
+ ...Css_1.Css.relative
20
+ .add("animationDelay", "300ms")
21
+ .addIn("&:before, &:after", {
22
+ ...circleCss,
23
+ ...Css_1.Css.add("content", "' '").absolute.dib.$,
24
+ })
25
+ .addIn("&:before", Css_1.Css.leftPx(-12).add("animationDelay", "0").$)
26
+ .addIn("&:after", Css_1.Css.rightPx(-12).add("animationDelay", "600ms").$).$,
27
+ } }, void 0) }), void 0));
28
+ }
29
+ exports.LoadingDots = LoadingDots;
@@ -2,35 +2,29 @@ import { ReactNode } from "react";
2
2
  import { PresentationFieldProps } from "../../components/PresentationContext";
3
3
  import { Value } from "../Value";
4
4
  import { BeamFocusableProps } from "../../interfaces";
5
- export interface SelectFieldBaseProps<O, V extends Value> extends BeamSelectFieldBaseProps<O, V> {
5
+ export interface BeamSelectFieldBaseProps<O, V extends Value> extends BeamFocusableProps, PresentationFieldProps {
6
6
  /** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. */
7
7
  getOptionMenuLabel?: (opt: O) => string | ReactNode;
8
8
  getOptionValue: (opt: O) => V;
9
9
  getOptionLabel: (opt: O) => string;
10
10
  /** The current value; it can be `undefined`, even if `V` cannot be. */
11
11
  values: V[] | undefined;
12
- onSelect: (values: V[]) => void;
13
- options: O[];
12
+ onSelect: (values: V[], opts: O[]) => void;
14
13
  multiselect?: boolean;
15
- }
16
- /**
17
- * Provides a non-native select/dropdown widget.
18
- *
19
- * The `O` type is a list of options to show, the `V` is the primitive value of a
20
- * given `O` (i.e. it's id) that you want to use as the current/selected value.
21
- *
22
- * Note that the `V extends Key` constraint come from react-aria,
23
- * and so we cannot easily change them.
24
- */
25
- export declare function SelectFieldBase<O, V extends Value>(props: SelectFieldBaseProps<O, V>): JSX.Element;
26
- export interface BeamSelectFieldBaseProps<T, V extends Value> extends BeamFocusableProps, PresentationFieldProps {
27
14
  disabledOptions?: V[];
15
+ options: O[] | {
16
+ initial: O[];
17
+ load: () => Promise<{
18
+ options: O[];
19
+ }>;
20
+ };
21
+ /** Whether the field is disabled. If a ReactNode, it's treated as a "disabled reason" that's shown in a tooltip. */
28
22
  disabled?: boolean | ReactNode;
29
23
  required?: boolean;
30
24
  errorMsg?: string;
31
25
  helperText?: string | ReactNode;
32
26
  /** Allow placing an icon/decoration within the input field. */
33
- fieldDecoration?: (opt: T) => ReactNode;
27
+ fieldDecoration?: (opt: O) => ReactNode;
34
28
  /** Sets the form field label. */
35
29
  label: string;
36
30
  /** Renders the label inside the input field, i.e. for filters. */
@@ -46,3 +40,13 @@ export interface BeamSelectFieldBaseProps<T, V extends Value> extends BeamFocusa
46
40
  /** Placeholder content */
47
41
  placeholder?: string;
48
42
  }
43
+ /**
44
+ * Provides a non-native select/dropdown widget.
45
+ *
46
+ * The `O` type is a list of options to show, the `V` is the primitive value of a
47
+ * given `O` (i.e. it's id) that you want to use as the current/selected value.
48
+ *
49
+ * Note that the `V extends Key` constraint come from react-aria,
50
+ * and so we cannot easily change them.
51
+ */
52
+ export declare function SelectFieldBase<O, V extends Value>(props: BeamSelectFieldBaseProps<O, V>): JSX.Element;
@@ -11,6 +11,7 @@ const Css_1 = require("../../Css");
11
11
  const ListBox_1 = require("./ListBox");
12
12
  const SelectFieldInput_1 = require("./SelectFieldInput");
13
13
  const Value_1 = require("../Value");
14
+ const utils_1 = require("../../utils");
14
15
  /**
15
16
  * Provides a non-native select/dropdown widget.
16
17
  *
@@ -22,16 +23,40 @@ const Value_1 = require("../Value");
22
23
  */
23
24
  function SelectFieldBase(props) {
24
25
  var _a;
25
- const { compact, disabled, errorMsg, helperText, label, hideLabel, required, inlineLabel, readOnly, onSelect, fieldDecoration, options, onBlur, onFocus, multiselect = false, getOptionLabel, getOptionValue, getOptionMenuLabel = getOptionLabel, sizeToContent = false, values, nothingSelectedText = "", contrast, disabledOptions, borderless, ...otherProps } = props;
26
+ const { disabled, readOnly, onSelect, options: maybeOptions, multiselect = false, getOptionLabel, getOptionValue, getOptionMenuLabel = getOptionLabel, values = [], nothingSelectedText = "", contrast, disabledOptions, borderless, ...otherProps } = props;
26
27
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
27
28
  const isDisabled = !!disabled;
28
29
  const isReadOnly = !!readOnly;
29
- function onSelectionChange(keys) {
30
+ const [fieldState, setFieldState] = (0, react_1.useState)(() => {
30
31
  var _a;
31
- // Close menu upon selection change only for Single selection mode
32
- if (!multiselect) {
33
- state.close();
32
+ const initOptions = Array.isArray(maybeOptions) ? maybeOptions : maybeOptions.initial;
33
+ const selectedOptions = initOptions.filter((o) => values.includes(getOptionValue(o)));
34
+ return {
35
+ isOpen: false,
36
+ selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
37
+ inputValue: getInputValue(initOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText),
38
+ filteredOptions: initOptions,
39
+ allOptions: initOptions,
40
+ selectedOptions: selectedOptions,
41
+ optionsLoading: false,
42
+ };
43
+ });
44
+ /** Resets field's input value and filtered options list for cases where the user exits the field without making changes (on Escape, or onBlur) */
45
+ function resetField() {
46
+ const inputValue = getInputValue(fieldState.allOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText);
47
+ // Conditionally reset the value if the current inputValue doesn't match that of the passed in value, or we filtered the list
48
+ if (inputValue !== fieldState.inputValue || fieldState.filteredOptions.length !== fieldState.allOptions.length) {
49
+ setFieldState((prevState) => ({
50
+ ...prevState,
51
+ isOpen: false,
52
+ inputValue,
53
+ filteredOptions: prevState.allOptions,
54
+ }));
34
55
  }
56
+ }
57
+ function onSelectionChange(keys) {
58
+ var _a;
59
+ // We don't currently handle the "all" case
35
60
  if (keys === "all") {
36
61
  return;
37
62
  }
@@ -41,7 +66,6 @@ function SelectFieldBase(props) {
41
66
  const selectionChanged = !(keys.size === state.selectionManager.selectedKeys.size &&
42
67
  [...keys].every((value) => state.selectionManager.selectedKeys.has(value)));
43
68
  if (multiselect && keys.size === 0) {
44
- // "All" happens if we selected everything or nothing.
45
69
  setFieldState({
46
70
  ...fieldState,
47
71
  isOpen: true,
@@ -49,70 +73,73 @@ function SelectFieldBase(props) {
49
73
  selectedKeys: [],
50
74
  selectedOptions: [],
51
75
  });
52
- selectionChanged && onSelect([]);
76
+ selectionChanged && onSelect([], []);
53
77
  return;
54
78
  }
55
- const keysArray = [...keys.values()];
56
- const firstKey = keysArray[0];
57
- // Even though the key is number|string, this will always be a string
58
- const firstSelectedOption = options.find((o) => (0, Value_1.valueToKey)(getOptionValue(o)) === firstKey);
59
- if (multiselect) {
60
- setFieldState({
61
- ...fieldState,
62
- // If menu is open then reset inputValue to "". Otherwise set inputValue depending on number of options selected.
63
- inputValue: state.isOpen ? "" : keysArray.length === 1 ? getOptionLabel(firstSelectedOption) : "",
64
- selectedKeys: keysArray,
65
- selectedOptions: options.filter((o) => keysArray.includes((0, Value_1.valueToKey)(getOptionValue(o)))),
66
- filteredOptions: options,
67
- });
68
- }
69
- else {
70
- setFieldState({
71
- ...fieldState,
72
- isOpen: false,
73
- inputValue: firstSelectedOption ? getOptionLabel(firstSelectedOption) : "",
74
- selectedKeys: [firstKey],
75
- selectedOptions: firstSelectedOption ? [firstSelectedOption] : [],
76
- });
77
- }
78
- selectionChanged && onSelect([...keys.values()].map(Value_1.keyToValue));
79
+ const selectedKeys = [...keys.values()];
80
+ const selectedOptions = fieldState.allOptions.filter((o) => selectedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o))));
81
+ const firstSelectedOption = selectedOptions[0];
82
+ setFieldState((prevState) => ({
83
+ ...prevState,
84
+ // Close menu upon selection change only for Single selection mode
85
+ isOpen: multiselect,
86
+ // If menu is open then reset inputValue to "". Otherwise set inputValue depending on number of options selected.
87
+ inputValue: multiselect && (state.isOpen || selectedKeys.length > 1)
88
+ ? ""
89
+ : firstSelectedOption
90
+ ? getOptionLabel(firstSelectedOption)
91
+ : "",
92
+ selectedKeys,
93
+ selectedOptions,
94
+ filteredOptions: fieldState.allOptions,
95
+ }));
96
+ selectionChanged && onSelect(selectedKeys.map(Value_1.keyToValue), selectedOptions);
79
97
  if (!multiselect) {
80
98
  // When a single select menu item changes, then blur the field AFTER `onSelect` has been called
81
99
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur();
82
100
  }
83
101
  }
84
102
  function onInputChange(value) {
85
- setFieldState((prevState) => ({
86
- ...prevState,
87
- isOpen: true,
88
- inputValue: value,
89
- filteredOptions: options.filter((o) => contains(getOptionLabel(o), value)),
90
- }));
103
+ if (value !== fieldState.inputValue) {
104
+ setFieldState((prevState) => ({
105
+ ...prevState,
106
+ inputValue: value,
107
+ filteredOptions: fieldState.allOptions.filter((o) => contains(getOptionLabel(o), value)),
108
+ }));
109
+ }
110
+ }
111
+ async function maybeInitLoad() {
112
+ if (!Array.isArray(maybeOptions)) {
113
+ setFieldState((prevState) => ({ ...prevState, optionsLoading: true }));
114
+ const { options } = await maybeOptions.load();
115
+ setFieldState((prevState) => ({
116
+ ...prevState,
117
+ filteredOptions: options,
118
+ allOptions: options,
119
+ optionsLoading: false,
120
+ }));
121
+ }
91
122
  }
123
+ const firstOpen = (0, react_1.useRef)(true);
92
124
  function onOpenChange(isOpen) {
125
+ if (firstOpen.current && isOpen) {
126
+ maybeInitLoad();
127
+ firstOpen.current = false;
128
+ }
93
129
  setFieldState((prevState) => ({
94
130
  ...prevState,
131
+ // When using the multiselect field, always empty the input upon open.
95
132
  inputValue: multiselect && isOpen ? "" : prevState.inputValue,
96
133
  isOpen,
97
134
  }));
98
135
  }
99
- function initFieldState() {
100
- // Use the current value to find the option
101
- const selectedKeys = values !== null && values !== void 0 ? values : [];
102
- const selectedOptions = options.filter((o) => selectedKeys.includes(getOptionValue(o)));
103
- return {
104
- isOpen: false,
105
- selectedKeys: selectedKeys.map(Value_1.valueToKey),
106
- inputValue: selectedOptions.length === 1
107
- ? getOptionLabel(selectedOptions[0])
108
- : multiselect && selectedOptions.length === 0
109
- ? nothingSelectedText
110
- : "",
111
- filteredOptions: options,
112
- selectedOptions: selectedOptions,
113
- };
114
- }
115
- const [fieldState, setFieldState] = (0, react_1.useState)(initFieldState);
136
+ // Used to calculate the rendered width of the combo box (input + button)
137
+ const comboBoxRef = (0, react_1.useRef)(null);
138
+ const triggerRef = (0, react_1.useRef)(null);
139
+ const inputRef = (0, react_1.useRef)(null);
140
+ const inputWrapRef = (0, react_1.useRef)(null);
141
+ const listBoxRef = (0, react_1.useRef)(null);
142
+ const popoverRef = (0, react_1.useRef)(null);
116
143
  const comboBoxProps = {
117
144
  ...otherProps,
118
145
  disabledKeys: disabledOptions === null || disabledOptions === void 0 ? void 0 : disabledOptions.map(Value_1.valueToKey),
@@ -120,7 +147,6 @@ function SelectFieldBase(props) {
120
147
  items: fieldState.filteredOptions,
121
148
  isDisabled,
122
149
  isReadOnly,
123
- label,
124
150
  onInputChange,
125
151
  onOpenChange,
126
152
  menuTrigger: "focus",
@@ -128,40 +154,68 @@ function SelectFieldBase(props) {
128
154
  };
129
155
  const state = (0, react_stately_1.useComboBoxState)({
130
156
  ...comboBoxProps,
157
+ allowsEmptyCollection: true,
131
158
  // useComboBoxState.onSelectionChange will be executed if a keyboard interaction (Enter key) is used to select an item
132
159
  onSelectionChange: (key) => {
133
160
  // ignore undefined/null keys - `null` can happen if input field's value is completely deleted after having a value assigned.
134
161
  if (key) {
135
- const selectedKeys = multipleSelectionState.selectedKeys;
162
+ const selectedKeys = state.selectionManager.selectedKeys;
136
163
  // Create the `newSelection` Set depending on the value type of SelectField.
137
164
  const newSelection = new Set(!multiselect ? [key] : [...selectedKeys, key]);
138
165
  // Use only the `multipleSelectionState` to manage selected keys
139
- multipleSelectionState.setSelectedKeys(newSelection);
166
+ state.selectionManager.setSelectedKeys(newSelection);
140
167
  }
141
168
  },
142
169
  });
143
- const multipleSelectionState = (0, react_stately_1.useMultipleSelectionState)({
170
+ //@ts-ignore - `selectionManager.state` exists, but not according to the types
171
+ state.selectionManager.state = (0, react_stately_1.useMultipleSelectionState)({
144
172
  selectionMode: multiselect ? "multiple" : "single",
145
173
  // Do not allow an empty selection if single select mode
146
174
  disallowEmptySelection: !multiselect,
147
175
  selectedKeys: fieldState.selectedKeys,
148
176
  onSelectionChange,
149
177
  });
150
- //@ts-ignore - `selectionManager.state` exists, but not according to the types
151
- state.selectionManager.state = multipleSelectionState;
152
178
  // Ensure we reset if the field's values change and the user is not actively selecting options.
153
179
  (0, react_1.useEffect)(() => {
154
- if (!state.isOpen) {
155
- setFieldState(initFieldState);
180
+ if (!state.isOpen && !(0, utils_1.areArraysEqual)(values, fieldState.selectedKeys)) {
181
+ setFieldState((prevState) => {
182
+ var _a;
183
+ const selectedOptions = prevState.allOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
184
+ return {
185
+ ...prevState,
186
+ selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
187
+ inputValue: selectedOptions.length === 1
188
+ ? getOptionLabel(selectedOptions[0])
189
+ : multiselect && selectedOptions.length === 0
190
+ ? nothingSelectedText
191
+ : "",
192
+ selectedOptions: selectedOptions,
193
+ };
194
+ });
156
195
  }
157
196
  }, [values]);
158
- // Used to calculate the rendered width of the combo box (input + button)
159
- const comboBoxRef = (0, react_1.useRef)(null);
160
- const triggerRef = (0, react_1.useRef)(null);
161
- const inputRef = (0, react_1.useRef)(null);
162
- const inputWrapRef = (0, react_1.useRef)(null);
163
- const listBoxRef = (0, react_1.useRef)(null);
164
- const popoverRef = (0, react_1.useRef)(null);
197
+ (0, react_1.useEffect)(() => {
198
+ // Only update the fieldset when options change, when options is an array.
199
+ // Otherwise, if the options are passed in as an object, then we assume the caller is updating options via a Promise and not via updating props.
200
+ if (Array.isArray(maybeOptions) && maybeOptions !== fieldState.allOptions) {
201
+ setFieldState((prevState) => {
202
+ var _a;
203
+ const selectedOptions = maybeOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
204
+ return {
205
+ ...prevState,
206
+ selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
207
+ inputValue: selectedOptions.length === 1
208
+ ? getOptionLabel(selectedOptions[0])
209
+ : multiselect && selectedOptions.length === 0
210
+ ? nothingSelectedText
211
+ : "",
212
+ selectedOptions: selectedOptions,
213
+ filteredOptions: maybeOptions,
214
+ allOptions: maybeOptions,
215
+ };
216
+ });
217
+ }
218
+ }, [maybeOptions]);
165
219
  // For the most part, the returned props contain `aria-*` and `id` attributes for accessibility purposes.
166
220
  const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
167
221
  ...comboBoxProps,
@@ -188,6 +242,13 @@ function SelectFieldBase(props) {
188
242
  // Ensures the menu never gets too small.
189
243
  minWidth: 200,
190
244
  };
191
- return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).$, ref: comboBoxRef }, { children: [(0, jsx_runtime_1.jsx)(SelectFieldInput_1.SelectFieldInput, Object.assign({}, otherProps, { buttonProps: buttonProps, buttonRef: triggerRef, compact: compact, errorMsg: errorMsg, helperText: helperText, fieldDecoration: fieldDecoration, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, isDisabled: isDisabled, required: required, isReadOnly: isReadOnly, state: state, onBlur: onBlur, onFocus: onFocus, inlineLabel: inlineLabel, label: label, hideLabel: hideLabel, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, sizeToContent: sizeToContent, contrast: contrast, nothingSelectedText: nothingSelectedText, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly) }), void 0), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, Object.assign({ triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200 }, { children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, Object.assign({}, listBoxProps, { positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast }), void 0) }), void 0))] }), void 0));
245
+ return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).$, ref: comboBoxRef }, { children: [(0, jsx_runtime_1.jsx)(SelectFieldInput_1.SelectFieldInput, Object.assign({}, otherProps, { buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, state: state, labelProps: labelProps, selectedOptions: fieldState.selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, nothingSelectedText: nothingSelectedText, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField }), void 0), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, Object.assign({ triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200 }, { children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, Object.assign({}, listBoxProps, { positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, loading: fieldState.optionsLoading }), void 0) }), void 0))] }), void 0));
192
246
  }
193
247
  exports.SelectFieldBase = SelectFieldBase;
248
+ function getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSelectedText) {
249
+ return selectedOptions.length === 1
250
+ ? getOptionLabel(selectedOptions[0])
251
+ : multiselect && selectedOptions.length === 0
252
+ ? nothingSelectedText
253
+ : "";
254
+ }
@@ -2,6 +2,7 @@ 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
+ import { Callback } from "../../types";
5
6
  interface SelectFieldInputProps<O, V extends Value> extends PresentationFieldProps {
6
7
  buttonProps: any;
7
8
  buttonRef: MutableRefObject<HTMLButtonElement | null>;
@@ -9,8 +10,6 @@ interface SelectFieldInputProps<O, V extends Value> extends PresentationFieldPro
9
10
  inputRef: MutableRefObject<HTMLInputElement | null>;
10
11
  inputWrapRef: MutableRefObject<HTMLDivElement | null>;
11
12
  state: ComboBoxState<O>;
12
- isDisabled: boolean;
13
- isReadOnly: boolean;
14
13
  fieldDecoration?: (opt: O) => ReactNode;
15
14
  errorMsg?: string;
16
15
  required?: boolean;
@@ -23,10 +22,11 @@ interface SelectFieldInputProps<O, V extends Value> extends PresentationFieldPro
23
22
  selectedOptions: O[];
24
23
  getOptionValue: (opt: O) => V;
25
24
  getOptionLabel: (opt: O) => string;
26
- sizeToContent: boolean;
25
+ sizeToContent?: boolean;
27
26
  contrast?: boolean;
28
27
  nothingSelectedText: string;
29
28
  tooltip?: ReactNode;
29
+ resetField: Callback;
30
30
  }
31
31
  export declare function SelectFieldInput<O, V extends Value>(props: SelectFieldInputProps<O, V>): import("@emotion/react/jsx-runtime").JSX.Element;
32
32
  export {};
@@ -7,19 +7,18 @@ 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 Value_1 = require("../Value");
11
10
  const utils_1 = require("../../utils");
12
11
  function SelectFieldInput(props) {
13
- const { inputProps, inputRef, inputWrapRef, buttonProps, buttonRef, compact, errorMsg, required, helperText, state, fieldDecoration, isDisabled, isReadOnly, onBlur, onFocus, inlineLabel, label, labelProps, hideLabel, selectedOptions, getOptionValue, getOptionLabel, sizeToContent, contrast = false, nothingSelectedText, ...otherProps } = props;
12
+ const { inputProps, buttonProps, buttonRef, errorMsg, state, fieldDecoration, onBlur, onFocus, inlineLabel, selectedOptions, getOptionValue, getOptionLabel, sizeToContent = false, contrast = false, nothingSelectedText, resetField, ...otherProps } = props;
14
13
  const [isFocused, setIsFocused] = (0, react_1.useState)(false);
15
14
  const isMultiSelect = state.selectionManager.selectionMode === "multiple";
16
15
  const showNumSelection = isMultiSelect && state.selectionManager.selectedKeys.size > 1;
17
16
  // For MultiSelect only show the `fieldDecoration` when input is not in focus.
18
17
  const showFieldDecoration = (!isMultiSelect || (isMultiSelect && !isFocused)) && fieldDecoration && selectedOptions.length === 1;
19
- return ((0, jsx_runtime_1.jsx)(TextFieldBase_1.TextFieldBase, Object.assign({}, otherProps, { inputRef: inputRef, inputWrapRef: inputWrapRef, label: label, readOnly: isReadOnly, hideLabel: hideLabel, labelProps: labelProps, inlineLabel: inlineLabel, compact: compact, required: required, errorMsg: errorMsg, helperText: helperText, contrast: contrast, xss: !inlineLabel && !isReadOnly ? Css_1.Css.fw5.$ : {}, startAdornment: (showNumSelection && ((0, jsx_runtime_1.jsx)("span", Object.assign({ css: Css_1.Css.wPx(16).hPx(16).fs0.br100.bgLightBlue700.white.tinyEm.df.aic.jcc.$ }, { children: state.selectionManager.selectedKeys.size }), void 0))) ||
20
- (showFieldDecoration && fieldDecoration(selectedOptions[0])), endAdornment: !isReadOnly && ((0, jsx_runtime_1.jsx)("button", Object.assign({}, buttonProps, { disabled: isDisabled, ref: buttonRef, css: {
18
+ return ((0, jsx_runtime_1.jsx)(TextFieldBase_1.TextFieldBase, Object.assign({}, otherProps, { readOnly: inputProps.readOnly, inlineLabel: inlineLabel, errorMsg: errorMsg, contrast: contrast, xss: !inlineLabel && !inputProps.readOnly ? Css_1.Css.fw5.$ : {}, startAdornment: (showNumSelection && ((0, jsx_runtime_1.jsx)("span", Object.assign({ css: Css_1.Css.wPx(16).hPx(16).fs0.br100.bgLightBlue700.white.tinyEm.df.aic.jcc.$ }, { children: state.selectionManager.selectedKeys.size }), void 0))) ||
19
+ (showFieldDecoration && fieldDecoration(selectedOptions[0])), endAdornment: !inputProps.readOnly && ((0, jsx_runtime_1.jsx)("button", Object.assign({}, buttonProps, { disabled: inputProps.disabled, ref: buttonRef, css: {
21
20
  ...Css_1.Css.br4.outline0.gray700.if(contrast).gray400.$,
22
- ...(isDisabled ? Css_1.Css.cursorNotAllowed.gray400.if(contrast).gray600.$ : {}),
21
+ ...(inputProps.disabled ? Css_1.Css.cursorNotAllowed.gray400.if(contrast).gray600.$ : {}),
23
22
  } }, { children: (0, jsx_runtime_1.jsx)(components_1.Icon, { icon: state.isOpen ? "chevronUp" : "chevronDown" }, void 0) }), void 0)), inputProps: {
24
23
  ...(0, react_aria_1.mergeProps)(inputProps, { "aria-invalid": Boolean(errorMsg), onInput: () => state.open() }),
25
24
  // Not merging the following as we want them to overwrite existing events
@@ -37,7 +36,7 @@ function SelectFieldInput(props) {
37
36
  return;
38
37
  }
39
38
  // By default, the Escape key would "revert" changes,
40
- // but we just want to close the menu and leave the selections as is
39
+ // but we just want to close the menu and leave the reset of the field state as is.
41
40
  if (e.key === "Escape") {
42
41
  state.close();
43
42
  return;
@@ -48,31 +47,31 @@ function SelectFieldInput(props) {
48
47
  // reset the field to its previous value. However, because we use a the Multiple Selection State manager,
49
48
  // then our `state.selectedKey` isn't set. So we need to properly reset the state ourselves.
50
49
  if (e.key === "Escape") {
51
- // Triggering `Escape` is basically like re-selecting currently selected option, so do that if there is one.
52
- state.selectionManager.setSelectedKeys(selectedOptions.length > 0 ? [(0, Value_1.valueToKey)(getOptionValue(selectedOptions[0]))] : []);
50
+ state.close();
51
+ resetField();
53
52
  return;
54
53
  }
55
54
  inputProps.onKeyDown && inputProps.onKeyDown(e);
56
55
  },
57
- onBlur: () => {
56
+ onBlur: (e) => {
57
+ // Do not call onBlur if readOnly or interacting within the input wrapper (such as the menu trigger button).
58
+ if (inputProps.readOnly ||
59
+ (props.inputWrapRef.current && props.inputWrapRef.current.contains(e.relatedTarget))) {
60
+ return;
61
+ }
58
62
  // We purposefully override onBlur here instead of using mergeProps, b/c inputProps.onBlur
59
63
  // goes into useComboBox's onBlur, which calls setFocused(false), which in useComboBoxState
60
64
  // detects a) there is no props.selectedKey (b/c we don't pass it), and b) there is an
61
65
  // `inputValue`, so it thinks it needs to call `resetInputValue()`.
62
- //
63
- // I assume we don't pass `selectedKey` b/c we support multiple keys.
64
- if (isReadOnly) {
65
- return;
66
- }
67
66
  setIsFocused(false);
68
67
  (0, utils_1.maybeCall)(onBlur);
69
68
  state.close();
70
- // Always call `setSelectedKeys` onBlur with its existing selected keys..
71
- // This ensures the field's `input.value` resets to what it should be in case it doesn't currently match.
72
- state.selectionManager.setSelectedKeys(state.selectionManager.selectedKeys);
69
+ // Always call `resetField` onBlur, this ensures the field's `input.value` resets
70
+ // to what it should be in case it doesn't currently match.
71
+ resetField();
73
72
  },
74
73
  onFocus: () => {
75
- if (isReadOnly)
74
+ if (inputProps.readOnly)
76
75
  return;
77
76
  setIsFocused(true);
78
77
  (0, utils_1.maybeCall)(onFocus);
@@ -6,6 +6,7 @@ interface VirtualizedOptionsProps<O> {
6
6
  onListHeightChange: (height: number) => void;
7
7
  contrast: boolean;
8
8
  scrollOnFocus?: boolean;
9
+ loading?: boolean | (() => JSX.Element);
9
10
  }
10
11
  export declare function VirtualizedOptions<O>(props: VirtualizedOptionsProps<O>): import("@emotion/react/jsx-runtime").JSX.Element;
11
12
  export {};
@@ -4,10 +4,11 @@ exports.VirtualizedOptions = void 0;
4
4
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const react_virtuoso_1 = require("react-virtuoso");
7
+ const LoadingDots_1 = require("./LoadingDots");
7
8
  const Option_1 = require("./Option");
8
9
  // Displays ListBox options in a virtualized container for performance reasons
9
10
  function VirtualizedOptions(props) {
10
- const { state, items, onListHeightChange, contrast, scrollOnFocus } = props;
11
+ const { state, items, onListHeightChange, contrast, scrollOnFocus, loading } = props;
11
12
  const virtuosoRef = (0, react_1.useRef)(null);
12
13
  const focusedItem = state.collection.getItem(state.selectionManager.focusedKey);
13
14
  const selectedItem = state.selectionManager.selectedKeys.size > 0
@@ -33,6 +34,10 @@ function VirtualizedOptions(props) {
33
34
  // Only send scrollToIndex functionality forward if we are not auto-scrolling on focus.
34
35
  scrollToIndex: scrollOnFocus ? undefined : (_a = virtuosoRef.current) === null || _a === void 0 ? void 0 : _a.scrollToIndex }, item.key));
35
36
  }
36
- } }, void 0));
37
+ }, components: !loading
38
+ ? {}
39
+ : {
40
+ Footer: typeof loading === "function" ? loading : () => (0, jsx_runtime_1.jsx)(LoadingDots_1.LoadingDots, { contrast: contrast }, void 0),
41
+ } }, void 0));
37
42
  }
38
43
  exports.VirtualizedOptions = VirtualizedOptions;
@@ -27,3 +27,4 @@ export declare class EmptyRef<T> implements MutableRefObject<T> {
27
27
  set current(value: T);
28
28
  }
29
29
  export declare const isAbsoluteUrl: (url: string) => boolean;
30
+ export declare function areArraysEqual(a: any[], b: any[]): boolean;
@@ -10,7 +10,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
10
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.isAbsoluteUrl = exports.EmptyRef = exports.safeEntries = exports.noop = exports.omitKey = exports.safeKeys = exports.maybeCall = exports.toGroupState = exports.toToggleState = void 0;
13
+ exports.areArraysEqual = exports.isAbsoluteUrl = exports.EmptyRef = exports.safeEntries = exports.noop = exports.omitKey = exports.safeKeys = exports.maybeCall = exports.toGroupState = exports.toToggleState = void 0;
14
14
  /** Adapts our state to what useToggleState returns in a stateless manner. */
15
15
  function toToggleState(isSelected, onChange) {
16
16
  return {
@@ -74,3 +74,7 @@ class EmptyRef {
74
74
  exports.EmptyRef = EmptyRef;
75
75
  const isAbsoluteUrl = (url) => /^(http(s?)):\/\//i.test(url);
76
76
  exports.isAbsoluteUrl = isAbsoluteUrl;
77
+ function areArraysEqual(a, b) {
78
+ return a.length === b.length && a.every((val, idx) => val === b[idx]);
79
+ }
80
+ exports.areArraysEqual = areArraysEqual;
package/dist/utils/sb.js CHANGED
@@ -6,7 +6,7 @@ const components_1 = require("../components");
6
6
  const Css_1 = require("../Css");
7
7
  const rtl_1 = require("./rtl");
8
8
  function withRouter(url, path) {
9
- return (storyFn) => (0, rtl_1.withRouter)(url, path).wrap(storyFn());
9
+ return (Story) => (0, rtl_1.withRouter)(url, path).wrap((0, jsx_runtime_1.jsx)(Story, {}, void 0));
10
10
  }
11
11
  exports.withRouter = withRouter;
12
12
  /** A somewhat typesafe way to set `FooStory.story` metadata. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.96.3",
3
+ "version": "2.97.3",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "format": "prettier --loglevel warn --write \"**/*.{ts,tsx,css,md}\""
34
34
  },
35
35
  "dependencies": {
36
- "@homebound/form-state": "^2.1.5",
36
+ "@homebound/form-state": "^2.2.13",
37
37
  "@react-aria/utils": "^3.9.0",
38
38
  "@react-hook/resize-observer": "^1.2.2",
39
39
  "@types/tinycolor2": "^1.4.2",
@@ -71,6 +71,7 @@
71
71
  "devDependencies": {
72
72
  "@babel/core": "^7.15.5",
73
73
  "@babel/plugin-proposal-class-properties": "^7.14.5",
74
+ "@babel/preset-env": "^7.16.5",
74
75
  "@babel/preset-typescript": "^7.15.0",
75
76
  "@emotion/babel-preset-css-prop": "^11.2.0",
76
77
  "@emotion/jest": "^11.3.0",
@@ -79,10 +80,12 @@
79
80
  "@homebound/rtl-utils": "^2.51.0",
80
81
  "@homebound/tsconfig": "^1.0.3",
81
82
  "@semantic-release/git": "^9.0.0",
82
- "@storybook/addon-essentials": "^6.3.4",
83
- "@storybook/addon-links": "^6.3.4",
84
- "@storybook/addons": "^6.3.4",
85
- "@storybook/react": "^6.3.4",
83
+ "@storybook/addon-essentials": "^6.4.9",
84
+ "@storybook/addon-interactions": "^6.4.9",
85
+ "@storybook/addon-links": "^6.4.9",
86
+ "@storybook/addons": "^6.4.9",
87
+ "@storybook/react": "^6.4.9",
88
+ "@storybook/testing-library": "^0.0.7",
86
89
  "@testing-library/jest-dom": "^5.11.9",
87
90
  "@testing-library/react-hooks": "^7.0.1",
88
91
  "@tsconfig/recommended": "^1.0.1",