@homebound/beam 2.321.0 → 2.322.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.
@@ -12,7 +12,6 @@ const Css_1 = require("../../Css");
12
12
  const ComboBoxInput_1 = require("./ComboBoxInput");
13
13
  const ListBox_1 = require("./ListBox");
14
14
  const Value_1 = require("../Value");
15
- const utils_1 = require("../../utils");
16
15
  /**
17
16
  * Provides a non-native select/dropdown widget that allows the user to type to filter the options.
18
17
  *
@@ -25,45 +24,49 @@ const utils_1 = require("../../utils");
25
24
  function ComboBoxBase(props) {
26
25
  var _a, _b, _c, _d;
27
26
  const { fieldProps } = (0, PresentationContext_1.usePresentationContext)();
28
- const { disabled, readOnly, onSelect, options: propOptions, multiselect = false, values = [], nothingSelectedText = "", contrast, disabledOptions, borderless, unsetLabel, getOptionLabel: propOptionLabel, getOptionValue: propOptionValue, getOptionMenuLabel: propOptionMenuLabel, ...otherProps } = props;
27
+ const { disabled, readOnly, onSelect, options: propOptions, multiselect = false, values: propValues, nothingSelectedText = "", contrast, disabledOptions, borderless, unsetLabel, getOptionLabel: propOptionLabel, getOptionValue: propOptionValue, getOptionMenuLabel: propOptionMenuLabel, ...otherProps } = props;
29
28
  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
29
  // Memoize the callback functions and handle the `unset` option if provided.
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]);
30
+ const getOptionLabel = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? unsetLabel : propOptionLabel(o)),
31
+ // propOptionLabel is basically always a lambda, so don't dep on it
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ [unsetLabel]);
34
+ const getOptionValue = (0, react_1.useCallback)((o) => (unsetLabel && o === exports.unsetOption ? undefined : propOptionValue(o)),
35
+ // propOptionValue is basically always a lambda, so don't dep on it
36
+ // eslint-disable-next-line react-hooks/exhaustive-deps
37
+ [unsetLabel]);
38
+ const getOptionMenuLabel = (0, react_1.useCallback)((o) => propOptionMenuLabel ? propOptionMenuLabel(o, Boolean(unsetLabel) && o === exports.unsetOption) : getOptionLabel(o),
39
+ // propOptionMenuLabel is basically always a lambda, so don't dep on it
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ [unsetLabel, getOptionLabel]);
34
42
  // Call `initializeOptions` to prepend the `unset` option if the `unsetLabel` was provided.
35
43
  const options = (0, react_1.useMemo)(() => initializeOptions(propOptions, getOptionValue, unsetLabel),
36
44
  // If the caller is using { current, load, options }, memoize on only `current` and `options` values.
37
45
  // ...and don't bother on memoizing on getOptionValue b/c it's basically always a lambda
38
46
  // eslint-disable-next-line react-hooks/exhaustive-deps
39
47
  Array.isArray(propOptions) ? [propOptions, unsetLabel] : [propOptions.current, propOptions.options, unsetLabel]);
48
+ const values = (0, react_1.useMemo)(() => propValues !== null && propValues !== void 0 ? propValues : [], [propValues]);
49
+ const selectedOptions = (0, react_1.useMemo)(() => {
50
+ return options.filter((o) => values.includes(getOptionValue(o)));
51
+ }, [options, values, getOptionValue]);
40
52
  const { contains } = (0, react_aria_1.useFilter)({ sensitivity: "base" });
41
53
  const isDisabled = !!disabled;
42
54
  const isReadOnly = !!readOnly;
43
55
  // Do a one-time initialize of fieldState
44
56
  const [fieldState, setFieldState] = (0, react_1.useState)(() => {
45
- var _a;
46
- const selectedOptions = options.filter((o) => values.includes(getOptionValue(o)));
47
57
  return {
48
- selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
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,
58
+ inputValue: getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSelectedText),
59
+ searchValue: undefined,
53
60
  optionsLoading: false,
54
61
  };
55
62
  });
63
+ const { searchValue } = fieldState;
64
+ const filteredOptions = (0, react_1.useMemo)(() => {
65
+ return !searchValue ? options : options.filter((o) => contains(getOptionLabel(o), searchValue));
66
+ }, [options, searchValue, getOptionLabel, contains]);
56
67
  /** Resets field's input value and filtered options list for cases where the user exits the field without making changes (on Escape, or onBlur) */
