@homebound/beam 2.106.2 → 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.
@@ -4,21 +4,18 @@ exports.CollapseToggle = void 0;
4
4
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
5
  const react_1 = require("react");
6
6
  const components_1 = require("..");
7
+ const hooks_1 = require("../../hooks");
7
8
  function CollapseToggle(props) {
8
9
  const { row } = props;
9
- const { isCollapsed, toggleCollapsed } = (0, react_1.useContext)(components_1.GridCollapseContext);
10
- const [, setTick] = (0, react_1.useState)(0);
11
- const currentlyCollapsed = isCollapsed(row.id);
12
- const toggleOnClick = (0, react_1.useCallback)(() => {
13
- toggleCollapsed(row.id);
14
- setTick(Date.now());
15
- }, [row.id, currentlyCollapsed, toggleCollapsed]);
16
- const iconKey = currentlyCollapsed ? "chevronRight" : "chevronDown";
17
- const headerIconKey = currentlyCollapsed ? "chevronsRight" : "chevronsDown";
10
+ const { rowState } = (0, react_1.useContext)(components_1.RowStateContext);
11
+ const isCollapsed = (0, hooks_1.useComputed)(() => rowState.isCollapsed(row.id), [rowState]);
12
+ const iconKey = isCollapsed ? "chevronRight" : "chevronDown";
13
+ const headerIconKey = isCollapsed ? "chevronsRight" : "chevronsDown";
14
+ // If we're not a header, only render a toggle if we have child rows to actually collapse
18
15
  const isHeader = row.kind === "header";
19
16
  if (!isHeader && (!props.row.children || props.row.children.length === 0)) {
20
17
  return null;
21
18
  }
22
- return (0, jsx_runtime_1.jsx)(components_1.IconButton, { onClick: toggleOnClick, icon: isHeader ? headerIconKey : iconKey }, void 0);
19
+ return (0, jsx_runtime_1.jsx)(components_1.IconButton, { onClick: () => rowState.toggleCollapsed(row.id), icon: isHeader ? headerIconKey : iconKey }, void 0);
23
20
  }
24
21
  exports.CollapseToggle = CollapseToggle;
@@ -1,4 +1,4 @@
1
- import React, { MutableRefObject, ReactElement, ReactNode } from "react";
1
+ import { MutableRefObject, ReactElement, ReactNode } from "react";
2
2
  import { PresentationContextProps, PresentationFieldProps } from "../PresentationContext";
3
3
  import { GridRowLookup } from "./GridRowLookup";
4
4
  import { Margin, Only, Properties, Typography, Xss } from "../../Css";
@@ -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,24 +300,9 @@ 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
- /**
305
- * Provides each row access to a method to check if it is collapsed and toggle it's collapsed state.
306
- *
307
- * Calling `toggleCollapse` will keep the row itself showing, but will hide any
308
- * children rows (specifically those that have this row's `id` in their `parentIds`
309
- * prop).
310
- *
311
- * headerCollapsed is used to trigger rows at the root level to rerender their chevron when all are
312
- * collapsed/expanded.
313
- */
314
- declare type GridCollapseContextProps = {
315
- headerCollapsed: boolean;
316
- isCollapsed: (id: string) => boolean;
317
- toggleCollapsed(id: string): void;
318
- };
319
- export declare const GridCollapseContext: React.Context<GridCollapseContextProps>;
320
307
  export declare function matchesFilter(maybeContent: ReactNode | GridCellContent, filter: string): boolean;
321
308
  export {};
@@ -22,9 +22,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.matchesFilter = exports.GridCollapseContext = exports.applyRowFn = exports.calcColumnSizes = exports.GridTable = exports.setGridTableDefaults = exports.setDefaultStyle = exports.setRunningInJest = exports.emptyCell = exports.DESC = exports.ASC = void 0;
25
+ exports.matchesFilter = exports.applyRowFn = exports.calcColumnSizes = exports.GridTable = exports.setGridTableDefaults = exports.setDefaultStyle = exports.setRunningInJest = exports.emptyCell = exports.DESC = exports.ASC = void 0;
26
26
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
27
- const utils_1 = require("@react-aria/utils");
28
27
  const memoize_one_1 = __importDefault(require("memoize-one"));
29
28
  const mobx_react_1 = require("mobx-react");
30
29
  const react_1 = __importStar(require("react"));
@@ -32,15 +31,17 @@ const react_router_dom_1 = require("react-router-dom");
32
31
  const react_virtuoso_1 = require("react-virtuoso");
33
32
  const CssReset_1 = require("../CssReset");
34
33
  const PresentationContext_1 = require("../PresentationContext");
34
+ const columnSizes_1 = require("./columnSizes");
35
35
  const GridRowLookup_1 = require("./GridRowLookup");
36
36
  const GridSortContext_1 = require("./GridSortContext");
37
37
  const nestedCards_1 = require("./nestedCards");
38
+ const RowState_1 = require("./RowState");
38
39
  const SortHeader_1 = require("./SortHeader");
