@homebound/beam 2.106.4 → 2.107.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.
@@ -176,6 +176,8 @@ export interface GridTableProps<R extends Kinded, S, X> {
176
176
  /** NOTE: This API is experimental and primarily intended for story and testing purposes */
177
177
  export declare type GridTableApi = {
178
178
  scrollToIndex: (index: number) => void;
179
+ /** Returns the ids of currently-selected rows. */
180
+ getSelectedRowIds(): string[];
179
181
  };
180
182
  /**
181
183
  * Renders data in our table layout.
@@ -298,7 +300,8 @@ export declare type GridDataRow<R extends Kinded> = {
298
300
  children?: GridDataRow<R>[];
299
301
  /** Whether to pin this sort to the first/last of its parent's children. */
300
302
  pin?: "first" | "last";
301
- } & DiscriminateUnion<R, "kind", R["kind"]>;
303
+ } & IfAny<R, {}, DiscriminateUnion<R, "kind", R["kind"]>>;
304
+ declare type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
302
305
  /** Return the content for a given column def applied to a given row. */
303
306
  export declare function applyRowFn<R extends Kinded>(column: GridColumn<R>, row: GridDataRow<R>): ReactNode | GridCellContent;
304
307
  export declare function matchesFilter(maybeContent: ReactNode | GridCellContent, filter: string): boolean;