57
68
  function resetField() {
58
- const inputValue = getInputValue(fieldState.allOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o))), getOptionLabel, multiselect, nothingSelectedText);
59
- // Conditionally reset the value if the current inputValue doesn't match that of the passed in value, or we filtered the list
60
- if (inputValue !== fieldState.inputValue || fieldState.filteredOptions.length !== fieldState.allOptions.length) {
61
- setFieldState((prevState) => ({
62
- ...prevState,
63
- inputValue,
64
- filteredOptions: prevState.allOptions,
65
- }));
66
- }
69
+ setFieldState((prevState) => ({ ...prevState, searchValue: undefined }));
67
70
  }
68
71
  function onSelectionChange(keys) {
69
72
  // We don't currently handle the "all" case
@@ -76,30 +79,11 @@ function ComboBoxBase(props) {
76
79
  const selectionChanged = !(keys.size === state.selectionManager.selectedKeys.size &&
77
80
  [...keys].every((value) => state.selectionManager.selectedKeys.has(value)));
78
81
  if (multiselect && keys.size === 0) {
79
- setFieldState({
80
- ...fieldState,
81
- inputValue: state.isOpen ? "" : nothingSelectedText,
82
- selectedKeys: [],
83
- selectedOptions: [],
84
- });
85
82
  selectionChanged && onSelect([], []);
86
83
  return;
87
84
  }
88
85
  const selectedKeys = [...keys.values()];
89
- const selectedOptions = fieldState.allOptions.filter((o) => selectedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o))));
90
- const firstSelectedOption = selectedOptions[0];
91
- setFieldState((prevState) => ({
92
- ...prevState,
93
- // If menu is open then reset inputValue to "". Otherwise set inputValue depending on number of options selected.
94
- inputValue: multiselect && (state.isOpen || selectedKeys.length > 1)
95
- ? ""
96
- : firstSelectedOption
97
- ? getOptionLabel(firstSelectedOption)
98
- : "",
99
- selectedKeys,
100
- selectedOptions,
101
- filteredOptions: fieldState.allOptions,
102
- }));
86
+ const selectedOptions = options.filter((o) => selectedKeys.includes((0, Value_1.valueToKey)(getOptionValue(o))));
103
87
  selectionChanged && onSelect(selectedKeys.map(Value_1.keyToValue), selectedOptions);
104
88
  if (!multiselect) {
105
89
  // Close menu upon selection change only for Single selection mode
@@ -108,11 +92,7 @@ function ComboBoxBase(props) {
108
92
  }
109
93
  function onInputChange(value) {
110
94
  if (value !== fieldState.inputValue) {
111
- setFieldState((prevState) => ({
112
- ...prevState,
113
- inputValue: value,
114
- filteredOptions: fieldState.allOptions.filter((o) => contains(getOptionLabel(o), value)),
115
- }));
95
+ setFieldState((prevState) => ({ ...prevState, inputValue: value, searchValue: value }));
116
96
  }
117
97
  }
118
98
  async function maybeInitLoad() {
@@ -147,7 +127,7 @@ function ComboBoxBase(props) {
147
127
  ...otherProps,
148
128
  disabledKeys: Object.keys(disabledOptionsWithReasons),
149
129
  inputValue: fieldState.inputValue,
150
- items: fieldState.filteredOptions,
130
+ items: filteredOptions,
151
131
  isDisabled,
152
132
  isReadOnly,
153
133
  onInputChange,
@@ -172,59 +152,34 @@ function ComboBoxBase(props) {
172
152
  }
173
153
  },
174
154
  });