39
40
  const sortRows_1 = require("./sortRows");
40
41
  const useSortState_1 = require("./useSortState");
41
42
  const Css_1 = require("../../Css");
43
+ const hooks_1 = require("../../hooks");
42
44
  const tinycolor2_1 = __importDefault(require("tinycolor2"));
43
- const use_debounce_1 = require("use-debounce");
44
45
  const _1 = require(".");
45
46
  exports.ASC = "ASC";
46
47
  exports.DESC = "DESC";
@@ -82,15 +83,19 @@ exports.setGridTableDefaults = setGridTableDefaults;
82
83
  * special styling to the row that uses `kind: "header"`.)
83
84
  */
84
85
  function GridTable(props) {
85
- var _a, _b, _c, _d, _e;
86
+ var _a, _b, _c, _d;
86
87
  const { id = "gridTable", as = "div", columns, rows, style = defaults.style, rowStyles, stickyHeader = defaults.stickyHeader, stickyOffset = "0", xss, sorting, filter, filterMaxRows, fallbackMessage = "No rows found.", infoMessage, setRowCount, observeRows, persistCollapse, api, resizeTarget, } = props;
87
- const [collapsedIds, collapseAllContext, collapseRowContext] = useToggleIds(rows, persistCollapse);
88
+ // Create a ref that always contains the latest rows, for our effectively-singleton RowState to use
89
+ const rowsRef = (0, react_1.useRef)(rows);
90
+ rowsRef.current = rows;
91
+ const [rowState] = (0, react_1.useState)(() => new RowState_1.RowState(rowsRef, persistCollapse));
88
92
  // We only use this in as=virtual mode, but keep this here for rowLookup to use
89
93
  const virtuosoRef = (0, react_1.useRef)(null);
90
94
  const tableRef = (0, react_1.useRef)(null);
91
95
  if (api) {
92
96
  api.current = {
93
97
  scrollToIndex: (index) => virtuosoRef.current && virtuosoRef.current.scrollToIndex(index),
98
+ getSelectedRowIds: () => rowState.selectedIds,
94
99
  };
95
100
  }
96
101
  const [sortState, setSortKey] = (0, useSortState_1.useSortState)(columns, sorting);
@@ -101,37 +106,9 @@ function GridTable(props) {
101
106
  }
102
107
  return rows;
103
108
  }, [columns, rows, sorting, sortState]);
104
- // Calculate the column sizes immediately rather than via the `debounce` method.
105
- // We do this for Storybook integrations that may use MockDate. MockDate changes the behavior of `new Date()`, which is used by `useDebounce` and essentially turns off the callback.
106
- const calculateImmediately = (0, react_1.useRef)(true);
107
- const [tableWidth, setTableWidth] = (0, react_1.useState)();
108
- // Calc our initial/first render sizes where we won't have a width yet
109
- const [columnSizes, setColumnSizes] = (0, react_1.useState)(calcColumnSizes(columns, (_a = style.nestedCards) === null || _a === void 0 ? void 0 : _a.firstLastColumnWidth, tableWidth, style.minWidthPx));
110
- const setTableAndColumnWidths = (0, react_1.useCallback)((width) => {
111
- var _a;
112
- setTableWidth(width);
113
- setColumnSizes(calcColumnSizes(columns, (_a = style.nestedCards) === null || _a === void 0 ? void 0 : _a.firstLastColumnWidth, width, style.minWidthPx));
114
- }, [setTableWidth, setColumnSizes, columns, style]);
115
- const setTableAndColumnWidthsDebounced = (0, use_debounce_1.useDebouncedCallback)(setTableAndColumnWidths, 100);
116
- const onResize = (0, react_1.useCallback)(() => {
117
- const target = (resizeTarget === null || resizeTarget === void 0 ? void 0 : resizeTarget.current) ? resizeTarget.current : tableRef.current;
118
- if (target && target.clientWidth !== tableWidth) {
119
- if (calculateImmediately.current) {
120
- calculateImmediately.current = false;
121
- setTableAndColumnWidths(target.clientWidth);
122
- }
123
- else {
124
- setTableAndColumnWidthsDebounced(target.clientWidth);
125
- }
126
- }
127
- }, [
128
- resizeTarget === null || resizeTarget === void 0 ? void 0 : resizeTarget.current,
129
- tableRef.current,
130
- setTableAndColumnWidths,
131
- calculateImmediately,
132
- setTableAndColumnWidthsDebounced,
133
- ]);
134
- (0, utils_1.useResizeObserver)({ ref: resizeTarget !== null && resizeTarget !== void 0 ? resizeTarget : tableRef, onResize });
109
+ const columnSizes = (0, columnSizes_1.useSetupColumnSizes)(style, columns, tableRef, resizeTarget);
110
+ // Make a single copy of our current collapsed state, so we'll have a single observer.
111
+ const collapsedIds = (0, hooks_1.useComputed)(() => rowState.collapsedIds, [rowState]);
135
112
  // Filter + flatten + component-ize the sorted rows.
