@marimo-team/islands 0.24.1-dev109 → 0.24.1-dev110

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.24.1-dev109",
3
+ "version": "0.24.1-dev110",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -5,7 +5,7 @@ import {
5
5
  getCoreRowModel,
6
6
  useReactTable,
7
7
  } from "@tanstack/react-table";
8
- import { fireEvent, render, screen } from "@testing-library/react";
8
+ import { fireEvent, render, screen, within } from "@testing-library/react";
9
9
  import { beforeAll, describe, expect, it, vi } from "vitest";
10
10
  import { TooltipProvider } from "@/components/ui/tooltip";
11
11
  import { ColumnExplorerPanel } from "../column-explorer-panel/column-explorer";
@@ -46,7 +46,7 @@ function PanelHarness({
46
46
  columns: TEST_COLUMNS,
47
47
  getCoreRowModel: getCoreRowModel(),
48
48
  locale: "en-US",
49
- state: {
49
+ initialState: {
50
50
  columnVisibility: Object.fromEntries(
51
51
  initiallyHidden.map((id) => [id, false]),
52
52
  ),
@@ -76,6 +76,20 @@ function getSearchInput() {
76
76
  return screen.getByPlaceholderText("Search columns...");
77
77
  }
78
78
 
79
+ function getColumnRow(columnName: string): HTMLElement {
80
+ const row = screen.getByText(columnName).closest('[role="option"]');
81
+ if (!row) {
82
+ throw new Error(`No row for column ${columnName}`);
83
+ }
84
+ return row as HTMLElement;
85
+ }
86
+
87
+ function getShowOnlyButton(columnName: string): HTMLElement {
88
+ return within(getColumnRow(columnName)).getByRole("button", {
89
+ name: "Show only this column",
90
+ });
91
+ }
92
+
79
93
  describe("ColumnExplorerPanel search", () => {
80
94
  it("shows all columns when search is empty", () => {
81
95
  renderPanel();
@@ -150,4 +164,49 @@ describe("ColumnExplorerPanel visibility actions", () => {
150
164
  expect(showAll()).toBeEnabled();
151
165
  expect(hideAll()).toBeEnabled();
152
166
  });
167
+
168
+ it("show-only icon isolates one column", () => {
169
+ renderPanel();
170
+ fireEvent.click(getShowOnlyButton("customer_name"));
171
+
172
+ expect(
173
+ within(getColumnRow("customer_name")).getByRole("button", {
174
+ name: "Hide column",
175
+ }),
176
+ ).toBeInTheDocument();
177
+ expect(
178
+ within(getColumnRow("cust_age")).getByRole("button", {
179
+ name: "Show column",
180
+ }),
181
+ ).toBeInTheDocument();
182
+ expect(
183
+ within(getColumnRow("order_total")).getByRole("button", {
184
+ name: "Show column",
185
+ }),
186
+ ).toBeInTheDocument();
187
+ });
188
+
189
+ it("disables show-only icon when the column is already alone", () => {
190
+ renderPanel({
191
+ initiallyHidden: ["cust_age", "order_total"],
192
+ });
193
+ expect(getShowOnlyButton("customer_name")).toHaveAttribute(
194
+ "aria-disabled",
195
+ "true",
196
+ );
197
+ });
198
+
199
+ it("disabled show-only icon does not toggle explorer row expansion", () => {
200
+ renderPanel({
201
+ initiallyHidden: ["cust_age", "order_total"],
202
+ });
203
+ const row = getColumnRow("customer_name");
204
+ expect(row.querySelector(".lucide-chevron-down")).not.toBeNull();
205
+
206
+ const button = getShowOnlyButton("customer_name");
207
+ fireEvent.pointerDown(button, { pointerId: 1, bubbles: true });
208
+ fireEvent.click(button, { bubbles: true });
209
+
210
+ expect(row.querySelector(".lucide-chevron-down")).not.toBeNull();
211
+ });
153
212
  });
@@ -7,6 +7,7 @@ import {
7
7
  } from "@tanstack/react-table";
8
8
  import { fireEvent, render, screen } from "@testing-library/react";
9
9
  import { beforeAll, describe, expect, it } from "vitest";
10
+ import { TooltipProvider } from "@/components/ui/tooltip";
10
11
  import { ColumnVisibilityDropdown } from "../column-visibility-dropdown";
11
12
  import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
12
13
 
@@ -70,11 +71,25 @@ function Harness({ initiallyHidden = [], nonHideable = [] }: HarnessProps) {
70
71
  }
71
72
 
72
73
  function renderAndOpen(props?: HarnessProps) {
73
- const result = render(<Harness {...(props ?? {})} />);
74
+ const result = render(
75
+ <TooltipProvider>
76
+ <Harness {...(props ?? {})} />
77
+ </TooltipProvider>,
78
+ );
74
79
  fireEvent.click(screen.getByTestId("column-visibility-trigger"));
75
80
  return result;
76
81
  }
77
82
 
83
+ function getShowOnlyButton(columnName: string): HTMLElement {
84
+ const button = getColumnOption(columnName).querySelector(
85
+ '[aria-label="Show only this column"]',
86
+ );
87
+ if (!button) {
88
+ throw new Error(`No show-only button for column ${columnName}`);
89
+ }
90
+ return button as HTMLElement;
91
+ }
92
+
78
93
  function getOptionTexts(): string[] {
79
94
  return screen.getAllByRole("option").map((el) => el.textContent ?? "");
80
95
  }
@@ -270,10 +285,124 @@ describe("ColumnVisibilityDropdown", () => {
270
285
  it("offers both bulk actions when matches are mixed", () => {
271
286
  renderAndOpen({ initiallyHidden: ["cust_age"] });
272
287
  fireEvent.change(getSearchInput(), { target: { value: "cust" } });
288
+ expect(screen.getByText(/Show only 2 matching/)).toBeInTheDocument();
273
289
  expect(screen.getByText(/Hide 1 matching/)).toBeInTheDocument();
274
290
  expect(screen.getByText(/Show 1 matching/)).toBeInTheDocument();
275
291
  });
276
292
 
293
+ it("lists show-only before hide and show bulk actions while searching", () => {
294
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
295
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
296
+ expect(getOptionTexts().slice(0, 3)).toEqual([
297
+ "Show only 2 matching",
298
+ "Hide 1 matching",
299
+ "Show 1 matching",
300
+ ]);
301
+ });
302
+
303
+ it("'Show only N matching' isolates matching columns", () => {
304
+ renderAndOpen({ initiallyHidden: ["cust_age"] });
305
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
306
+ fireEvent.click(getColumnOption("Show only 2 matching"));
307
+ fireEvent.change(getSearchInput(), { target: { value: "" } });
308
+
309
+ expect(
310
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
311
+ ).toBeNull();
312
+ expect(
313
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
314
+ ).toBeNull();
315
+ expect(
316
+ getColumnOption("order_total").querySelector(".lucide-eye-off"),
317
+ ).not.toBeNull();
318
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
319
+ expect(getColumnOption("Show only 2 matching")).toHaveAttribute(
320
+ "aria-disabled",
321
+ "true",
322
+ );
323
+ });
324
+
325
+ it("disables 'Show only N matching' when already showing only matches", () => {
326
+ renderAndOpen({
327
+ initiallyHidden: ["order_total"],
328
+ });
329
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
330
+ expect(getColumnOption("Show only 2 matching")).toHaveAttribute(
331
+ "aria-disabled",
332
+ "true",
333
+ );
334
+ });
335
+
336
+ it("omits 'Show only N matching' without search text", () => {
337
+ renderAndOpen();
338
+ expect(
339
+ screen.queryByText(/Show only \d+ matching/),
340
+ ).not.toBeInTheDocument();
341
+ });
342
+
343
+ it("omits search bulk actions when only non-hideable columns match", () => {
344
+ renderAndOpen({ nonHideable: ["customer_name"] });
345
+ fireEvent.change(getSearchInput(), { target: { value: "customer" } });
346
+ expect(screen.getByText("customer_name")).toBeInTheDocument();
347
+ expect(
348
+ screen.queryByText(/Show only \d+ matching/),
349
+ ).not.toBeInTheDocument();
350
+ expect(screen.queryByText(/Hide \d+ matching/)).not.toBeInTheDocument();
351
+ expect(screen.queryByText(/Show \d+ matching/)).not.toBeInTheDocument();
352
+ });
353
+
354
+ it("show-only icon isolates one column without clearing search", () => {
355
+ renderAndOpen();
356
+ fireEvent.change(getSearchInput(), { target: { value: "cust" } });
357
+ fireEvent.click(getShowOnlyButton("customer_name"));
358
+ expect(getSearchInput()).toHaveValue("cust");
359
+ fireEvent.change(getSearchInput(), { target: { value: "" } });
360
+
361
+ expect(
362
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
363
+ ).toBeNull();
364
+ expect(
365
+ getColumnOption("cust_age").querySelector(".lucide-eye-off"),
366
+ ).not.toBeNull();
367
+ expect(
368
+ getColumnOption("order_total").querySelector(".lucide-eye-off"),
369
+ ).not.toBeNull();
370
+ });
371
+
372
+ it("show-only icon does not toggle the row visibility", () => {
373
+ renderAndOpen();
374
+ fireEvent.click(getShowOnlyButton("customer_name"));
375
+
376
+ expect(
377
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
378
+ ).toBeNull();
379
+ });
380
+
381
+ it("disables show-only icon when the column is already alone", () => {
382
+ renderAndOpen({
383
+ initiallyHidden: ["cust_age", "order_total"],
384
+ });
385
+ expect(getShowOnlyButton("customer_name")).toHaveAttribute(
386
+ "aria-disabled",
387
+ "true",
388
+ );
389
+ });
390
+
391
+ it("disabled show-only icon blocks pointer activation of the row", () => {
392
+ renderAndOpen({
393
+ initiallyHidden: ["cust_age", "order_total"],
394
+ });
395
+ const button = getShowOnlyButton("customer_name");
396
+ expect(button).toHaveAttribute("aria-disabled", "true");
397
+
398
+ fireEvent.pointerDown(button, { pointerId: 1, bubbles: true });
399
+ fireEvent.click(button, { bubbles: true });
400
+
401
+ expect(
402
+ getColumnOption("customer_name").querySelector(".lucide-eye-off"),
403
+ ).toBeNull();
404
+ });
405
+
277
406
  it("renders non-hideable columns disabled and without an eye toggle", () => {
278
407
  renderAndOpen({ nonHideable: ["customer_name"] });
279
408
  const option = getColumnOption("customer_name");
@@ -1,8 +1,62 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import {
4
+ type ColumnDef,
5
+ getCoreRowModel,
6
+ useReactTable,
7
+ } from "@tanstack/react-table";
3
8
  import { act, renderHook } from "@testing-library/react";
4
9
  import { describe, expect, it } from "vitest";
5
- import { useColumnVisibility } from "../hooks/use-column-visibility";
10
+ import {
11
+ applyShowOnlyColumns,
12
+ getShowOnlyColumnState,
13
+ getShowOnlyVisibility,
14
+ isShowingOnly,
15
+ isShowingOnlyColumns,
16
+ useColumnVisibility,
17
+ } from "../hooks/use-column-visibility";
18
+ import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "../types";
19
+
20
+ type Row = Record<string, unknown>;
21
+
22
+ const TEST_COLUMNS: ColumnDef<Row>[] = [
23
+ { id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID, enableHiding: false },
24
+ {
25
+ id: INDEX_COLUMN_NAME,
26
+ accessorKey: INDEX_COLUMN_NAME,
27
+ enableHiding: false,
28
+ },
29
+ { id: "customer_name", accessorKey: "customer_name" },
30
+ { id: "cust_age", accessorKey: "cust_age" },
31
+ { id: "order_total", accessorKey: "order_total" },
32
+ ];
33
+
34
+ function createTestTable({
35
+ initiallyHidden = [],
36
+ nonHideable = [],
37
+ }: {
38
+ initiallyHidden?: string[];
39
+ nonHideable?: string[];
40
+ } = {}) {
41
+ const { result } = renderHook(() =>
42
+ useReactTable<Row>({
43
+ data: [],
44
+ columns: TEST_COLUMNS.map((column) =>
45
+ nonHideable.includes(column.id as string)
46
+ ? { ...column, enableHiding: false }
47
+ : column,
48
+ ),
49
+ getCoreRowModel: getCoreRowModel(),
50
+ locale: "en-US",
51
+ initialState: {
52
+ columnVisibility: Object.fromEntries(
53
+ initiallyHidden.map((id) => [id, false]),
54
+ ),
55
+ },
56
+ }),
57
+ );
58
+ return result.current;
59
+ }
6
60
 
7
61
  describe("useColumnVisibility", () => {
8
62
  it("should initialize with correct default values", () => {
@@ -40,3 +94,101 @@ describe("useColumnVisibility", () => {
40
94
  expect(result.current.columnVisibility).toEqual({ a: false, c: false });
41
95
  });
42
96
  });
97
+
98
+ describe("getShowOnlyVisibility", () => {
99
+ it("shows matching hideable columns and hides the rest", () => {
100
+ const table = createTestTable({ initiallyHidden: ["cust_age"] });
101
+
102
+ expect(
103
+ getShowOnlyVisibility(table, ["customer_name", "order_total"]),
104
+ ).toEqual({
105
+ customer_name: true,
106
+ cust_age: false,
107
+ order_total: true,
108
+ });
109
+ });
110
+
111
+ it("ignores non-hideable columns in the input", () => {
112
+ const table = createTestTable({ nonHideable: ["customer_name"] });
113
+
114
+ expect(
115
+ getShowOnlyVisibility(table, [
116
+ SELECT_COLUMN_ID,
117
+ "customer_name",
118
+ "cust_age",
119
+ ]),
120
+ ).toEqual({
121
+ cust_age: true,
122
+ order_total: false,
123
+ });
124
+ });
125
+
126
+ it("hides every hideable column when the input is empty", () => {
127
+ const table = createTestTable();
128
+
129
+ expect(getShowOnlyVisibility(table, [])).toEqual({
130
+ customer_name: false,
131
+ cust_age: false,
132
+ order_total: false,
133
+ });
134
+ });
135
+ });
136
+
137
+ describe("isShowingOnly", () => {
138
+ it("returns true when visible hideable columns match the input", () => {
139
+ const table = createTestTable({
140
+ initiallyHidden: ["cust_age", "order_total"],
141
+ });
142
+
143
+ expect(isShowingOnly(table, ["customer_name"])).toBe(true);
144
+ });
145
+
146
+ it("returns false when extra hideable columns remain visible", () => {
147
+ const table = createTestTable();
148
+
149
+ expect(isShowingOnly(table, ["customer_name"])).toBe(false);
150
+ });
151
+
152
+ it("ignores non-hideable columns in the input", () => {
153
+ const table = createTestTable({
154
+ initiallyHidden: ["cust_age", "order_total"],
155
+ nonHideable: ["customer_name"],
156
+ });
157
+
158
+ expect(isShowingOnly(table, [SELECT_COLUMN_ID, "customer_name"])).toBe(
159
+ true,
160
+ );
161
+ });
162
+
163
+ it("returns true when every hideable column is hidden and the input is empty", () => {
164
+ const table = createTestTable({
165
+ initiallyHidden: ["customer_name", "cust_age", "order_total"],
166
+ });
167
+
168
+ expect(isShowingOnly(table, [])).toBe(true);
169
+ });
170
+ });
171
+
172
+ describe("isShowingOnlyColumns", () => {
173
+ it("reuses precomputed visible hideable columns", () => {
174
+ const table = createTestTable({
175
+ initiallyHidden: ["cust_age", "order_total"],
176
+ });
177
+ const state = getShowOnlyColumnState(table);
178
+
179
+ expect(isShowingOnlyColumns(state, ["customer_name"])).toBe(true);
180
+ expect(isShowingOnly(table, ["customer_name"])).toBe(true);
181
+ });
182
+ });
183
+
184
+ describe("applyShowOnlyColumns", () => {
185
+ it("shows only the requested hideable columns", () => {
186
+ const table = createTestTable({ initiallyHidden: ["cust_age"] });
187
+
188
+ act(() => {
189
+ applyShowOnlyColumns(table, ["customer_name", "order_total"]);
190
+ });
191
+
192
+ expect(isShowingOnly(table, ["customer_name", "order_total"])).toBe(true);
193
+ });
194
+ });
@@ -10,7 +10,7 @@ import {
10
10
  EyeIcon,
11
11
  EyeOffIcon,
12
12
  } from "lucide-react";
13
- import { useState } from "react";
13
+ import { useMemo, useState } from "react";
14
14
  import { useLocale } from "react-aria";
15
15
  import {
16
16
  AddDataframeChart,
@@ -52,8 +52,12 @@ import type { Column, Table } from "@tanstack/react-table";
52
52
  import { cn } from "@/utils/cn";
53
53
  import {
54
54
  getColumnCountForDisplay,
55
+ getShowOnlyColumnState,
55
56
  getUserColumnVisibilityCounts,
57
+ isShowingOnlyColumns,
58
+ type ShowOnlyColumnState,
56
59
  } from "../hooks/use-column-visibility";
60
+ import { ShowOnlyColumnButton } from "../show-only-column-button";
57
61
 
58
62
  interface ColumnExplorerPanelProps<TData> {
59
63
  previewColumn: PreviewColumn;
@@ -94,6 +98,11 @@ export function ColumnExplorerPanel<TData>({
94
98
  hiddenColumns: hiddenColumnCount,
95
99
  } = getColumnCountForDisplay(table, totalColumns);
96
100
  const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
101
+ const columnVisibility = table.getState().columnVisibility;
102
+ const showOnlyColumnState = useMemo(
103
+ () => getShowOnlyColumnState(table),
104
+ [table, columnVisibility],
105
+ );
97
106
 
98
107
  const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
99
108
  numRows: totalRows,
@@ -162,6 +171,8 @@ export function ColumnExplorerPanel<TData>({
162
171
  dataType={dataType}
163
172
  externalType={externalType}
164
173
  previewColumn={previewColumn}
174
+ table={table}
175
+ showOnlyColumnState={showOnlyColumnState}
165
176
  defaultExpanded={index === 0}
166
177
  />
167
178
  );
@@ -179,6 +190,8 @@ function ColumnItem<TData>({
179
190
  dataType,
180
191
  externalType,
181
192
  previewColumn,
193
+ table,
194
+ showOnlyColumnState,
182
195
  defaultExpanded = false,
183
196
  }: {
184
197
  columnName: string;
@@ -186,6 +199,8 @@ function ColumnItem<TData>({
186
199
  dataType: DataType;
187
200
  externalType: string;
188
201
  previewColumn: PreviewColumn;
202
+ table: Table<TData>;
203
+ showOnlyColumnState: ShowOnlyColumnState;
189
204
  defaultExpanded?: boolean;
190
205
  }) {
191
206
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
@@ -218,36 +233,50 @@ function ColumnItem<TData>({
218
233
  )}
219
234
  />
220
235
  {column?.getCanHide() && (
221
- <Tooltip
222
- content={column.getIsVisible() ? "Hide column" : "Show column"}
223
- delayDuration={400}
224
- >
225
- <Button
226
- type="button"
227
- variant="text"
228
- size="icon"
229
- aria-label={
230
- column.getIsVisible() ? "Hide column" : "Show column"
231
- }
236
+ <>
237
+ <ShowOnlyColumnButton
238
+ table={table}
239
+ columnIds={[columnName]}
240
+ disabled={isShowingOnlyColumns(showOnlyColumnState, [
241
+ columnName,
242
+ ])}
232
243
  className={cn(
233
- "hover:bg-muted text-muted-foreground hover:text-primary",
234
244
  column.getIsVisible()
235
245
  ? "group-hover:opacity-100 opacity-0"
236
246
  : "opacity-100",
237
247
  )}
238
- onClick={(e) => {
239
- e.preventDefault();
240
- e.stopPropagation();
241
- column.toggleVisibility(!column.getIsVisible());
242
- }}
248
+ />
249
+ <Tooltip
250
+ content={column.getIsVisible() ? "Hide column" : "Show column"}
251
+ delayDuration={400}
243
252
  >
244
- {column.getIsVisible() ? (
245
- <EyeIcon className="h-3 w-3" strokeWidth={2.5} />
246
- ) : (
247
- <EyeOffIcon className="h-3 w-3" strokeWidth={2.5} />
248
- )}
249
- </Button>
250
- </Tooltip>
253
+ <Button
254
+ type="button"
255
+ variant="text"
256
+ size="icon"
257
+ aria-label={
258
+ column.getIsVisible() ? "Hide column" : "Show column"
259
+ }
260
+ className={cn(
261
+ "hover:bg-muted text-muted-foreground hover:text-primary",
262
+ column.getIsVisible()
263
+ ? "group-hover:opacity-100 opacity-0"
264
+ : "opacity-100",
265
+ )}
266
+ onClick={(e) => {
267
+ e.preventDefault();
268
+ e.stopPropagation();
269
+ column.toggleVisibility(!column.getIsVisible());
270
+ }}
271
+ >
272
+ {column.getIsVisible() ? (
273
+ <EyeIcon className="h-3 w-3" strokeWidth={2.5} />
274
+ ) : (
275
+ <EyeOffIcon className="h-3 w-3" strokeWidth={2.5} />
276
+ )}
277
+ </Button>
278
+ </Tooltip>
279
+ </>
251
280
  )}
252
281
  <span className="text-xs text-muted-foreground">{externalType}</span>
253
282
  </div>
@@ -5,8 +5,8 @@
5
5
  // https://github.com/TanStack/table/issues/5567
6
6
 
7
7
  import type { Table } from "@tanstack/react-table";
8
- import { Columns3Icon, EyeIcon, EyeOffIcon } from "lucide-react";
9
- import React from "react";
8
+ import { Columns3Icon, EyeIcon, EyeOffIcon, ScanEyeIcon } from "lucide-react";
9
+ import React, { useMemo } from "react";
10
10
  import { ColumnName } from "@/components/datasources/components";
11
11
  import { Button } from "@/components/ui/button";
12
12
  import {
@@ -28,6 +28,11 @@ import { cn } from "@/utils/cn";
28
28
  import { Events } from "@/utils/events";
29
29
  import { smartMatchFilter } from "@/utils/smartMatch";
30
30
  import { NAMELESS_COLUMN_PREFIX } from "./columns";
31
+ import {
32
+ applyShowOnlyColumns,
33
+ isShowingOnlyColumns,
34
+ } from "./hooks/use-column-visibility";
35
+ import { ShowOnlyColumnButton } from "./show-only-column-button";
31
36
  import { INDEX_COLUMN_NAME, SELECT_COLUMN_ID } from "./types";
32
37
 
33
38
  function getUserColumns<TData>(table: Table<TData>) {
@@ -95,6 +100,23 @@ export const ColumnVisibilityDropdown = <TData,>({
95
100
  > =>
96
101
  action.kind === "select-matching" || action.kind === "deselect-matching",
97
102
  );
103
+ const showOnlyMatchIds =
104
+ list.searchQuery !== ""
105
+ ? list.visibleOptions
106
+ .filter((option) => !option.disabled)
107
+ .map((option) => option.value)
108
+ : [];
109
+ const showOnlyColumnState = useMemo(
110
+ () => ({
111
+ visibleHideableColumnIds: hideableIds.filter(
112
+ (id) => !hiddenIds.includes(id),
113
+ ),
114
+ hideableColumnIdSet: new Set(hideableIds),
115
+ }),
116
+ [hideableIds, hiddenIds],
117
+ );
118
+ const hasSearchBulkActions =
119
+ showOnlyMatchIds.length > 0 || matchingActions.length > 0;
98
120
 
99
121
  return (
100
122
  <Popover open={list.open} onOpenChange={list.setOpen}>
@@ -145,8 +167,25 @@ export const ColumnVisibilityDropdown = <TData,>({
145
167
  <CommandSeparator />
146
168
  </>
147
169
  ) : (
148
- matchingActions.length > 0 && (
170
+ hasSearchBulkActions && (
149
171
  <>
172
+ <CommandItem
173
+ value="__show_only_matching__"
174
+ disabled={
175
+ showOnlyMatchIds.length === 0 ||
176
+ isShowingOnlyColumns(
177
+ showOnlyColumnState,
178
+ showOnlyMatchIds,
179
+ )
180
+ }
181
+ onSelect={() => {
182
+ applyShowOnlyColumns(table, showOnlyMatchIds);
183
+ }}
184
+ className="cursor-pointer"
185
+ >
186
+ <ScanEyeIcon className="w-3 h-3 mr-1.5" />
187
+ Show only {showOnlyMatchIds.length} matching
188
+ </CommandItem>
150
189
  {matchingActions.map((action) => (
151
190
  <CommandItem
152
191
  key={action.kind}
@@ -194,17 +233,26 @@ export const ColumnVisibilityDropdown = <TData,>({
194
233
  />
195
234
  )}
196
235
  {!option.disabled && (
197
- <span
198
- className={cn(
199
- "ml-auto",
200
- hidden ? "text-primary" : "text-muted-foreground",
201
- )}
202
- >
203
- {hidden ? (
204
- <EyeOffIcon className="w-3 h-3" />
205
- ) : (
206
- <EyeIcon className="w-3 h-3" />
207
- )}
236
+ <span className="ml-auto flex items-center gap-0.5">
237
+ <ShowOnlyColumnButton
238
+ table={table}
239
+ columnIds={[option.value]}
240
+ disabled={isShowingOnlyColumns(showOnlyColumnState, [
241
+ option.value,
242
+ ])}
243
+ iconClassName="w-3 h-3"
244
+ />
245
+ <span
246
+ className={cn(
247
+ hidden ? "text-primary" : "text-muted-foreground",
248
+ )}
249
+ >
250
+ {hidden ? (
251
+ <EyeOffIcon className="w-3 h-3" />
252
+ ) : (
253
+ <EyeIcon className="w-3 h-3" />
254
+ )}
255
+ </span>
208
256
  </span>
209
257
  )}
210
258
  </CommandItem>