155
+ const selectedKeys = (0, react_1.useMemo)(() => {
156
+ return selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)));
157
+ }, [selectedOptions, getOptionValue]);
175
158
  // @ts-ignore - `selectionManager.state` exists, but not according to the types
176
159
  state.selectionManager.state = (0, react_stately_1.useMultipleSelectionState)({
177
160
  selectionMode: multiselect ? "multiple" : "single",
178
161
  // Do not allow an empty selection if single select mode
179
162
  disallowEmptySelection: !multiselect,
180
- selectedKeys: fieldState.selectedKeys,
163
+ selectedKeys,
181
164
  onSelectionChange,
182
165
  });
183
- // Ensure we reset if the field's values change and the user is not actively selecting options.
166
+ // Reset inputValue when closed or selected changes
184
167
  (0, react_1.useEffect)(() => {
185
- if (!state.isOpen && !(0, utils_1.areArraysEqual)(values, fieldState.selectedKeys)) {
186
- setFieldState((prevState) => {
187
- var _a;
188
- const selectedOptions = prevState.allOptions.filter((o) => values === null || values === void 0 ? void 0 : values.includes(getOptionValue(o)));
189
- return {
190
- ...prevState,
191
- selectedKeys: (_a = selectedOptions === null || selectedOptions === void 0 ? void 0 : selectedOptions.map((o) => (0, Value_1.valueToKey)(getOptionValue(o)))) !== null && _a !== void 0 ? _a : [],
192
- inputValue: selectedOptions.length === 1
193
- ? getOptionLabel(selectedOptions[0])
194
- : multiselect && selectedOptions.length === 0
195
- ? nothingSelectedText
196
- : "",
197
- selectedOptions: selectedOptions,
198
- };
199
- });
168
+ if (state.isOpen && multiselect) {
169
+ // While the multiselect is open, let the user keep typing
170
+ setFieldState((prevState) => ({
171
+ ...prevState,
172
+ inputValue: "",
173
+ searchValue: "",
174
+ }));
200
175
  }
201
- },
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
203
- // eslint-disable-next-line react-hooks/exhaustive-deps
204
- [values]);
205
- // Re-sync fieldState.allOptions
206
- (0, react_1.useEffect)(() => {
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 {
176
+ else {
177
+ setFieldState((prevState) => ({
211
178
  ...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
- });
223
- },
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
226
- // eslint-disable-next-line react-hooks/exhaustive-deps
227
- [options]);
179
+ inputValue: getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSelectedText),
180
+ }));
181
+ }
182
+ }, [state.isOpen, selectedOptions, getOptionLabel, multiselect, nothingSelectedText]);
228
183
  // For the most part, the returned props contain `aria-*` and `id` attributes for accessibility purposes.
229
184
  const { buttonProps: triggerProps, inputProps, listBoxProps, labelProps, } = (0, react_aria_1.useComboBox)({
230
185
  ...comboBoxProps,
@@ -251,7 +206,7 @@ function ComboBoxBase(props) {
251
206
  // Ensures the menu never gets too small.
252
207
  minWidth: 200,
253
208
  };
254
- return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, 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 }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200, children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, { ...listBoxProps, positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: fieldState.selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, horizontalLayout: labelStyle === "left", loading: fieldState.optionsLoading, disabledOptionsWithReasons: disabledOptionsWithReasons }) }))] }));
209
+ return ((0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.fdc.w100.maxw((0, Css_1.px)(550)).if(labelStyle === "left").maxw100.$, ref: comboBoxRef, children: [(0, jsx_runtime_1.jsx)(ComboBoxInput_1.ComboBoxInput, { ...otherProps, buttonProps: buttonProps, buttonRef: triggerRef, inputProps: inputProps, inputRef: inputRef, inputWrapRef: inputWrapRef, listBoxRef: listBoxRef, state: state, labelProps: labelProps, selectedOptions: selectedOptions, getOptionValue: getOptionValue, getOptionLabel: getOptionLabel, contrast: contrast, nothingSelectedText: nothingSelectedText, borderless: borderless, tooltip: (0, components_1.resolveTooltip)(disabled, undefined, readOnly), resetField: resetField }), state.isOpen && ((0, jsx_runtime_1.jsx)(internal_1.Popover, { triggerRef: triggerRef, popoverRef: popoverRef, positionProps: positionProps, onClose: () => state.close(), isOpen: state.isOpen, minWidth: 200, children: (0, jsx_runtime_1.jsx)(ListBox_1.ListBox, { ...listBoxProps, positionProps: positionProps, state: state, listBoxRef: listBoxRef, selectedOptions: selectedOptions, getOptionLabel: getOptionLabel, getOptionValue: (o) => (0, Value_1.valueToKey)(getOptionValue(o)), contrast: contrast, horizontalLayout: labelStyle === "left", loading: fieldState.optionsLoading, disabledOptionsWithReasons: disabledOptionsWithReasons }) }))] }));
255
210
  }
