@purpurds/table 8.14.0 → 8.16.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.
@@ -180,7 +180,7 @@ describe("Data Table", () => {
180
180
  });
181
181
 
182
182
  describe("Expand table", () => {
183
- it("should have an expand button", async () => {
183
+ it("should have an expand button, handle its own state, and default to false", async () => {
184
184
  const onToggleExpandMock = vi.fn();
185
185
 
186
186
  render(
@@ -191,9 +191,11 @@ describe("Data Table", () => {
191
191
  toolbarCopy={{
192
192
  buttons: {
193
193
  expand: "Expand",
194
+ minimize: "Minimize",
194
195
  },
195
196
  ariaLabels: {
196
197
  expand: "Expand table",
198
+ minimize: "Minimize table",
197
199
  },
198
200
  }}
199
201
  onToggleExpand={onToggleExpandMock}
@@ -202,8 +204,295 @@ describe("Data Table", () => {
202
204
 
203
205
  const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
204
206
 
207
+ // First click should expand (pass true)
205
208
  await userEvent.click(expandButton);
206
- expect(onToggleExpandMock).toHaveBeenCalled();
209
+ expect(onToggleExpandMock).toHaveBeenCalledWith(true);
210
+
211
+ // Second click should minimize (pass false)
212
+ await userEvent.click(expandButton);
213
+ expect(onToggleExpandMock).toHaveBeenCalledWith(false);
214
+ });
215
+
216
+ describe("with defaultExpanded prop, without controlledExpanded prop", () => {
217
+ it("should start in expanded state when defaultExpanded is true", () => {
218
+ const onToggleExpandMock = vi.fn();
219
+
220
+ render(
221
+ <Table
222
+ columns={createColumnDefSmall()}
223
+ data={tableDataSmall}
224
+ enableToolbar={true}
225
+ defaultExpanded={true}
226
+ toolbarCopy={{
227
+ buttons: {
228
+ expand: "Expand",
229
+ minimize: "Minimize",
230
+ },
231
+ ariaLabels: {
232
+ expand: "Expand table",
233
+ minimize: "Minimize table",
234
+ },
235
+ }}
236
+ onToggleExpand={onToggleExpandMock}
237
+ />
238
+ );
239
+
240
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
241
+
242
+ // Button should show minimize state
243
+ expect(expandButton).toHaveTextContent("Minimize");
244
+ expect(expandButton).toHaveAttribute("aria-expanded", "true");
245
+ expect(expandButton).toHaveAttribute("aria-label", "Minimize table");
246
+ });
247
+
248
+ it("should start in minimized state when defaultExpanded is false", () => {
249
+ const onToggleExpandMock = vi.fn();
250
+
251
+ render(
252
+ <Table
253
+ columns={createColumnDefSmall()}
254
+ data={tableDataSmall}
255
+ enableToolbar={true}
256
+ defaultExpanded={false}
257
+ toolbarCopy={{
258
+ buttons: {
259
+ expand: "Expand",
260
+ minimize: "Minimize",
261
+ },
262
+ ariaLabels: {
263
+ expand: "Expand table",
264
+ minimize: "Minimize table",
265
+ },
266
+ }}
267
+ onToggleExpand={onToggleExpandMock}
268
+ />
269
+ );
270
+
271
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
272
+
273
+ // Button should show expand state
274
+ expect(expandButton).toHaveTextContent("Expand");
275
+ expect(expandButton).toHaveAttribute("aria-expanded", "false");
276
+ expect(expandButton).toHaveAttribute("aria-label", "Expand table");
277
+ });
278
+
279
+ it("should toggle state internally when defaultExpanded is provided", async () => {
280
+ const onToggleExpandMock = vi.fn();
281
+
282
+ render(
283
+ <Table
284
+ columns={createColumnDefSmall()}
285
+ data={tableDataSmall}
286
+ enableToolbar={true}
287
+ defaultExpanded={false}
288
+ toolbarCopy={{
289
+ buttons: {
290
+ expand: "Expand",
291
+ minimize: "Minimize",
292
+ },
293
+ ariaLabels: {
294
+ expand: "Expand table",
295
+ minimize: "Minimize table",
296
+ },
297
+ }}
298
+ onToggleExpand={onToggleExpandMock}
299
+ />
300
+ );
301
+
302
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
303
+
304
+ // Initial state - collapsed
305
+ expect(expandButton).toHaveTextContent("Expand");
306
+
307
+ // Click to expand
308
+ await userEvent.click(expandButton);
309
+ expect(onToggleExpandMock).toHaveBeenCalledWith(true);
310
+ expect(expandButton).toHaveTextContent("Minimize");
311
+
312
+ // Click to collapse
313
+ await userEvent.click(expandButton);
314
+ expect(onToggleExpandMock).toHaveBeenCalledWith(false);
315
+ expect(expandButton).toHaveTextContent("Expand");
316
+ });
317
+ });
318
+
319
+ describe("with controlledExpanded prop", () => {
320
+ it("should use controlledExpanded value when provided", () => {
321
+ const onToggleExpandMock = vi.fn();
322
+
323
+ const { rerender } = render(
324
+ <Table
325
+ columns={createColumnDefSmall()}
326
+ data={tableDataSmall}
327
+ enableToolbar={true}
328
+ controlledExpanded={false}
329
+ toolbarCopy={{
330
+ buttons: {
331
+ expand: "Expand",
332
+ minimize: "Minimize",
333
+ },
334
+ ariaLabels: {
335
+ expand: "Expand table",
336
+ minimize: "Minimize table",
337
+ },
338
+ }}
339
+ onToggleExpand={onToggleExpandMock}
340
+ />
341
+ );
342
+
343
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
344
+
345
+ // Should be collapsed
346
+ expect(expandButton).toHaveTextContent("Expand");
347
+ expect(expandButton).toHaveAttribute("aria-expanded", "false");
348
+
349
+ // Rerender with expanded state
350
+ rerender(
351
+ <Table
352
+ columns={createColumnDefSmall()}
353
+ data={tableDataSmall}
354
+ enableToolbar={true}
355
+ controlledExpanded={true}
356
+ toolbarCopy={{
357
+ buttons: {
358
+ expand: "Expand",
359
+ minimize: "Minimize",
360
+ },
361
+ ariaLabels: {
362
+ expand: "Expand table",
363
+ minimize: "Minimize table",
364
+ },
365
+ }}
366
+ onToggleExpand={onToggleExpandMock}
367
+ />
368
+ );
369
+
370
+ // Should be expanded
371
+ expect(expandButton).toHaveTextContent("Minimize");
372
+ expect(expandButton).toHaveAttribute("aria-expanded", "true");
373
+ });
374
+
375
+ it("should not manage state internally when controlledExpanded is provided", async () => {
376
+ const onToggleExpandMock = vi.fn();
377
+
378
+ render(
379
+ <Table
380
+ columns={createColumnDefSmall()}
381
+ data={tableDataSmall}
382
+ enableToolbar={true}
383
+ controlledExpanded={false}
384
+ toolbarCopy={{
385
+ buttons: {
386
+ expand: "Expand",
387
+ minimize: "Minimize",
388
+ },
389
+ ariaLabels: {
390
+ expand: "Expand table",
391
+ minimize: "Minimize table",
392
+ },
393
+ }}
394
+ onToggleExpand={onToggleExpandMock}
395
+ />
396
+ );
397
+
398
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
399
+
400
+ // Initial state - collapsed
401
+ expect(expandButton).toHaveTextContent("Expand");
402
+
403
+ // Click should trigger callback but not change internal state
404
+ await userEvent.click(expandButton);
405
+ expect(onToggleExpandMock).toHaveBeenCalledWith(true);
406
+
407
+ // Button should remain in collapsed state since parent didn't update controlledExpanded
408
+ expect(expandButton).toHaveTextContent("Expand");
409
+ });
410
+
411
+ it("should ignore defaultExpanded when controlledExpanded is provided", () => {
412
+ const onToggleExpandMock = vi.fn();
413
+
414
+ render(
415
+ <Table
416
+ columns={createColumnDefSmall()}
417
+ data={tableDataSmall}
418
+ enableToolbar={true}
419
+ controlledExpanded={false}
420
+ defaultExpanded={true}
421
+ toolbarCopy={{
422
+ buttons: {
423
+ expand: "Expand",
424
+ minimize: "Minimize",
425
+ },
426
+ ariaLabels: {
427
+ expand: "Expand table",
428
+ minimize: "Minimize table",
429
+ },
430
+ }}
431
+ onToggleExpand={onToggleExpandMock}
432
+ />
433
+ );
434
+
435
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
436
+
437
+ // Should use controlledExpanded (false) instead of defaultExpanded (true)
438
+ expect(expandButton).toHaveTextContent("Expand");
439
+ expect(expandButton).toHaveAttribute("aria-expanded", "false");
440
+ });
441
+
442
+ it("should handle toggling between expanded states with controlled prop", () => {
443
+ const onToggleExpandMock = vi.fn();
444
+
445
+ const { rerender } = render(
446
+ <Table
447
+ columns={createColumnDefSmall()}
448
+ data={tableDataSmall}
449
+ enableToolbar={true}
450
+ controlledExpanded={true}
451
+ toolbarCopy={{
452
+ buttons: {
453
+ expand: "Expand",
454
+ minimize: "Minimize",
455
+ },
456
+ ariaLabels: {
457
+ expand: "Expand table",
458
+ minimize: "Minimize table",
459
+ },
460
+ }}
461
+ onToggleExpand={onToggleExpandMock}
462
+ />
463
+ );
464
+
465
+ const expandButton = screen.getByTestId(Selectors.TOOLBAR.EXPAND_BUTTON);
466
+
467
+ // Initially expanded
468
+ expect(expandButton).toHaveTextContent("Minimize");
469
+ expect(expandButton).toHaveAttribute("aria-label", "Minimize table");
470
+
471
+ // Update to collapsed
472
+ rerender(
473
+ <Table
474
+ columns={createColumnDefSmall()}
475
+ data={tableDataSmall}
476
+ enableToolbar={true}
477
+ controlledExpanded={false}
478
+ toolbarCopy={{
479
+ buttons: {
480
+ expand: "Expand",
481
+ minimize: "Minimize",
482
+ },
483
+ ariaLabels: {
484
+ expand: "Expand table",
485
+ minimize: "Minimize table",
486
+ },
487
+ }}
488
+ onToggleExpand={onToggleExpandMock}
489
+ />
490
+ );
491
+
492
+ // Should be collapsed
493
+ expect(expandButton).toHaveTextContent("Expand");
494
+ expect(expandButton).toHaveAttribute("aria-label", "Expand table");
495
+ });
207
496
  });
208
497
  });
209
498
 
@@ -217,10 +506,10 @@ describe("Data Table", () => {
217
506
  settingsDrawerCopy={copy.settingsDrawer}
218
507
  toolbarCopy={{
219
508
  buttons: {
220
- settings: "Settings",
509
+ settings: "Table settings",
221
510
  },
222
511
  ariaLabels: {
223
- settings: "Settings",
512
+ settings: "Table settings",
224
513
  },
225
514
  }}
226
515
  />
package/src/table.tsx CHANGED
@@ -141,6 +141,8 @@ export const Table = <TData extends RowData>({
141
141
  getRowId,
142
142
  onExportData,
143
143
  onRowsCountChange,
144
+ controlledExpanded,
145
+ defaultExpanded,
144
146
  onToggleExpand,
145
147
  onPrimaryButtonClick,
146
148
  onSecondaryButtonClick,
@@ -154,6 +156,8 @@ export const Table = <TData extends RowData>({
154
156
  const [isExportDrawerOpen, setExportDrawerIsOpen] = useState(false);
155
157
 
156
158
  // Determine if controlled based on BOTH prop AND handler being provided
159
+ const isExpandedControlled =
160
+ controlledExpanded !== undefined && onToggleExpand !== undefined;
157
161
  const isShowColumnFiltersControlled =
158
162
  controlledShowColumnFilters !== undefined && onShowColumnFiltersChange !== undefined;
159
163
  const isStickyFirstColumnControlled =
@@ -176,6 +180,9 @@ export const Table = <TData extends RowData>({
176
180
  : true;
177
181
 
178
182
  // Internal state - only used when not controlled
183
+ const [expandedInternal, setExpandedInternal] = useState(
184
+ defaultExpanded ?? false
185
+ );
179
186
  const [showColumnFiltersInternal, setShowColumnFiltersInternal] = useState(
180
187
  defaultShowColumnFilters ?? Boolean(props.enableFilters)
181
188
  );
@@ -185,6 +192,9 @@ export const Table = <TData extends RowData>({
185
192
  const [stickyHeadersInternal, setStickyHeadersInternal] = useState(effectiveDefaultStickyHeaders);
186
193
 
187
194
  // Use controlled value if fully controlled, otherwise use internal state
195
+ const expanded = isExpandedControlled
196
+ ? controlledExpanded!
197
+ : expandedInternal;
188
198
  const showColumnFiltersEnabled = isShowColumnFiltersControlled
189
199
  ? controlledShowColumnFilters!
190
200
  : showColumnFiltersInternal;
@@ -196,6 +206,13 @@ export const Table = <TData extends RowData>({
196
206
  : stickyHeadersInternal;
197
207
 
198
208
  // Wrapper functions to handle both controlled and uncontrolled state
209
+ const onToggleExpandInternal = (value: boolean) => {
210
+ if (!isExpandedControlled) {
211
+ setExpandedInternal(value);
212
+ }
213
+ onToggleExpand!(value);
214
+ };
215
+
199
216
  const setShowColumnFiltersEnabled = (value: boolean | ((prev: boolean) => boolean)) => {
200
217
  const newValue = typeof value === "function" ? value(showColumnFiltersEnabled) : value;
201
218
 
@@ -515,11 +532,12 @@ export const Table = <TData extends RowData>({
515
532
  exportDrawerAriaControls={`${uid}-export-drawer`}
516
533
  onSetDrawerIsOpen={setSettingsDrawerIsOpen}
517
534
  onResetColumnFilters={handleResetColumnFilters}
518
- onToggleExpand={onToggleExpand}
535
+ onToggleExpand={onToggleExpandInternal}
519
536
  onExportData={handleExportButton}
520
537
  totalRowCount={toolbarTotalRowCount ?? tanstackTable.getRowCount()}
521
538
  visibleRowCount={tanstackTable.getRowModel().rows.length}
522
539
  toolbarCopy={toolbarCopy}
540
+ expanded={expanded}
523
541
  isSettingsDrawerOpen={isSettingsDrawerOpen}
524
542
  hasExportsDrawer={Array.isArray(exportFormats)}
525
543
  isExportDrawerOpen={isExportDrawerOpen}
@@ -219,7 +219,7 @@ export const copy = {
219
219
  description: "Change general table settings",
220
220
  rearrangeDescription: "Choose how you want to reorder the columns.",
221
221
  buttons: {
222
- rearrange: "Reorder",
222
+ rearrange: "Reorder columns",
223
223
  done: "Done",
224
224
  },
225
225
  ariaLabels: {
@@ -248,12 +248,14 @@ export const copy = {
248
248
  buttons: {
249
249
  clearFilters: "Clear filters",
250
250
  expand: "Expand",
251
+ minimize: "Minimize",
251
252
  export: "Export",
252
- settings: "Settings",
253
+ settings: "Table settings",
253
254
  },
254
255
  ariaLabels: {
255
- clearFilters: "Clear all filters",
256
+ clearFilters: "Clear all table filters",
256
257
  expand: "Expand table",
258
+ minimize: "Minimize table",
257
259
  export: "Open export drawer",
258
260
  settings: "Open settings drawer",
259
261
  },
package/src/types.ts CHANGED
@@ -315,6 +315,8 @@ export type WithoutToolbarProps = {
315
315
  enableToolbar?: false | undefined;
316
316
  exportFormats?: never;
317
317
  onExportData?: never;
318
+ controlledExpanded?: never;
319
+ defaultExpanded?: never;
318
320
  onToggleExpand?: never;
319
321
  settingsDrawerCopy?: never;
320
322
  toolbarCopy?: never;
@@ -342,10 +344,25 @@ type WithFiltersProps = {
342
344
  * Props for enabling an expand button in a data table component.
343
345
  */
344
346
  type WithExpandButtonProps = {
347
+ /**
348
+ * Current state of expansion.
349
+ * Regardless of setting, the consumer must listen to onToggleExpand to change the container size accordingly. The table itself only shows the Expand button differently.
350
+ * If defined (true or false), this is the state the table will be shown in. Consumer must listen to onToggleExpand, and update this to the new value. defaultExpanded will have no effect.
351
+ * If undefined, the table will manage its own state, starting with defaultExpanded.
352
+ */
353
+ controlledExpanded?: boolean;
354
+
355
+ /**
356
+ * Initial state of expansion.
357
+ * Defaults to false (collapsed=minimized) if not provided.
358
+ * No effect if controlledExpanded is provided.
359
+ */
360
+ defaultExpanded?: boolean;
361
+
345
362
  /**
346
363
  * Callback function triggered when toggling the expansion state of rows.
347
364
  */
348
- onToggleExpand: () => void;
365
+ onToggleExpand: (value: boolean) => void;
349
366
 
350
367
  /**
351
368
  * Copy for the toolbar buttons related to expanding rows.
@@ -353,9 +370,11 @@ type WithExpandButtonProps = {
353
370
  toolbarCopy: {
354
371
  buttons: {
355
372
  expand: string;
373
+ minimize: string;
356
374
  };
357
375
  ariaLabels: {
358
376
  expand: string;
377
+ minimize: string;
359
378
  };
360
379
  };
361
380
  };
@@ -408,13 +427,17 @@ type WithoutExportProps = {
408
427
  };
409
428
 
410
429
  type WithoutExpandButtonProps = {
430
+ controlledExpanded?: never;
431
+ defaultExpanded?: never;
411
432
  onToggleExpand?: never;
412
433
  toolbarCopy?: {
413
434
  buttons?: {
414
435
  expand?: never;
436
+ minimize?: never;
415
437
  };
416
438
  ariaLabels?: {
417
439
  expand?: never;
440
+ minimize?: never;
418
441
  };
419
442
  };
420
443
  };