136
113
  let [headerRows, filteredRows] = (0, react_1.useMemo)(() => {
137
114
  // Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
@@ -141,19 +118,19 @@ function GridTable(props) {
141
118
  // changes, and so by not passing the sortProps, it means the data rows' React.memo will still cache them.
142
119
  const sortProps = row.kind === "header" ? { sorting, sortState, setSortKey } : { sorting };
143
120
  const RowComponent = observeRows ? ObservedGridRow : MemoizedGridRow;
144
- return ((0, jsx_runtime_1.jsx)(exports.GridCollapseContext.Provider, Object.assign({ value: row.kind === "header" ? collapseAllContext : collapseRowContext }, { children: (0, jsx_runtime_1.jsx)(RowComponent, Object.assign({}, {
145
- as,
146
- columns,
147
- row,
148
- style,
149
- rowStyles,
150
- stickyHeader,
151
- stickyOffset,
152
- openCards: nestedCards ? nestedCards.currentOpenCards() : undefined,
153
- columnSizes,
154
- level,
155
- ...sortProps,
156
- }), void 0) }), `${row.kind}-${row.id}`));
121
+ return ((0, jsx_runtime_1.jsx)(RowComponent, Object.assign({}, {
122
+ as,
123
+ columns,
124
+ row,
125
+ style,
126
+ rowStyles,
127
+ stickyHeader,
128
+ stickyOffset,
129
+ openCards: nestedCards ? nestedCards.currentOpenCards() : undefined,
130
+ columnSizes,
131
+ level,
132
+ ...sortProps,
133
+ }), `${row.kind}-${row.id}`));
157
134
  }
158
135
  // Split out the header rows from the data rows so that we can put an `infoMessage` in between them (if needed).
159
136
  const headerRows = [];
@@ -166,8 +143,8 @@ function GridTable(props) {
166
143
  const matches = filters.length === 0 ||
167
144
  row.pin ||
168
145
  filters.every((filter) => columns.map((c) => applyRowFn(c, row)).some((maybeContent) => matchesFilter(maybeContent, filter)));
169
- // Even if we don't pass the filter, one of our children might, so we continue on after this check
170
146
  let isCard = false;
147
+ // Even if we don't pass the filter, one of our children might, so we continue on after this check
171
148
  if (matches) {
172
149
  isCard = nestedCards && nestedCards.maybeOpenCard(row);
173
150
  filteredRows.push([row, makeRowComponent(row, level)]);
@@ -206,11 +183,9 @@ function GridTable(props) {
206
183
  sortState,
207
184
  stickyHeader,
208
185
  stickyOffset,
209
- collapsedIds,
210
- collapseAllContext,
211
- collapseRowContext,
212
186
  observeRows,
213
187
  columnSizes,
188
+ collapsedIds,
214
189
  ]);
215
190
  let tooManyClientSideRows = false;
216
191
  if (filterMaxRows && filteredRows.length > filterMaxRows) {
@@ -228,8 +203,8 @@ function GridTable(props) {
228
203
  }, [filteredRows === null || filteredRows === void 0 ? void 0 : filteredRows.length, setRowCount]);
229
204
  const noData = filteredRows.length === 0;
230
205
  const firstRowMessage = (noData && fallbackMessage) || (tooManyClientSideRows && "Hiding some rows, use filter...") || infoMessage;
231
- const borderless = (_b = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _b === void 0 ? void 0 : _b.borderless;
232
- const typeScale = (_c = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _c === void 0 ? void 0 : _c.typeScale;
206
+ const borderless = (_a = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _a === void 0 ? void 0 : _a.borderless;
207
+ const typeScale = (_b = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _b === void 0 ? void 0 : _b.typeScale;
233
208
  const fieldProps = (0, react_1.useMemo)(() => ({
234
209
  hideLabel: true,
235
210
  numberAlignment: "right",
@@ -243,7 +218,8 @@ function GridTable(props) {
243
218
  // just trust the GridTable impl that, at runtime, `as=virtual` will (other than being virtualized)
244
219
  // behave semantically the same as `as=div` did for its tests.
245
220
  const _as = as === "virtual" && runningInJest ? "div" : as;
246
- return ((0, jsx_runtime_1.jsx)(PresentationContext_1.PresentationProvider, Object.assign({ fieldProps: fieldProps, wrap: (_d = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _d === void 0 ? void 0 : _d.wrap }, { children: renders[_as](style, id, columns, headerRows, filteredRows, firstRowMessage, stickyHeader, (_e = style.nestedCards) === null || _e === void 0 ? void 0 : _e.firstLastColumnWidth, xss, virtuosoRef, tableRef) }), void 0));
221
+ const rowStateContext = (0, react_1.useMemo)(() => ({ rowState }), [rowState]);
222
+ return ((0, jsx_runtime_1.jsx)(RowState_1.RowStateContext.Provider, Object.assign({ value: rowStateContext }, { children: (0, jsx_runtime_1.jsx)(PresentationContext_1.PresentationProvider, Object.assign({ fieldProps: fieldProps, wrap: (_c = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _c === void 0 ? void 0 : _c.wrap }, { children: renders[_as](style, id, columns, headerRows, filteredRows, firstRowMessage, stickyHeader, (_d = style.nestedCards) === null || _d === void 0 ? void 0 : _d.firstLastColumnWidth, xss, virtuosoRef, tableRef) }), void 0) }), void 0));
247
223
  }
248
224
  exports.GridTable = GridTable;
249
225
  // Determine which HTML element to use to build the GridTable
@@ -494,7 +470,7 @@ function GridRow(props) {
494
470
  // Decrement colspan count and skip if greater than 1.
495
471
  if (currentColspan > 1) {
496
472
  currentColspan -= 1;
497
- return;
473
+ return null;
498
474
  }
499
475
  const maybeContent = applyRowFn(column, row);
500
476
  currentColspan = isGridCellContent(maybeContent) ? (_a = maybeContent.colspan) !== null && _a !== void 0 ? _a : 1 : 1;
@@ -624,11 +600,6 @@ const defaultRenderFn = (as) => (key, css, content) => {
624
600
  const Cell = as === "table" ? "td" : "div";
625
601
  return ((0, jsx_runtime_1.jsx)(Cell, Object.assign({ css: { ...css, ...tableRowStyles(as) } }, { children: content }), key));
626
602
  };
627
- exports.GridCollapseContext = react_1.default.createContext({
628
- headerCollapsed: false,
629
- isCollapsed: () => false,
630
- toggleCollapsed: () => { },
631
- });
632
603
  /** Sets up the `GridContext` so that header cells can access the current sort settings. */
633
604
  const headerRenderFn = (columns, column, sortState, setSortKey, as) => (key, css, content) => {
634
605
  const [currentKey, direction] = sortState || [];
@@ -710,102 +681,8 @@ exports.matchesFilter = matchesFilter;
710
681
  function maybeDarken(color, defaultColor) {
711
682
  return color ? (0, tinycolor2_1.default)(color).darken(4).toString() : defaultColor;
712
683
  }
713
- // Get the rows that are already in the toggled state, so we can keep them toggled
714
- function getCollapsedRows(persistCollapse) {
715
- if (!persistCollapse)
716
- return [];
717
- const collapsedGridRowIds = localStorage.getItem(persistCollapse);
718
- return collapsedGridRowIds ? JSON.parse(collapsedGridRowIds) : [];
719
- }
720
- /**
721
- * A custom hook to manage a list of ids.
722
- *
723
- * What's special about this hook is that we manage a stable identity
724
- * for the `toggleId` function, so that rows that have _not_ toggled
725
- * themselves on/off will have an unchanged callback and so not be
726
- * re-rendered.
727
- *
728
- * That said, when they do trigger a `toggleId`, the stable/"stale" callback
729
- * function should see/update the latest list of values, which is not possible with a
730
- * traditional `useState` hook because it captures the original/stale list identity.
731
- */
732
- function useToggleIds(rows, persistCollapse) {
733
- // Make a list that we will only mutate, so that our callbacks have a stable identity.
734
- const [collapsedIds] = (0, react_1.useState)(getCollapsedRows(persistCollapse));
735
- // Use this to trigger the component to re-render even though we're not calling `setList`
736
- const [tick, setTick] = (0, react_1.useState)("");
737
- // Checking whether something is collapsed does not depend on anything
738
- const isCollapsed = (0, react_1.useCallback)((id) => collapsedIds.includes(id),
739
- // eslint-disable-next-line react-hooks/exhaustive-deps
740
- []);
741
- const collapseAllContext = (0, react_1.useMemo)(() => {
742
- // Create the stable `toggleCollapsed`, i.e. we are purposefully passing an (almost) empty dep list
743
- // Since only toggling all rows required knowledge of what the rows are
744
- const toggleAll = (_id) => {
745
- // We have different behavior when going from expand/collapse all.
746
- const isAllCollapsed = collapsedIds[0] === "header";
747
- collapsedIds.splice(0, collapsedIds.length);
748
- if (isAllCollapsed) {
749
- // Expand all means keep `collapsedIds` empty
750
- }
751
- else {
752
- // Otherwise push `header` on the list as a hint that we're in the collapsed-all state
753
- collapsedIds.push("header");
754
- // Find all non-leaf rows so that toggling "all collapsed" -> "all not collapsed" opens
755
- // the parent rows of any level.
756
- const parentIds = new Set();
757
- const todo = [...rows];
758
- while (todo.length > 0) {
759
- const r = todo.pop();
760
- if (r.children) {
761
- parentIds.add(r.id);
762
- todo.push(...r.children);
763
- }
764
- }
765
- // And then mark all parent rows as collapsed.
766
- collapsedIds.push(...parentIds);
767
- }
768
- if (persistCollapse) {
769
- localStorage.setItem(persistCollapse, JSON.stringify(collapsedIds));
770
- }
771
- // Trigger a re-render
772
- setTick(collapsedIds.join(","));
773
- };
774
- return { headerCollapsed: isCollapsed("header"), isCollapsed, toggleCollapsed: toggleAll };
775
- },
776
- // eslint-disable-next-line react-hooks/exhaustive-deps
777
- [rows]);
778
- const collapseRowContext = (0, react_1.useMemo)(() => {
779
- // Create the stable `toggleCollapsed`, i.e. we are purposefully passing an empty dep list
780
- // Since toggling a single row does not need to know about the other rows
781
- const toggleRow = (id) => {
782
- // This is the regular/non-header behavior to just add/remove the individual row id
783
- const i = collapsedIds.indexOf(id);
784
- if (i === -1) {
785
- collapsedIds.push(id);
786
- }
787
- else {
788
- collapsedIds.splice(i, 1);
789
- }
790
- if (persistCollapse) {
791
- localStorage.setItem(persistCollapse, JSON.stringify(collapsedIds));
792
- }
793
- // Trigger a re-render
794
- setTick(collapsedIds.join(","));
795
- };
796
- return { headerCollapsed: isCollapsed("header"), isCollapsed, toggleCollapsed: toggleRow };
797
- },
798
- // eslint-disable-next-line react-hooks/exhaustive-deps
799
- [collapseAllContext.isCollapsed("header")]);
800
- // Return a copy of the list, b/c we want external useMemos that do explicitly use the
801
- // entire list as a dep to re-render whenever the list is changed (which they need to
802
- // see as new list identity).
803
- // eslint-disable-next-line react-hooks/exhaustive-deps
804
- const copy = (0, react_1.useMemo)(() => [...collapsedIds], [tick, collapsedIds]);
805
- return [copy, collapseAllContext, collapseRowContext];
806
- }
807
- /** GridTable as Table utility to apply <tr> element override styles */
808
- const tableRowStyles = (as, column) => {
684
+ /** GridTable as Table utility to apply <tr> element override styles. */
685
+ function tableRowStyles(as, column) {
809
686
  const thWidth = column === null || column === void 0 ? void 0 : column.w;
810
687
  return as === "table"
811
688
  ? {
@@ -813,4 +690,4 @@ const tableRowStyles = (as, column) => {
813
690
  ...(thWidth ? Css_1.Css.w(thWidth).$ : {}),
814
691
  }
815
692
  : {};
816
- };
693
+ }
@@ -0,0 +1,38 @@
1
+ import React, { MutableRefObject } from "react";
2
+ import { GridDataRow } from "./GridTable";
3
+ export declare type SelectedState = "checked" | "unchecked" | "partial";
4
+ /**
5
+ * Stores the collapsed & selected state of rows.
6
+ *
7
+ * I.e. this implements "collapse parent" --> "hides children", and
8
+ * "select parent" --> "select parent + children".
9
+ *
10
+ * There should be a single, stable `RowStateStore` instance per `GridTable`, so
11
+ * that children don't have to re-render even as we incrementally add/remove rows
12
+ * to the table (i.e. the top-level rows identity changes, but each row within it
13
+ * may not).
14
+ *
15
+ * We use mobx ObservableSets/ObservableMaps to drive granular re-rendering of rows/cells
16
+ * that need to change their toggle/select on/off in response to parent/child
17
+ * changes.
18
+ */
19
+ export declare class RowState {
20
+ private rows;
21
+ private persistCollapse;
22
+ private readonly collapsedRows;
23
+ private readonly selectedRows;
24
+ /**
25
+ * Creates the `RowState` for a given `GridTable`.
26
+ */
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;
31
+ get collapsedIds(): string[];
32
+ isCollapsed(id: string): boolean;
33
+ toggleCollapsed(id: string): void;
34
+ }
35
+ /** Provides a context for rows to access their table's `RowState`. */
36
+ export declare const RowStateContext: React.Context<{
37
+ rowState: RowState;
38
+ }>;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RowStateContext = exports.RowState = void 0;
7
+ const mobx_1 = require("mobx");
8
+ const react_1 = __importDefault(require("react"));
9
+ /**
10
+ * Stores the collapsed & selected state of rows.
11
+ *
12
+ * I.e. this implements "collapse parent" --> "hides children", and
13
+ * "select parent" --> "select parent + children".
14
+ *
15
+ * There should be a single, stable `RowStateStore` instance per `GridTable`, so
16
+ * that children don't have to re-render even as we incrementally add/remove rows
17
+ * to the table (i.e. the top-level rows identity changes, but each row within it
18
+ * may not).
19
+ *
20
+ * We use mobx ObservableSets/ObservableMaps to drive granular re-rendering of rows/cells
21
+ * that need to change their toggle/select on/off in response to parent/child
22
+ * changes.
23
+ */
24
+ class RowState {
25
+ /**
26
+ * Creates the `RowState` for a given `GridTable`.
27
+ */
28
+ constructor(rows, persistCollapse) {
29
+ this.rows = rows;
30
+ this.persistCollapse = persistCollapse;
31
+ this.selectedRows = new mobx_1.ObservableMap();
32
+ this.collapsedRows = new mobx_1.ObservableSet(persistCollapse ? readLocalCollapseState(persistCollapse) : []);
33
+ // Make ourselves an observable so that mobx will do caching of .collapseIds so
34
+ // that it'll be a stable identity for GridTable to useMemo against.
35
+ (0, mobx_1.makeAutoObservable)(this, { rows: false }); // as any b/c rows is private, so the mapped type doesn't see it
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
+ }
93
+ get collapsedIds() {
94
+ return [...this.collapsedRows.values()];
95
+ }
96
+ // Should be called in an Observer/useComputed to trigger re-renders
97
+ isCollapsed(id) {
98
+ return this.collapsedRows.has(id) || this.collapsedRows.has("header");
99
+ }
100
+ toggleCollapsed(id) {
101
+ const collapsedIds = [...this.collapsedRows.values()];
102
+ // We have different behavior when going from expand/collapse all.
103
+ if (id === "header") {
104
+ const isAllCollapsed = collapsedIds[0] === "header";
105
+ if (isAllCollapsed) {
106
+ // Expand all means keep `collapsedIds` empty
107
+ collapsedIds.splice(0, collapsedIds.length);
108
+ }
109
+ else {
110
+ // Otherwise push `header` on the list as a hint that we're in the collapsed-all state
111
+ collapsedIds.push("header");
112
+ // Find all non-leaf rows so that toggling "all collapsed" -> "all not collapsed" opens
113
+ // the parent rows of any level.
114
+ const parentIds = new Set();
115
+ const todo = [...this.rows.current];
116
+ while (todo.length > 0) {
117
+ const r = todo.pop();
118
+ if (r.children) {
119
+ parentIds.add(r.id);
120
+ todo.push(...r.children);
121
+ }
122
+ }
123
+ // And then mark all parent rows as collapsed.
124
+ collapsedIds.push(...parentIds);
125
+ }
126
+ }
127
+ else {
128
+ // This is the regular/non-header behavior to just add/remove the individual row id
129
+ const i = collapsedIds.indexOf(id);
130
+ if (i === -1) {
131
+ collapsedIds.push(id);
132
+ }
133
+ else {
134
+ collapsedIds.splice(i, 1);
135
+ }
136
+ }
137
+ this.collapsedRows.replace(collapsedIds);
138
+ if (this.persistCollapse) {
139
+ localStorage.setItem(this.persistCollapse, JSON.stringify(collapsedIds));
140
+ }
141
+ }
142
+ }
143
+ exports.RowState = RowState;
144
+ /** Provides a context for rows to access their table's `RowState`. */
145
+ exports.RowStateContext = react_1.default.createContext({
146
+ get rowState() {
147
+ throw new Error("No RowStateContext provider");
148
+ },
149
+ });
150
+ // Get the rows that are already in the toggled state, so we can keep them toggled
151
+ function readLocalCollapseState(persistCollapse) {
152
+ const collapsedGridRowIds = localStorage.getItem(persistCollapse);
153
+ return collapsedGridRowIds ? JSON.parse(collapsedGridRowIds) : [];
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
+ }
@@ -0,0 +1,22 @@
1
+ import { MutableRefObject } from "react";
2
+ import { GridColumn, GridStyle } from "./GridTable";
3
+ /**
4
+ * Calculates an array of sizes for each of our columns.
5
+ *
6
+ * We originally supported CSS grid-template-column definitions which allowed fancier,
7
+ * dynamic/content-based widths, but have eventually dropped it mainly due to:
8
+ *
9
+ * 1. In virtual tables, a) the table never has all of the rows in DOM at a single time,
10
+ * so any "content-based" widths will change as you scroll the table, which is weird, and
11
+ * b) a sticky header and rows are put in different DOM parent elements by react-virtuoso,
12
+ * so wouldn't arrive at the same "content-based" widths.
13
+ *
14
+ * 2. Using CSS grid but still have a row-level div for hover/focus targeting required
15
+ * a "fake" `display: contents` div that couldn't have actually any styles applied to it.
16
+ *
17
+ * So we've just got with essentially fixed/deterministic widths, i.e. `px` or `percent` or
18
+ * `fr`.
19
+ *
20
+ * Disclaimer that we roll our own `fr` b/c we're not in CSS grid anymore.
21
+ */
22
+ export declare function useSetupColumnSizes(style: GridStyle, columns: GridColumn<any>[], tableRef: MutableRefObject<HTMLElement | null>, resizeTarget: MutableRefObject<HTMLElement | null> | undefined): string[];
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useSetupColumnSizes = void 0;
4
+ const utils_1 = require("@react-aria/utils");
5
+ const react_1 = require("react");
6
+ const GridTable_1 = require("./GridTable");
7
+ const use_debounce_1 = require("use-debounce");
8
+ /**
9
+ * Calculates an array of sizes for each of our columns.
10
+ *
11
+ * We originally supported CSS grid-template-column definitions which allowed fancier,
12
+ * dynamic/content-based widths, but have eventually dropped it mainly due to:
13
+ *
14
+ * 1. In virtual tables, a) the table never has all of the rows in DOM at a single time,
15
+ * so any "content-based" widths will change as you scroll the table, which is weird, and
16
+ * b) a sticky header and rows are put in different DOM parent elements by react-virtuoso,
17
+ * so wouldn't arrive at the same "content-based" widths.
18
+ *
19
+ * 2. Using CSS grid but still have a row-level div for hover/focus targeting required
20
+ * a "fake" `display: contents` div that couldn't have actually any styles applied to it.
21
+ *
22
+ * So we've just got with essentially fixed/deterministic widths, i.e. `px` or `percent` or
23
+ * `fr`.
24
+ *
25
+ * Disclaimer that we roll our own `fr` b/c we're not in CSS grid anymore.
26
+ */
27
+ function useSetupColumnSizes(style, columns, tableRef, resizeTarget) {
28
+ var _a, _b;
29
+ // Calculate the column sizes immediately rather than via the `debounce` method.
30
+ // We do this for Storybook integrations that may use MockDate. MockDate changes the behavior of `new Date()`,
31
+ // which is used internally by `useDebounce`, so the frozen clock means the callback is never called.
32
+ const calculateImmediately = (0, react_1.useRef)(true);
33
+ const [tableWidth, setTableWidth] = (0, react_1.useState)();
34
+ // Calc our initial/first render sizes where we won't have a width yet
35
+ const [columnSizes, setColumnSizes] = (0, react_1.useState)(
36
+ // TODO Add a useEffect to re-calc this on change
37
+ (0, GridTable_1.calcColumnSizes)(columns, (_a = style.nestedCards) === null || _a === void 0 ? void 0 : _a.firstLastColumnWidth, tableWidth, style.minWidthPx));
38
+ const setTableAndColumnWidths = (0, react_1.useCallback)((width) => {
39
+ var _a;
40
+ setTableWidth(width);
41
+ setColumnSizes((0, GridTable_1.calcColumnSizes)(columns, (_a = style.nestedCards) === null || _a === void 0 ? void 0 : _a.firstLastColumnWidth, width, style.minWidthPx));
42
+ }, [setTableWidth, setColumnSizes, columns, style]);
43
+ const setTableAndColumnWidthsDebounced = (0, use_debounce_1.useDebouncedCallback)(setTableAndColumnWidths, 100);
44
+ const target = (_b = resizeTarget === null || resizeTarget === void 0 ? void 0 : resizeTarget.current) !== null && _b !== void 0 ? _b : tableRef.current;
45
+ const onResize = (0, react_1.useCallback)(() => {
46
+ if (target && target.clientWidth !== tableWidth) {
47
+ if (calculateImmediately.current) {
48
+ calculateImmediately.current = false;
49
+ setTableAndColumnWidths(target.clientWidth);
50
+ }
51
+ else {
52
+ setTableAndColumnWidthsDebounced(target.clientWidth);
53
+ }
54
+ }
55
+ }, [target, tableWidth, setTableAndColumnWidths, setTableAndColumnWidthsDebounced]);
56
+ (0, utils_1.useResizeObserver)({ ref: resizeTarget !== null && resizeTarget !== void 0 ? resizeTarget : tableRef, onResize });
57
+ return columnSizes;
58
+ }
59
+ exports.useSetupColumnSizes = useSetupColumnSizes;
@@ -3,7 +3,8 @@ export * from "./columns";
3
3
  export type { GridRowLookup } from "./GridRowLookup";
4
4
  export { GridSortContext } from "./GridSortContext";
5
5
  export { ASC, DESC, GridTable, setDefaultStyle, setGridTableDefaults } from "./GridTable";
6
- export type { Direction, GridCellAlignment, GridCellContent, GridCollapseContext, GridColumn, GridDataRow, GridRowStyles, GridSortConfig, GridStyle, GridTableDefaults, GridTableProps, GridTableXss, Kinded, RowStyle, setRunningInJest, } from "./GridTable";
6
+ export type { Direction, GridCellAlignment, GridCellContent, GridColumn, GridDataRow, GridRowStyles, GridSortConfig, GridStyle, GridTableDefaults, GridTableProps, GridTableXss, Kinded, RowStyle, setRunningInJest, } from "./GridTable";
7
+ export { RowState, RowStateContext } from "./RowState";
7
8
  export { simpleDataRows, simpleHeader, simpleRows } from "./simpleHelpers";
8
9
  export type { SimpleHeaderAndDataOf, SimpleHeaderAndDataWith } from "./simpleHelpers";
9
10
  export { SortHeader } from "./SortHeader";
@@ -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.defaultStyle = exports.condensedStyle = exports.cardStyle = exports.beamTotalsFlexibleStyle = exports.beamTotalsFixedStyle = exports.beamNestedFlexibleStyle = exports.beamNestedFixedStyle = exports.beamFlexibleStyle = exports.beamFixedStyle = exports.SortHeader = exports.simpleRows = exports.simpleHeader = exports.simpleDataRows = exports.setGridTableDefaults = exports.setDefaultStyle = exports.GridTable = exports.DESC = exports.ASC = exports.GridSortContext = void 0;
13
+ exports.defaultStyle = exports.condensedStyle = exports.cardStyle = exports.beamTotalsFlexibleStyle = exports.beamTotalsFixedStyle = exports.beamNestedFlexibleStyle = exports.beamNestedFixedStyle = exports.beamFlexibleStyle = exports.beamFixedStyle = exports.SortHeader = exports.simpleRows = exports.simpleHeader = exports.simpleDataRows = exports.RowStateContext = exports.RowState = exports.setGridTableDefaults = exports.setDefaultStyle = exports.GridTable = exports.DESC = exports.ASC = exports.GridSortContext = void 0;
14
14
  __exportStar(require("./CollapseToggle"), exports);
15
15
  __exportStar(require("./columns"), exports);
16
16
  var GridSortContext_1 = require("./GridSortContext");
@@ -21,6 +21,9 @@ Object.defineProperty(exports, "DESC", { enumerable: true, get: function () { re
21
21
  Object.defineProperty(exports, "GridTable", { enumerable: true, get: function () { return GridTable_1.GridTable; } });
22
22
  Object.defineProperty(exports, "setDefaultStyle", { enumerable: true, get: function () { return GridTable_1.setDefaultStyle; } });
23
23
  Object.defineProperty(exports, "setGridTableDefaults", { enumerable: true, get: function () { return GridTable_1.setGridTableDefaults; } });
24
+ var RowState_1 = require("./RowState");
25
+ Object.defineProperty(exports, "RowState", { enumerable: true, get: function () { return RowState_1.RowState; } });
26
+ Object.defineProperty(exports, "RowStateContext", { enumerable: true, get: function () { return RowState_1.RowStateContext; } });
24
27
  var simpleHelpers_1 = require("./simpleHelpers");
25
28
  Object.defineProperty(exports, "simpleDataRows", { enumerable: true, get: function () { return simpleHelpers_1.simpleDataRows; } });
26
29
  Object.defineProperty(exports, "simpleHeader", { enumerable: true, get: function () { return simpleHelpers_1.simpleHeader; } });
@@ -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
  }
@@ -25,7 +25,7 @@ require("trix/dist/trix.css");
25
25
  function RichTextField(props) {
26
26
  const { mergeTags, label, value = "", onChange, onBlur = utils_1.noop, onFocus = utils_1.noop, readOnly } = props;
27
27
  // We get a reference to the Editor instance after trix-init fires
28
- const editor = (0, react_2.useRef)(undefined);
28
+ const [editor, setEditor] = (0, react_2.useState)();
29
29
  const editorElement = (0, react_2.useRef)();
30
30
  // Keep track of what we pass to onChange, so that we can make ourselves keep looking
31
31
  // like a controlled input, i.e. by only calling loadHTML if a new incoming `value` !== `currentHtml`,
@@ -48,12 +48,13 @@ function RichTextField(props) {
48
48
  const targetEl = e.target;
49
49
  if (targetEl.id === id) {
50
50
  editorElement.current = targetEl;
51
- editor.current = editorElement.current.editor;
51
+ const editor = editorElement.current.editor;
52
+ setEditor(editor);
52
53
  if (mergeTags !== undefined) {
53
54
  attachTributeJs(mergeTags, editorElement.current);
54
55
  }
55
56
  currentHtml.current = value;
56
- editor.current.loadHTML(value || "");
57
+ editor.loadHTML(value || "");
57
58
  // Remove listener once we've initialized
58
59
  window.removeEventListener("trix-initialize", onEditorInit);
59
60
  function trixChange(e) {
@@ -88,10 +89,10 @@ function RichTextField(props) {
88
89
  }, []);
89
90
  (0, react_2.useEffect)(() => {
90
91
  // If our value prop changes (without the change coming from us), reload it
91
- if (!readOnly && editor.current && value !== currentHtml.current) {
92
- editor.current.loadHTML(value || "");
92
+ if (!readOnly && editor && value !== currentHtml.current) {
93
+ editor.loadHTML(value || "");
93
94
  }
94
- }, [value, readOnly]);
95
+ }, [value, readOnly, editor]);
95
96
  const { placeholder, autoFocus } = props;
96
97
  if (!readOnly) {
97
98
  return ((0, jsx_runtime_1.jsxs)("div", Object.assign({ css: Css_1.Css.w100.maxw("550px").$ }, { children: [label && (0, jsx_runtime_1.jsx)(Label_1.Label, { labelProps: {}, label: label }, void 0), (0, jsx_runtime_1.jsxs)("div", Object.assign({ css: { ...Css_1.Css.br4.bgWhite.$, ...trixCssOverrides } }, { children: [(0, jsx_runtime_1.jsx)("input", { type: "hidden", id: `input-${id}`, value: value }, void 0), (0, react_2.createElement)("trix-editor", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.106.2",
3
+ "version": "2.107.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",