256
211
  exports.ComboBoxBase = ComboBoxBase;
257
212
  function getInputValue(selectedOptions, getOptionLabel, multiselect, nothingSelectedText) {
@@ -298,6 +253,3 @@ function disabledOptionToKeyedTuple(disabledOption) {
298
253
  }
299
254
  }
300
255
  exports.disabledOptionToKeyedTuple = disabledOptionToKeyedTuple;
301
- function asArray(arrayOrElement) {
302
- return Array.isArray(arrayOrElement) ? arrayOrElement : arrayOrElement ? [arrayOrElement] : [];
303
- }
@@ -29,7 +29,7 @@ export declare function rowAnd(r: RenderResult, rowNum: number, testId: string):
29
29
  "
30
30
  `);
31
31
  * */
32
- export declare function tableSnapshot(r: RenderResult): string;
32
+ export declare function tableSnapshot(r: RenderResult, columnNames?: string[]): string;
33
33
  /** RTL wrapper for Beam's SuperDrawer/Modal context. */
34
34
  export declare const withBeamRTL: Wrapper;
35
35
  /**
package/dist/utils/rtl.js CHANGED
@@ -76,17 +76,30 @@ exports.rowAnd = rowAnd;
76
76
  "
77
77
  `);
78
78
  * */
79
- function tableSnapshot(r) {
79
+ function tableSnapshot(r, columnNames = []) {
80
80
  const tableEl = r.getByTestId("gridTable");
81
81
  const dataRows = Array.from(tableEl.querySelectorAll("[data-gridrow]"));
82
82
  const hasExpandableHeader = !!tableEl.querySelector(`[data-testid="expandableColumn"]`);
83
- const tableDataAsStrings = dataRows.map((row) => {
83
+ let tableDataAsStrings = dataRows.map((row) => {
84
84
  return Array.from(row.childNodes).map(getTextFromTableCellNode);
85
85
  });
86
- return toMarkupTableString({ tableRows: tableDataAsStrings, hasExpandableHeader });
86
+ // If the user wants a subset of columns, look for column names
87
+ if (columnNames.length > 0) {
88
+ const headerCells = tableDataAsStrings[0];
89
+ if (headerCells) {
90
+ const columnIndices = columnNames.map((name) => {
91
+ const i = headerCells.indexOf(name);
92
+ if (i === -1)
93
+ throw new Error(`Could not find header '${name}' in ${headerCells.join(", ")}`);
94
+ return i;
95
+ });
96
+ tableDataAsStrings = tableDataAsStrings.map((row) => columnIndices.map((index) => row[index]));
97
+ }
98
+ }
99
+ return toMarkupTableString(tableDataAsStrings, hasExpandableHeader);
87
100
  }
88
101
  exports.tableSnapshot = tableSnapshot;
89
- function toMarkupTableString({ tableRows, hasExpandableHeader, }) {
102
+ function toMarkupTableString(tableRows, hasExpandableHeader) {
90
103
  // Find the largest width of each column to set a consistent width for each row
91
104
  const columnWidths = tableRows.reduce((acc, row) => {
92
105
  row.forEach((cell, columnIndex) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.321.0",
3
+ "version": "2.322.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",