@@ -95,6 +95,7 @@ function GridTable(props) {
95
95
  if (api) {
96
96
  api.current = {
97
97
  scrollToIndex: (index) => virtuosoRef.current && virtuosoRef.current.scrollToIndex(index),
98
+ getSelectedRowIds: () => rowState.selectedIds,
98
99
  };
99
100
  }
100
101
  const [sortState, setSortKey] = (0, useSortState_1.useSortState)(columns, sorting);
@@ -1,5 +1,6 @@
1
1
  import React, { MutableRefObject } from "react";
2
2
  import { GridDataRow } from "./GridTable";
3
+ export declare type SelectedState = "checked" | "unchecked" | "partial";
3
4
  /**
4
5
  * Stores the collapsed & selected state of rows.
5
6
  *
@@ -19,10 +20,14 @@ export declare class RowState {
19
20
  private rows;
20
21
  private persistCollapse;
21
22
  private readonly collapsedRows;
23
+ private readonly selectedRows;
22
24
  /**
23
25
  * Creates the `RowState` for a given `GridTable`.
24
26
  */
25
27
  constructor(rows: MutableRefObject<GridDataRow<any>[]>, persistCollapse: string | undefined);
28
+ get selectedIds(): string[];
29
+ getSelected(id: string): SelectedState;
30
+ selectRow(id: string, selected: boolean): void;
26
31
  get collapsedIds(): string[];
27
32
  isCollapsed(id: string): boolean;
28
33
  toggleCollapsed(id: string): void;
@@ -6,9 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.RowStateContext = exports.RowState = void 0;
7
7
  const mobx_1 = require("mobx");
8
8
  const react_1 = __importDefault(require("react"));
9
- // Will be used in the next PR...
10
- // A parent row can be partially selected when some children are selected/some aren't.
11
- // export type SelectedState = "checked" | "unchecked" | "partial";
12
9
  /**
13
10
  * Stores the collapsed & selected state of rows.
14
11
  *
@@ -25,19 +22,74 @@ const react_1 = __importDefault(require("react"));
25
22
  * changes.
26
23
  */
27
24
  class RowState {
28
- // Coming in future PR
29
- // readonly selectedRows = new ObservableMap<string, "checked" | "unchecked" | "partial">();
30
25
  /**
31
26
  * Creates the `RowState` for a given `GridTable`.
32
27
  */
33
28
  constructor(rows, persistCollapse) {
34
29
  this.rows = rows;
35
30
  this.persistCollapse = persistCollapse;
31
+ this.selectedRows = new mobx_1.ObservableMap();
36
32
  this.collapsedRows = new mobx_1.ObservableSet(persistCollapse ? readLocalCollapseState(persistCollapse) : []);
37
33
  // Make ourselves an observable so that mobx will do caching of .collapseIds so
38
34
  // that it'll be a stable identity for GridTable to useMemo against.
39
35
  (0, mobx_1.makeAutoObservable)(this, { rows: false }); // as any b/c rows is private, so the mapped type doesn't see it
40
36
  }
37
+ get selectedIds() {
38
+ // Return only ids that are fully checked, i.e. not partial
39
+ const ids = [...this.selectedRows.entries()].filter(([, v]) => v === "checked").map(([k]) => k);
40
+ // Hide our header marker
41
+ const headerIndex = ids.indexOf("header");
42
+ if (headerIndex > -1) {
43
+ ids.splice(headerIndex, 1);
44
+ }
45
+ return ids;
46
+ }
47
+ // Should be called in an Observer/useComputed to trigger re-renders
48
+ getSelected(id) {
49
+ // We may not have every row in here, i.e. on 1st page load or after clicking here, so assume unchecked
50
+ return this.selectedRows.get(id) || "unchecked";
51
+ }
52
+ selectRow(id, selected) {
53
+ if (id === "header") {
54
+ // Select/unselect all has special behavior
55
+ if (selected) {
56
+ // Just mash the header + all rows + children as selected
57
+ const map = new Map();
58
+ map.set("header", "checked");
59
+ visit(this.rows.current, (row) => map.set(row.id, "checked"));
60
+ this.selectedRows.replace(map);
61
+ }
62
+ else {
63
+ // Similarly "unmash" all rows + children.
64
+ this.selectedRows.clear();
65
+ }
66
+ }
67
+ else {
68
+ // This is the regular/non-header behavior to just add/remove the individual row id,
69
+ // plus percolate the change down-to-child + up-to-parents.
70
+ // Find the clicked on row
71
+ const curr = findRow(this.rows.current, id);
72
+ if (!curr) {
73
+ return;
74
+ }
75
+ // Everything here & down is deterministically on/off
76
+ const map = new Map();
77
+ visit([curr.row], (row) => map.set(row.id, selected ? "checked" : "unchecked"));
78
+ // Now walk up the parents and see if they are now-all-checked/now-all-unchecked/some-of-each
79
+ for (const parent of [...curr.parents].reverse()) {
80
+ if (parent.children) {
81
+ const children = parent.children.map((row) => map.get(row.id) || this.getSelected(row.id));
82
+ map.set(parent.id, deriveParentSelected(children));
83
+ }
84
+ }
85
+ // And do the header + top-level "children" as a final one-off
86
+ const children = this.rows.current
87
+ .filter((row) => row.id !== "header")
88
+ .map((row) => map.get(row.id) || this.getSelected(row.id));
89
+ map.set("header", deriveParentSelected(children));
90
+ this.selectedRows.merge(map);
91
+ }
92
+ }
41
93
  get collapsedIds() {
42
94
  return [...this.collapsedRows.values()];
43
95
  }
@@ -100,3 +152,34 @@ function readLocalCollapseState(persistCollapse) {
100
152
  const collapsedGridRowIds = localStorage.getItem(persistCollapse);
101
153
  return collapsedGridRowIds ? JSON.parse(collapsedGridRowIds) : [];
102
154
  }
155
+ /** Finds a row by id, and returns it + any parents. */
156
+ function findRow(rows, id) {
157
+ // This is technically an array of "maybe FoundRow"
158
+ const todo = rows.map((row) => ({ row, parents: [] }));
159
+ while (todo.length > 0) {
160
+ const curr = todo.pop();
161
+ if (curr.row.id === id) {
162
+ return curr;
163
+ }
164
+ else if (curr.row.children) {
165
+ // Search our children and pass along us as the parent
166
+ todo.push(...curr.row.children.map((child) => ({ row: child, parents: [...curr.parents, curr.row] })));
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+ function deriveParentSelected(children) {
172
+ const allChecked = children.every((child) => child === "checked");
173
+ const allUnchecked = children.every((child) => child === "unchecked");
174
+ return allChecked ? "checked" : allUnchecked ? "unchecked" : "partial";
175
+ }
176
+ function visit(rows, fn) {
177
+ const todo = [...rows];
178
+ while (todo.length > 0) {
179
+ const row = todo.pop();
180
+ fn(row);
181
+ if (row.children) {
182
+ todo.push(...row.children);
183
+ }
184
+ }
185
+ }
@@ -1,6 +1,6 @@
1
1
  import { FieldState } from "@homebound/form-state";
2
2
  import { CheckboxProps } from "../inputs";
3
- export declare type BoundCheckboxFieldProps = Omit<CheckboxProps, "values" | "onChange" | "label"> & {
3
+ export declare type BoundCheckboxFieldProps = Omit<CheckboxProps, "selected" | "onChange" | "label"> & {
4
4
  field: FieldState<any, boolean | null | undefined>;
5
5
  /** Make optional so that callers can override if they want to. */
6
6
  onChange?: (values: boolean) => void;
@@ -2,17 +2,12 @@ import { ReactNode } from "react";
2
2
  export interface CheckboxProps {
3
3
  label: string;
4
4
  checkboxOnly?: boolean;
5
+ selected: boolean | "indeterminate";
5
6
  /** Handler that is called when the element's selection state changes. */
6
7
  onChange: (selected: boolean) => void;
7
8
  /** Additional text displayed below label */
8
9
  description?: string;
9
10
  disabled?: boolean;
10
- /**
11
- * Indeterminism is presentational only.
12
- * The indeterminate visual representation remains regardless of user interaction.
13
- */
14
- indeterminate?: boolean;
15
- selected?: boolean;
16
11
  errorMsg?: string;
17
12
  helperText?: string | ReactNode;
18
13
  /** Callback fired when focus removes from the component */
@@ -7,12 +7,14 @@ const react_aria_1 = require("react-aria");
7
7
  const react_stately_1 = require("react-stately");
8
8
  const CheckboxBase_1 = require("./CheckboxBase");
9
9
  function Checkbox(props) {
10
- const { label, indeterminate: isIndeterminate = false, disabled: isDisabled = false, selected, ...otherProps } = props;
11
- const ariaProps = { isSelected: selected, isDisabled, isIndeterminate, ...otherProps };
10
+ const { label, disabled: isDisabled = false, selected, ...otherProps } = props;
11
+ // Treat indeterminate as false so that clicking on indeterminate always goes --> true.
12
+ const isSelected = selected === true;
13
+ const isIndeterminate = selected === "indeterminate";
14
+ const ariaProps = { isSelected, isDisabled, isIndeterminate, ...otherProps };
12
15
  const checkboxProps = { ...ariaProps, "aria-label": label };
13
16
  const ref = (0, react_1.useRef)(null);
14
17
  const toggleState = (0, react_stately_1.useToggleState)(ariaProps);
15
- const isSelected = toggleState.isSelected;
16
18
  const { inputProps } = (0, react_aria_1.useCheckbox)(checkboxProps, toggleState, ref);
17
19
  return ((0, jsx_runtime_1.jsx)(CheckboxBase_1.CheckboxBase, Object.assign({ ariaProps: ariaProps, isDisabled: isDisabled, isIndeterminate: isIndeterminate, isSelected: isSelected, inputProps: inputProps, label: label }, otherProps), void 0));
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.106.4",
3
+ "version": "2.107.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",