carbon-components-svelte/src/DataTable/ToolbarSearch.svelte
Eric Liu f09c2e2c31
fix(toolbar-search): re-filter rows if DataTable rows change (#2154)
Fixes #2143

Make `ToolbarSearch` filtering reactive to `DataTable` rows.

Previously, `ToolbarSearch` did not update when `DataTable` rows
changed. Now it subscribes to the context rows and re-runs
`filterRows` in `afterUpdate` to prevent infinite loops.
2025-04-19 13:33:07 -07:00

122 lines
2.8 KiB
Svelte

<script>
/**
* @restProps {input}
* @event {null} clear
*/
/**
* Specify the value of the search input
* @type {number | string}
*/
export let value = "";
/** Set to `true` to expand the search bar */
export let expanded = false;
/** Set to `true` to keep the search bar expanded */
export let persistent = false;
/** Set to `true` to disable the search bar */
export let disabled = false;
/**
* Set to `true` to filter table rows using the search value.
*
* If `true`, the default search excludes `id`, `cells` fields and
* only does a basic comparison on string and number type cell values.
*
* To implement your own client-side filtering, pass a function
* that accepts a row and value and returns a boolean.
* @type {boolean | ((row: import("./DataTable.svelte").DataTableRow, value: number | string) => boolean)}
*/
export let shouldFilterRows = false;
/**
* The filtered row ids
* @type {ReadonlyArray<import("./DataTable.svelte").DataTableRowId>}
*/
export let filteredRowIds = [];
/** Specify the tabindex */
export let tabindex = "0";
/**
* Obtain a reference to the input HTML element
* @type {null | HTMLInputElement}
*/
export let ref = null;
import { tick, getContext, afterUpdate, onMount } from "svelte";
import Search from "../Search/Search.svelte";
const ctx = getContext("DataTable") ?? {};
let rows = null;
let unsubscribe = null;
$: if (shouldFilterRows) {
unsubscribe = ctx?.tableRows.subscribe((tableRows) => {
// Only update if the rows have actually changed.
// This approach works in both Svelte 4 and Svelte 5.
if (JSON.stringify(tableRows) !== JSON.stringify(rows)) {
rows = tableRows;
}
});
} else {
rows = null;
}
onMount(() => {
return () => {
unsubscribe?.();
};
});
afterUpdate(() => {
// Only filter rows in a callback to avoid an infinite update loop.
if (rows !== null) {
filteredRowIds = ctx?.filterRows(value, shouldFilterRows);
}
});
async function expandSearch() {
await tick();
if (disabled || persistent || expanded) return;
expanded = true;
await tick();
ref.focus();
}
$: expanded = !!value.length;
$: classes = [
expanded && "bx--toolbar-search-container-active",
persistent
? "bx--toolbar-search-container-persistent"
: "bx--toolbar-search-container-expandable",
disabled && "bx--toolbar-search-container-disabled",
]
.filter(Boolean)
.join(" ");
</script>
<Search
{tabindex}
{disabled}
{...$$restProps}
searchClass="{classes} {$$restProps.class}"
bind:ref
bind:value
on:clear
on:clear={expandSearch}
on:change
on:input
on:focus
on:focus={expandSearch}
on:blur
on:blur={() => {
expanded = !persistent && !!value.length;
}}
on:keyup
on:keydown
on:paste
/>