@svgrid/grid 2.6.3 → 2.6.5
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/dist/SvGrid.controller.svelte.d.ts +2 -1
- package/dist/SvGrid.controller.svelte.js +145 -3
- package/dist/SvGrid.css +32 -0
- package/dist/SvGrid.svelte +64 -6
- package/dist/SvGrid.types.d.ts +61 -0
- package/dist/a11y/grid-announcements.d.ts +58 -0
- package/dist/a11y/grid-announcements.js +70 -0
- package/dist/advanced-filter.svelte.d.ts +42 -0
- package/dist/advanced-filter.svelte.js +13 -0
- package/dist/build-api.js +44 -2
- package/dist/cdn/{GridMenus-BoVKgAv8.js → GridMenus-CBvjL5u4.js} +14 -14
- package/dist/cdn/{GridMenus-DL1zrDwf.js → GridMenus-DAzp6YXc.js} +14 -14
- package/dist/cdn/{src-M2NfKnjX.js → src-3WKN7L4G.js} +5932 -5743
- package/dist/cdn/{src-CEpw6Ato.js → src-Cl7h5-Ux.js} +3371 -3182
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/filter-operators.d.ts +2 -1
- package/dist/filter-operators.js +5 -6
- package/dist/filtering/excel-filters.d.ts +1 -0
- package/dist/filtering/excel-filters.js +5 -0
- package/dist/filtering/filter-operator-catalogue.d.ts +42 -0
- package/dist/filtering/filter-operator-catalogue.js +42 -0
- package/dist/filtering/predicate-expr.d.ts +78 -0
- package/dist/filtering/predicate-expr.js +1 -0
- package/dist/filtering/row-predicate.d.ts +17 -0
- package/dist/filtering/row-predicate.js +55 -0
- package/dist/grid-messages.d.ts +7 -0
- package/dist/grid-messages.js +7 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/server-data-source.d.ts +34 -0
- package/dist/server-data-source.js +19 -0
- package/dist/svgrid-wrapper.types.d.ts +37 -5
- package/dist/themes/index.js +12 -4
- package/package.json +2 -1
- package/src/SvGrid.controller.svelte.ts +169 -2
- package/src/SvGrid.css +32 -0
- package/src/SvGrid.svelte +64 -6
- package/src/SvGrid.types.ts +61 -0
- package/src/a11y/grid-announcements.test.ts +78 -0
- package/src/a11y/grid-announcements.ts +94 -0
- package/src/a11y.announce.test.ts +250 -0
- package/src/a11y.axe.test.ts +194 -0
- package/src/advanced-filter.svelte.ts +59 -0
- package/src/build-api.ts +43 -2
- package/src/filter-operators.ts +9 -6
- package/src/filtering/excel-filters.ts +15 -0
- package/src/filtering/filter-operator-catalogue.ts +74 -0
- package/src/filtering/node-entry.test.ts +75 -0
- package/src/filtering/predicate-expr.ts +50 -0
- package/src/filtering/row-predicate.ts +57 -0
- package/src/grid-messages.ts +18 -0
- package/src/index.ts +28 -0
- package/src/server-data-source.test.ts +94 -0
- package/src/server-data-source.ts +56 -0
- package/src/svgrid-wrapper.types.ts +37 -5
- package/src/svgrid.advanced-filter.test.ts +481 -0
- package/src/svgrid.displayed-rows.test.ts +144 -0
- package/src/svgrid.group-props-reactive.svelte.test.ts +151 -0
- package/src/svgrid.group-props.test.ts +280 -0
- package/src/themes/contrast.test.ts +112 -0
- package/src/themes/index.ts +12 -4
- package/themes/catppuccin.css +3 -3
- package/themes/dracula.css +3 -3
- package/themes/nord.css +3 -3
- package/themes/notion.css +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CellEditorOption, type Column, type ColumnDef, type Row, type RowData, type TableFeatures, type ChartType, type ChartSpec, type ChartValueFormat } from "./index";
|
|
1
|
+
import { type GridPredicateExpr, type CellEditorOption, type Column, type ColumnDef, type Row, type RowData, type TableFeatures, type ChartType, type ChartSpec, type ChartValueFormat } from "./index";
|
|
2
2
|
import "./sv-grid-scrollbar";
|
|
3
3
|
import { type ColumnStat } from "./conditional-formatting";
|
|
4
4
|
import type { Props, SelectionRange, CellEditState, FilterOperator, MenuPosition, ContextMenuTarget, ChartingConfig } from "./SvGrid.types";
|
|
@@ -31,6 +31,7 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
|
|
|
31
31
|
valueTo2?: string;
|
|
32
32
|
join?: "AND" | "OR";
|
|
33
33
|
}>;
|
|
34
|
+
advancedFilter: GridPredicateExpr | null;
|
|
34
35
|
verticalScrollbarEl: HTMLElement | null;
|
|
35
36
|
horizontalScrollbarEl: HTMLElement | null;
|
|
36
37
|
scrollVersion: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyGroupAggregate, compileExcelFilter, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createTreeRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
|
|
1
|
+
import { applyGroupAggregate, applyRowPredicate, compileExcelFilter, getAdvancedFilterEngine, normalizeForFilter, createColumnVirtualizer, createCoreRowModel, createExpandedRowModel, createFilteredRowModel, createGroupedRowModel, createTreeRowModel, createSvelteVirtualizer, createSortedRowModel, createSvGrid, getGridCellDomId, sortFns, } from "./index";
|
|
2
2
|
import { createRowScrollScaling, resolveMaxDomHeight, } from "./virtualization/scroll-scaling";
|
|
3
3
|
import "./sv-grid-scrollbar";
|
|
4
4
|
import { computeColumnStat, formatsNeedingStats, formatNeedsStats, } from "./conditional-formatting";
|
|
@@ -21,6 +21,8 @@ import { buildAutoGroupColumns, insertGroupFooters, paginateGroupedRows } from "
|
|
|
21
21
|
import { createGridApi, } from "./build-api";
|
|
22
22
|
import { createClipboard, } from "./clipboard";
|
|
23
23
|
import { resolveGridMessages } from "./grid-messages";
|
|
24
|
+
import { announce } from "./a11y/live-region";
|
|
25
|
+
import { filterAnnouncement, selectionAnnouncement, } from "./a11y/grid-announcements";
|
|
24
26
|
import { getPivotEngine, hasPivotEngine } from "./pivot-view.svelte";
|
|
25
27
|
import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
|
|
26
28
|
import { isBucketableColumn, buildBuckets, isInBucket, } from "./facet-buckets";
|
|
@@ -190,6 +192,11 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
190
192
|
let globalFilter = $state("");
|
|
191
193
|
let scrollContainer = $state(null);
|
|
192
194
|
let gridRootEl = $state(null);
|
|
195
|
+
// Advanced filter (Pro). Seeded once from `initialAdvancedFilter` and driven
|
|
196
|
+
// thereafter through the API - deliberately NOT a live prop, so it cannot
|
|
197
|
+
// repeat the `externalFilter` trap of looking controlled while being read
|
|
198
|
+
// exactly once.
|
|
199
|
+
let advancedFilter = $state(props.initialAdvancedFilter ?? null);
|
|
193
200
|
let filterRowValues = $state({});
|
|
194
201
|
let filterMenuValues = $state({});
|
|
195
202
|
let verticalScrollbarEl = $state(null);
|
|
@@ -508,13 +515,15 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
508
515
|
},
|
|
509
516
|
state: {
|
|
510
517
|
columnFilters: [],
|
|
511
|
-
|
|
518
|
+
// svelte-ignore state_referenced_locally
|
|
519
|
+
grouping: treeDataConfig ? [] : [...(props.groupBy ?? [])],
|
|
512
520
|
// svelte-ignore state_referenced_locally
|
|
513
521
|
sorting: props.initialSorting ?? [],
|
|
514
522
|
// svelte-ignore state_referenced_locally
|
|
515
523
|
pagination: { pageIndex: 0, pageSize: props.pageSize ?? 10 },
|
|
516
524
|
rowSelection: {},
|
|
517
|
-
|
|
525
|
+
// svelte-ignore state_referenced_locally
|
|
526
|
+
expanded: { ...(props.expanded ?? {}) },
|
|
518
527
|
activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
|
|
519
528
|
},
|
|
520
529
|
});
|
|
@@ -571,6 +580,44 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
571
580
|
lastSeededOrder = incoming;
|
|
572
581
|
userColumnOrder = props.columnOrder ? [...props.columnOrder] : [];
|
|
573
582
|
});
|
|
583
|
+
// `groupBy` prop -> engine grouping state. Same shape as the `columnOrder`
|
|
584
|
+
// re-seed above: we only write when the PROP itself changes, so a group-by
|
|
585
|
+
// set from the column menu or `api.setGroupBy()` is not clobbered on the next
|
|
586
|
+
// unrelated re-render.
|
|
587
|
+
let lastSeededGroupBy = (props.groupBy ?? []).join("|");
|
|
588
|
+
$effect(() => {
|
|
589
|
+
if (treeDataConfig)
|
|
590
|
+
return;
|
|
591
|
+
const incoming = (props.groupBy ?? []).join("|");
|
|
592
|
+
if (incoming === lastSeededGroupBy)
|
|
593
|
+
return;
|
|
594
|
+
lastSeededGroupBy = incoming;
|
|
595
|
+
grid.setGrouping(() => [...(props.groupBy ?? [])]);
|
|
596
|
+
});
|
|
597
|
+
// `expanded` prop -> engine expansion state, and the store -> the
|
|
598
|
+
// `onExpandedChange` callback. `lastSeededExpanded` is written from BOTH
|
|
599
|
+
// directions so a controlled parent that echoes the callback value straight
|
|
600
|
+
// back into the prop does not trigger a redundant re-seed (and so no
|
|
601
|
+
// prop -> state -> callback -> prop loop can form).
|
|
602
|
+
let lastSeededExpanded = JSON.stringify(props.expanded ?? {});
|
|
603
|
+
$effect(() => {
|
|
604
|
+
const incoming = JSON.stringify(props.expanded ?? {});
|
|
605
|
+
if (incoming === lastSeededExpanded)
|
|
606
|
+
return;
|
|
607
|
+
lastSeededExpanded = incoming;
|
|
608
|
+
grid.setExpanded(() => ({ ...(props.expanded ?? {}) }));
|
|
609
|
+
});
|
|
610
|
+
$effect(() => {
|
|
611
|
+
let previous = grid.getState().expanded ?? {};
|
|
612
|
+
return grid.store.subscribe(() => {
|
|
613
|
+
const next = grid.getState().expanded ?? {};
|
|
614
|
+
if (next === previous)
|
|
615
|
+
return;
|
|
616
|
+
previous = next;
|
|
617
|
+
lastSeededExpanded = JSON.stringify(next);
|
|
618
|
+
props.onExpandedChange?.({ ...next });
|
|
619
|
+
});
|
|
620
|
+
});
|
|
574
621
|
// Declared here rather than beside the other pagination/state derivations:
|
|
575
622
|
// the auto-group column pipeline below reads it, and a `const` used above its
|
|
576
623
|
// declaration is a type error even though `$derived` is lazy enough at runtime.
|
|
@@ -1039,6 +1086,33 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1039
1086
|
return allowed.has(String(raw ?? ""));
|
|
1040
1087
|
}));
|
|
1041
1088
|
}
|
|
1089
|
+
// --- Advanced filter (Pro) ---------------------------------------------
|
|
1090
|
+
// Runs LAST for two reasons: it evaluates over the smallest row set, and
|
|
1091
|
+
// its aggregates (`SUM(amount) > 1000`) fold over what the user is
|
|
1092
|
+
// currently looking at rather than the raw dataset. Composed with AND
|
|
1093
|
+
// against the three stages above.
|
|
1094
|
+
//
|
|
1095
|
+
// Fails OPEN throughout. No engine registered (enterprise absent), an
|
|
1096
|
+
// expression the engine cannot compile, or a throw from inside it all
|
|
1097
|
+
// leave `rows` untouched. A half-applied filter that quietly shows the
|
|
1098
|
+
// wrong rows is worse than one that visibly did nothing.
|
|
1099
|
+
if (advancedFilter) {
|
|
1100
|
+
const engine = getAdvancedFilterEngine();
|
|
1101
|
+
if (engine) {
|
|
1102
|
+
try {
|
|
1103
|
+
const predicate = engine(advancedFilter, {
|
|
1104
|
+
getValue: getRowColumnValue,
|
|
1105
|
+
locale: props.filterLocale ?? props.localization?.locale,
|
|
1106
|
+
rows,
|
|
1107
|
+
});
|
|
1108
|
+
if (predicate)
|
|
1109
|
+
rows = applyRowPredicate(rows, predicate, isGroupRow);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
// Malformed expression: leave the rows alone.
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1042
1116
|
return rows;
|
|
1043
1117
|
});
|
|
1044
1118
|
/**
|
|
@@ -1334,6 +1408,72 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1334
1408
|
// Resolved chrome messages (English defaults merged with `localeText`). One
|
|
1335
1409
|
// map every consumer (SvGrid.svelte, GridFooter, menus, filter labels) reads.
|
|
1336
1410
|
const gridMessages = $derived(resolveGridMessages(props.localization?.text));
|
|
1411
|
+
// ---- Screen-reader status announcements (WCAG 4.1.3) ----
|
|
1412
|
+
// Only genuine status messages belong here: information no focus change
|
|
1413
|
+
// reveals. `a11y/grid-announcements.ts` documents what is deliberately left
|
|
1414
|
+
// unsaid, and why saying it would make the grid talk over itself.
|
|
1415
|
+
/** Data rows before any local filtering - the denominator in "12 of 250". */
|
|
1416
|
+
const unfilteredRowTotal = $derived.by(() => {
|
|
1417
|
+
dataStateVersion;
|
|
1418
|
+
void internalData;
|
|
1419
|
+
return grid.getRowModel().rows.length;
|
|
1420
|
+
});
|
|
1421
|
+
/** Whether any of the five filter surfaces is currently narrowing the rows. */
|
|
1422
|
+
const anyFilterActive = $derived(globalFilter !== "" ||
|
|
1423
|
+
advancedFilter !== null ||
|
|
1424
|
+
Object.keys(filterRowValues).length > 0 ||
|
|
1425
|
+
Object.keys(filterMenuValues).length > 0 ||
|
|
1426
|
+
Object.keys(valueFilters).length > 0);
|
|
1427
|
+
let filtersWereActive = false;
|
|
1428
|
+
let filterAnnounceTimer;
|
|
1429
|
+
$effect(() => {
|
|
1430
|
+
// In external-filter mode the server decides what matched, so the local
|
|
1431
|
+
// counts describe only the page in hand. Announcing them would misreport
|
|
1432
|
+
// the result set, which is worse than staying silent.
|
|
1433
|
+
if (externalFilterEnabled)
|
|
1434
|
+
return;
|
|
1435
|
+
const visible = paginatedRowTotal;
|
|
1436
|
+
const total = unfilteredRowTotal;
|
|
1437
|
+
const active = anyFilterActive;
|
|
1438
|
+
const messages = gridMessages;
|
|
1439
|
+
untrack(() => {
|
|
1440
|
+
const message = filterAnnouncement(visible, total, active, filtersWereActive, messages);
|
|
1441
|
+
filtersWereActive = active;
|
|
1442
|
+
if (!message)
|
|
1443
|
+
return;
|
|
1444
|
+
// Typing in the global filter re-runs this per keystroke. A polite region
|
|
1445
|
+
// queues rather than replaces, so without a debounce the user hears the
|
|
1446
|
+
// count for every prefix they typed before the one they care about.
|
|
1447
|
+
clearTimeout(filterAnnounceTimer);
|
|
1448
|
+
filterAnnounceTimer = setTimeout(() => announce(message), 400);
|
|
1449
|
+
});
|
|
1450
|
+
return () => clearTimeout(filterAnnounceTimer);
|
|
1451
|
+
});
|
|
1452
|
+
// Forward advanced-filter changes to the consumer. Same dedupe-and-skip-first
|
|
1453
|
+
// pattern as the selection forwarder: the panel that authors the expression
|
|
1454
|
+
// lives outside the grid, so a change the grid makes itself (the toolbar's
|
|
1455
|
+
// clear, clearAllFilters) is invisible to it otherwise.
|
|
1456
|
+
let lastAdvancedFilterSerialized = JSON.stringify(untrack(() => advancedFilter) ?? null);
|
|
1457
|
+
$effect(() => {
|
|
1458
|
+
const serialized = JSON.stringify(advancedFilter ?? null);
|
|
1459
|
+
if (serialized === untrack(() => lastAdvancedFilterSerialized))
|
|
1460
|
+
return;
|
|
1461
|
+
untrack(() => {
|
|
1462
|
+
lastAdvancedFilterSerialized = serialized;
|
|
1463
|
+
props.onAdvancedFilterChange?.(advancedFilter ?? null);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
let lastAnnouncedSelectionCount = 0;
|
|
1467
|
+
$effect(() => {
|
|
1468
|
+
const count = Object.values(rowSelectionState).filter(Boolean).length;
|
|
1469
|
+
const messages = gridMessages;
|
|
1470
|
+
untrack(() => {
|
|
1471
|
+
const message = selectionAnnouncement(lastAnnouncedSelectionCount, count, messages);
|
|
1472
|
+
lastAnnouncedSelectionCount = count;
|
|
1473
|
+
if (message)
|
|
1474
|
+
announce(message);
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1337
1477
|
// ---- In-grid pivot mode ----
|
|
1338
1478
|
// The pivot ENGINE ships in @svgrid/enterprise (registered via the pivot-view
|
|
1339
1479
|
// seam). When `pivot` is set and the engine is present, the grid runs it over
|
|
@@ -2638,6 +2778,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2638
2778
|
set filterRowValues(v) { filterRowValues = v; },
|
|
2639
2779
|
get filterMenuValues() { return filterMenuValues; },
|
|
2640
2780
|
set filterMenuValues(v) { filterMenuValues = v; },
|
|
2781
|
+
get advancedFilter() { return advancedFilter; },
|
|
2782
|
+
set advancedFilter(v) { advancedFilter = v; },
|
|
2641
2783
|
get verticalScrollbarEl() { return verticalScrollbarEl; },
|
|
2642
2784
|
set verticalScrollbarEl(v) { verticalScrollbarEl = v; },
|
|
2643
2785
|
get horizontalScrollbarEl() { return horizontalScrollbarEl; },
|
package/dist/SvGrid.css
CHANGED
|
@@ -122,6 +122,38 @@
|
|
|
122
122
|
border-color: var(--sg-accent, #2563eb);
|
|
123
123
|
color: #fff;
|
|
124
124
|
}
|
|
125
|
+
/* Advanced-filter indicator. Reads as state rather than as another action, so
|
|
126
|
+
it uses the accent fill the toolbar buttons reserve for `is-active` instead
|
|
127
|
+
of the neutral button shell. */
|
|
128
|
+
.sv-grid-advf-chip {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
gap: 6px;
|
|
132
|
+
padding: 5px 6px 5px 11px;
|
|
133
|
+
border-radius: 999px;
|
|
134
|
+
border: 1px solid color-mix(in srgb, var(--sg-accent, #2563eb) 35%, transparent);
|
|
135
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 12%, transparent);
|
|
136
|
+
color: var(--sg-accent, #2563eb);
|
|
137
|
+
font-size: 12.5px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
}
|
|
140
|
+
.sv-grid-advf-clear {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
width: 18px;
|
|
145
|
+
height: 18px;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: 999px;
|
|
148
|
+
background: transparent;
|
|
149
|
+
color: inherit;
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: background-color 120ms ease;
|
|
153
|
+
}
|
|
154
|
+
.sv-grid-advf-clear:hover {
|
|
155
|
+
background: color-mix(in srgb, var(--sg-accent, #2563eb) 25%, transparent);
|
|
156
|
+
}
|
|
125
157
|
.sv-grid-tool-panel {
|
|
126
158
|
position: absolute;
|
|
127
159
|
top: 0;
|
package/dist/SvGrid.svelte
CHANGED
|
@@ -1900,8 +1900,29 @@
|
|
|
1900
1900
|
</label>
|
|
1901
1901
|
{/if}
|
|
1902
1902
|
|
|
1903
|
-
{#if toolPanelEnabled || ctrl.chartingEnabled}
|
|
1903
|
+
{#if toolPanelEnabled || ctrl.chartingEnabled || ctrl.advancedFilter}
|
|
1904
1904
|
<div class="sv-grid-toolbar">
|
|
1905
|
+
{#if ctrl.advancedFilter}
|
|
1906
|
+
<!--
|
|
1907
|
+
An advanced filter is set from a panel the consumer mounts, which
|
|
1908
|
+
may be scrolled away or in a drawer. Without something here, rows
|
|
1909
|
+
are missing from the grid with nothing on screen to explain why or
|
|
1910
|
+
to undo it. This is that something: state plus a way out.
|
|
1911
|
+
-->
|
|
1912
|
+
<span class="sv-grid-advf-chip">
|
|
1913
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
1914
|
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
1915
|
+
</svg>
|
|
1916
|
+
{messages.advancedFilterActive}
|
|
1917
|
+
<button
|
|
1918
|
+
type="button"
|
|
1919
|
+
class="sv-grid-advf-clear"
|
|
1920
|
+
aria-label={messages.advancedFilterClear}
|
|
1921
|
+
title={messages.advancedFilterClear}
|
|
1922
|
+
onclick={() => (ctrl.advancedFilter = null)}
|
|
1923
|
+
>✕</button>
|
|
1924
|
+
</span>
|
|
1925
|
+
{/if}
|
|
1905
1926
|
{#if toolPanelEnabled}
|
|
1906
1927
|
<button
|
|
1907
1928
|
type="button"
|
|
@@ -2332,6 +2353,18 @@
|
|
|
2332
2353
|
aria-orientation="vertical"
|
|
2333
2354
|
aria-label={`Resize ${toolPanelHeaderLabel(header.column)}`}
|
|
2334
2355
|
tabindex="0"
|
|
2356
|
+
{...(() => {
|
|
2357
|
+
// A focusable separator is a widget, so ARIA requires
|
|
2358
|
+
// aria-valuenow. The value it exposes is the column
|
|
2359
|
+
// width the arrow keys change; 40 is the floor
|
|
2360
|
+
// enforced in resizeColumnByKeyboard.
|
|
2361
|
+
const w = Math.round(ctrl.getColumnWidth(header.column.id));
|
|
2362
|
+
return {
|
|
2363
|
+
"aria-valuenow": w,
|
|
2364
|
+
"aria-valuemin": 40,
|
|
2365
|
+
"aria-valuetext": `${w} pixels`,
|
|
2366
|
+
};
|
|
2367
|
+
})()}
|
|
2335
2368
|
onpointerdown={(event) =>
|
|
2336
2369
|
startColumnResize(event, header.column.id)}
|
|
2337
2370
|
onkeydown={(event) =>
|
|
@@ -2359,9 +2392,14 @@
|
|
|
2359
2392
|
></th>
|
|
2360
2393
|
{/if}
|
|
2361
2394
|
{#if showRowSelectionEffective}
|
|
2395
|
+
<!-- Filter-row spacer over the selection column. Blank and
|
|
2396
|
+
non-interactive, so hide it from the a11y tree the same
|
|
2397
|
+
way the header-row twin does; an exposed empty th is an
|
|
2398
|
+
axe `empty-table-header` violation. -->
|
|
2362
2399
|
<th
|
|
2363
2400
|
class="sv-grid-column sv-grid-selection-column"
|
|
2364
2401
|
style={`width: ${selectionColumnWidth}px; min-width: ${selectionColumnWidth}px; max-width: ${selectionColumnWidth}px; left: ${showRowNumbersEffective ? rowNumberColumnWidth : 0}px;`}
|
|
2402
|
+
aria-hidden="true"
|
|
2365
2403
|
></th>
|
|
2366
2404
|
{/if}
|
|
2367
2405
|
{#if columnVirtualizationEnabled && columnWindowStart > 0}
|
|
@@ -2688,6 +2726,7 @@
|
|
|
2688
2726
|
class:sv-grid-cell-cf={hasConditionalFormats}
|
|
2689
2727
|
class:sv-grid-cell-invalid={cellValidity.invalid}
|
|
2690
2728
|
class:sv-grid-cell-has-note={cellNote != null}
|
|
2729
|
+
aria-invalid={cellValidity.invalid ? "true" : undefined}
|
|
2691
2730
|
title={cellValidity.message ?? undefined}
|
|
2692
2731
|
data-svgrid-row={rowIndex}
|
|
2693
2732
|
data-svgrid-col={colIndex}
|
|
@@ -2935,6 +2974,7 @@
|
|
|
2935
2974
|
class:sv-grid-cell-cf={hasConditionalFormats}
|
|
2936
2975
|
class:sv-grid-cell-invalid={cellValidity.invalid}
|
|
2937
2976
|
class:sv-grid-cell-has-note={cellNote != null}
|
|
2977
|
+
aria-invalid={cellValidity.invalid ? "true" : undefined}
|
|
2938
2978
|
data-svgrid-row={rowIndex}
|
|
2939
2979
|
data-svgrid-col={colIndex}
|
|
2940
2980
|
data-col-id={rendered.column.id}
|
|
@@ -3008,6 +3048,20 @@
|
|
|
3008
3048
|
cellValue,
|
|
3009
3049
|
)}
|
|
3010
3050
|
{/if}
|
|
3051
|
+
{#if cellValidity.invalid && cellValidity.message}
|
|
3052
|
+
<!-- The validation message, for assistive tech.
|
|
3053
|
+
On this path the message is shown visually by
|
|
3054
|
+
the pointerenter tooltip above, which is mouse-
|
|
3055
|
+
only - a keyboard or screen-reader user would
|
|
3056
|
+
get `aria-invalid` with no reason attached.
|
|
3057
|
+
A visually-hidden span inside the cell is read
|
|
3058
|
+
as part of the cell when focus lands on it, and
|
|
3059
|
+
unlike `title` it adds no second native tooltip
|
|
3060
|
+
competing with the custom one. -->
|
|
3061
|
+
<span class="sv-grid-sr-only"
|
|
3062
|
+
>{cellValidity.message}</span
|
|
3063
|
+
>
|
|
3064
|
+
{/if}
|
|
3011
3065
|
{#if cellNote != null && !isEditing}
|
|
3012
3066
|
<!-- Excel-style per-cell note indicator. The
|
|
3013
3067
|
triangle itself is the hot-zone; hover
|
|
@@ -3067,17 +3121,21 @@
|
|
|
3067
3121
|
{#if showRowNumbersEffective}
|
|
3068
3122
|
<!-- Row-number column has no aggregate; the digit it normally
|
|
3069
3123
|
shows is the row index, which doesn't make sense to sum. -->
|
|
3070
|
-
|
|
3124
|
+
<!-- Blank spacer, not a header - see the selection cell below. -->
|
|
3125
|
+
<td
|
|
3071
3126
|
class="sv-grid-column sv-grid-summary-column sv-grid-row-number-column"
|
|
3072
3127
|
style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
|
|
3073
|
-
></
|
|
3128
|
+
></td>
|
|
3074
3129
|
{/if}
|
|
3075
3130
|
{#if showRowSelectionEffective}
|
|
3076
|
-
<!-- Selection column is checkbox-only; no aggregate.
|
|
3077
|
-
|
|
3131
|
+
<!-- Selection column is checkbox-only; no aggregate. A blank
|
|
3132
|
+
spacer in the summary row carries no header semantics, so
|
|
3133
|
+
it is a td: an empty th is a real a11y violation
|
|
3134
|
+
(axe `empty-table-header`). -->
|
|
3135
|
+
<td
|
|
3078
3136
|
class="sv-grid-column sv-grid-summary-column sv-grid-selection-column"
|
|
3079
3137
|
style={`width: ${selectionColumnWidth}px; min-width: ${selectionColumnWidth}px; max-width: ${selectionColumnWidth}px; left: ${showRowNumbersEffective ? rowNumberColumnWidth : 0}px;`}
|
|
3080
|
-
></
|
|
3138
|
+
></td>
|
|
3081
3139
|
{/if}
|
|
3082
3140
|
{#if columnVirtualizationEnabled && columnWindowStart > 0}
|
|
3083
3141
|
<th
|
package/dist/SvGrid.types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ConditionalFormat } from "./conditional-formatting";
|
|
|
4
4
|
import type { GroupDisplayType } from "./group-display";
|
|
5
5
|
import type { GridMessages } from "./grid-messages";
|
|
6
6
|
import type { GridPivotConfig } from "./pivot-view.svelte";
|
|
7
|
+
import type { GridPredicateExpr } from "./filtering/predicate-expr";
|
|
7
8
|
import type { MenuItem } from "./SvMenuList.svelte";
|
|
8
9
|
/** The calendar views the scheduler can render. {@link SchedulerConfig}. The
|
|
9
10
|
* `timeline*` views are horizontal: time runs left→right and resources are
|
|
@@ -1024,6 +1025,44 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1024
1025
|
indentPx?: number;
|
|
1025
1026
|
};
|
|
1026
1027
|
groupable?: boolean;
|
|
1028
|
+
/**
|
|
1029
|
+
* Group the rows by these column ids, outermost first - `['region',
|
|
1030
|
+
* 'country']` rolls country up inside region.
|
|
1031
|
+
*
|
|
1032
|
+
* ```svelte
|
|
1033
|
+
* <SvGrid {data} {columns} {features} groupBy={['department']} />
|
|
1034
|
+
* ```
|
|
1035
|
+
*
|
|
1036
|
+
* The prop seeds the group-by list and re-applies whenever it changes, so it
|
|
1037
|
+
* works both as initial state and as a controlled value. The column menu and
|
|
1038
|
+
* `api.setGroupBy()` write the same state; a change from either survives
|
|
1039
|
+
* until this prop's own value changes.
|
|
1040
|
+
*
|
|
1041
|
+
* Ignored when `treeData` is set - a row cannot be both a hierarchy node and
|
|
1042
|
+
* bucketed under a group banner.
|
|
1043
|
+
*/
|
|
1044
|
+
groupBy?: ReadonlyArray<string>;
|
|
1045
|
+
/**
|
|
1046
|
+
* Which group / tree rows are expanded, keyed by row id.
|
|
1047
|
+
*
|
|
1048
|
+
* Expansion is owned by the engine by default; pass this prop to hoist it
|
|
1049
|
+
* (saved views, "expand everything on load", persisted UI state). Like
|
|
1050
|
+
* `groupBy` it seeds the state and re-applies whenever it changes.
|
|
1051
|
+
*
|
|
1052
|
+
* Group row ids are built from the grouping path, not the display label:
|
|
1053
|
+
* `group_department_Engineering`, and one level deeper
|
|
1054
|
+
* `group_department_Engineering_role_Senior`. Tree rows key off the engine's
|
|
1055
|
+
* row id instead (set `getRowId` to make that your own id). Prefer capturing
|
|
1056
|
+
* the map from `onExpandedChange` over hand-building these keys.
|
|
1057
|
+
*/
|
|
1058
|
+
expanded?: Record<string, boolean>;
|
|
1059
|
+
/**
|
|
1060
|
+
* Fired whenever the expanded set changes - chevron clicks,
|
|
1061
|
+
* `api.setRowExpanded()`, `expandAllGroups()` / `collapseAllGroups()`.
|
|
1062
|
+
* Receives the full next map, so it can be written straight back into
|
|
1063
|
+
* `expanded` for a controlled setup.
|
|
1064
|
+
*/
|
|
1065
|
+
onExpandedChange?: (expanded: Record<string, boolean>) => void;
|
|
1027
1066
|
/**
|
|
1028
1067
|
* Render a subtotal row after each group's children, carrying that group's
|
|
1029
1068
|
* aggregate values under the columns they belong to (the columns with an
|
|
@@ -1335,6 +1374,28 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1335
1374
|
id: string;
|
|
1336
1375
|
desc: boolean;
|
|
1337
1376
|
}>;
|
|
1377
|
+
/**
|
|
1378
|
+
* The advanced-filter expression to start in (Pro). Applied once at mount;
|
|
1379
|
+
* change it afterwards through `api.setAdvancedFilter()`.
|
|
1380
|
+
*
|
|
1381
|
+
* Filtering only happens once `@svgrid/enterprise`'s `enableAdvancedFilter()`
|
|
1382
|
+
* has registered a compiler - without it the expression is stored and
|
|
1383
|
+
* reported but no rows are removed, so the free grid degrades to a no-op
|
|
1384
|
+
* rather than to a wrong row set.
|
|
1385
|
+
*/
|
|
1386
|
+
initialAdvancedFilter?: GridPredicateExpr | null;
|
|
1387
|
+
/**
|
|
1388
|
+
* Fires whenever the advanced-filter expression changes, whatever changed it:
|
|
1389
|
+
* `api.setAdvancedFilter()`, `clearAllFilters()`, or the toolbar's clear
|
|
1390
|
+
* control.
|
|
1391
|
+
*
|
|
1392
|
+
* The panel that authors the expression is mounted by you, outside the grid,
|
|
1393
|
+
* so it cannot see a change the grid made on its own. Without this, clearing
|
|
1394
|
+
* from the toolbar leaves that panel showing a filter the grid is no longer
|
|
1395
|
+
* applying. Feed this back into the panel's `expression` prop to keep them
|
|
1396
|
+
* agreeing.
|
|
1397
|
+
*/
|
|
1398
|
+
onAdvancedFilterChange?: (expr: GridPredicateExpr | null) => void;
|
|
1338
1399
|
/**
|
|
1339
1400
|
* When `true`, the grid still records column-filter / global-filter /
|
|
1340
1401
|
* facet state (so the menu UI works and indicators light up) but does
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grid-announcements - what the grid says to a screen reader, and when.
|
|
3
|
+
*
|
|
4
|
+
* WCAG 4.1.3 covers *status messages*: information the user needs that does not
|
|
5
|
+
* receive focus. That is a narrow set, and getting the boundary right matters
|
|
6
|
+
* more than announcing a lot.
|
|
7
|
+
*
|
|
8
|
+
* Things the grid deliberately does NOT announce here, because the
|
|
9
|
+
* accessibility tree already conveys them when focus lands:
|
|
10
|
+
*
|
|
11
|
+
* - the value of the cell you moved to (roving tabindex moves focus)
|
|
12
|
+
* - the sort state of a header (`aria-sort` on the columnheader)
|
|
13
|
+
* - whether the row you are on is selected (`aria-selected` on the row)
|
|
14
|
+
*
|
|
15
|
+
* Announcing those again would make the grid talk over itself. What is left is
|
|
16
|
+
* genuinely invisible to a screen reader: how many rows survived a filter, and
|
|
17
|
+
* bulk selection changes that move no focus (select-all, clear).
|
|
18
|
+
*
|
|
19
|
+
* These builders are pure so they can be unit-tested without a DOM, and take
|
|
20
|
+
* their strings from `GridMessages` so an announcement is translated like any
|
|
21
|
+
* other label.
|
|
22
|
+
*/
|
|
23
|
+
/** The subset of `GridMessages` this module reads. */
|
|
24
|
+
export type AnnouncementMessages = {
|
|
25
|
+
announceFilterResults: string;
|
|
26
|
+
announceNoMatches: string;
|
|
27
|
+
announceFiltersCleared: string;
|
|
28
|
+
announceRowsSelected: string;
|
|
29
|
+
announceSelectionCleared: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Interpolate `{name}` placeholders.
|
|
33
|
+
*
|
|
34
|
+
* The rest of `GridMessages` is single words assembled by the caller (`of`,
|
|
35
|
+
* `rowsSuffix`). That works for labels but not for sentences: languages order
|
|
36
|
+
* "12 of 250 match" differently, and a translator handed three separate atoms
|
|
37
|
+
* cannot fix the order. Announcements are whole sentences, so they get whole
|
|
38
|
+
* templates. An unknown placeholder is left alone rather than blanked, so a
|
|
39
|
+
* typo in an override is visible instead of silently eating the number.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatMessage(template: string, vars: Record<string, string | number>): string;
|
|
42
|
+
/**
|
|
43
|
+
* What to say after the row set changes, or null to stay quiet.
|
|
44
|
+
*
|
|
45
|
+
* `visible` is the count after filtering but BEFORE pagination - the answer to
|
|
46
|
+
* "how many rows matched", not "how many are on this page".
|
|
47
|
+
*/
|
|
48
|
+
export declare function filterAnnouncement(visible: number, total: number, filtersActive: boolean, filtersWereActive: boolean, messages: AnnouncementMessages): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* What to say after the selection changes, or null to stay quiet.
|
|
51
|
+
*
|
|
52
|
+
* Only BULK changes are announced. Selecting one row moves focus to it and the
|
|
53
|
+
* screen reader reads it as selected from `aria-selected`; announcing "1 row
|
|
54
|
+
* selected" on top of that is the grid talking over itself. A change of more
|
|
55
|
+
* than one row cannot have come from a single focus move, so it is the
|
|
56
|
+
* select-all / range / clear case that otherwise passes silently.
|
|
57
|
+
*/
|
|
58
|
+
export declare function selectionAnnouncement(previousCount: number, count: number, messages: AnnouncementMessages): string | null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grid-announcements - what the grid says to a screen reader, and when.
|
|
3
|
+
*
|
|
4
|
+
* WCAG 4.1.3 covers *status messages*: information the user needs that does not
|
|
5
|
+
* receive focus. That is a narrow set, and getting the boundary right matters
|
|
6
|
+
* more than announcing a lot.
|
|
7
|
+
*
|
|
8
|
+
* Things the grid deliberately does NOT announce here, because the
|
|
9
|
+
* accessibility tree already conveys them when focus lands:
|
|
10
|
+
*
|
|
11
|
+
* - the value of the cell you moved to (roving tabindex moves focus)
|
|
12
|
+
* - the sort state of a header (`aria-sort` on the columnheader)
|
|
13
|
+
* - whether the row you are on is selected (`aria-selected` on the row)
|
|
14
|
+
*
|
|
15
|
+
* Announcing those again would make the grid talk over itself. What is left is
|
|
16
|
+
* genuinely invisible to a screen reader: how many rows survived a filter, and
|
|
17
|
+
* bulk selection changes that move no focus (select-all, clear).
|
|
18
|
+
*
|
|
19
|
+
* These builders are pure so they can be unit-tested without a DOM, and take
|
|
20
|
+
* their strings from `GridMessages` so an announcement is translated like any
|
|
21
|
+
* other label.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Interpolate `{name}` placeholders.
|
|
25
|
+
*
|
|
26
|
+
* The rest of `GridMessages` is single words assembled by the caller (`of`,
|
|
27
|
+
* `rowsSuffix`). That works for labels but not for sentences: languages order
|
|
28
|
+
* "12 of 250 match" differently, and a translator handed three separate atoms
|
|
29
|
+
* cannot fix the order. Announcements are whole sentences, so they get whole
|
|
30
|
+
* templates. An unknown placeholder is left alone rather than blanked, so a
|
|
31
|
+
* typo in an override is visible instead of silently eating the number.
|
|
32
|
+
*/
|
|
33
|
+
export function formatMessage(template, vars) {
|
|
34
|
+
return template.replace(/\{(\w+)\}/g, (whole, key) => key in vars ? String(vars[key]) : whole);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What to say after the row set changes, or null to stay quiet.
|
|
38
|
+
*
|
|
39
|
+
* `visible` is the count after filtering but BEFORE pagination - the answer to
|
|
40
|
+
* "how many rows matched", not "how many are on this page".
|
|
41
|
+
*/
|
|
42
|
+
export function filterAnnouncement(visible, total, filtersActive, filtersWereActive, messages) {
|
|
43
|
+
if (filtersActive) {
|
|
44
|
+
if (visible === 0)
|
|
45
|
+
return messages.announceNoMatches;
|
|
46
|
+
return formatMessage(messages.announceFilterResults, { visible, total });
|
|
47
|
+
}
|
|
48
|
+
// Clearing a filter is itself a status change worth confirming; without this
|
|
49
|
+
// the user hears nothing back and cannot tell the clear took effect.
|
|
50
|
+
if (filtersWereActive) {
|
|
51
|
+
return formatMessage(messages.announceFiltersCleared, { total });
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What to say after the selection changes, or null to stay quiet.
|
|
57
|
+
*
|
|
58
|
+
* Only BULK changes are announced. Selecting one row moves focus to it and the
|
|
59
|
+
* screen reader reads it as selected from `aria-selected`; announcing "1 row
|
|
60
|
+
* selected" on top of that is the grid talking over itself. A change of more
|
|
61
|
+
* than one row cannot have come from a single focus move, so it is the
|
|
62
|
+
* select-all / range / clear case that otherwise passes silently.
|
|
63
|
+
*/
|
|
64
|
+
export function selectionAnnouncement(previousCount, count, messages) {
|
|
65
|
+
if (Math.abs(count - previousCount) <= 1)
|
|
66
|
+
return null;
|
|
67
|
+
if (count === 0)
|
|
68
|
+
return messages.announceSelectionCleared;
|
|
69
|
+
return formatMessage(messages.announceRowsSelected, { count });
|
|
70
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration seam for the advanced filter (a `@svgrid/enterprise` feature).
|
|
3
|
+
*
|
|
4
|
+
* The grid owns the state and the pipeline slot; enterprise owns the compiler.
|
|
5
|
+
* Same shape as `pivot-view.svelte.ts`: a module-level `$state` holder with a
|
|
6
|
+
* register / get / has triple, so the free package carries no commercial code.
|
|
7
|
+
*/
|
|
8
|
+
import type { GridPredicateExpr } from './filtering/predicate-expr';
|
|
9
|
+
/** What the engine needs in order to compile an expression into a predicate. */
|
|
10
|
+
export type AdvancedFilterCompileContext<TRow> = {
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a column id to its cell value for a row. The grid passes the same
|
|
13
|
+
* accessor the column-menu filter stage uses, so a `cmp` on a `fieldFn` or
|
|
14
|
+
* value-getter column resolves identically in both.
|
|
15
|
+
*/
|
|
16
|
+
getValue: (row: TRow, columnId: string) => unknown;
|
|
17
|
+
/** Locale for text folding, matching the column filters. */
|
|
18
|
+
locale?: string | ReadonlyArray<string>;
|
|
19
|
+
/**
|
|
20
|
+
* The rows entering this stage. Aggregates (`SUM(amount) > 1000`) are folded
|
|
21
|
+
* over exactly these, once, so they see the rows the user is currently
|
|
22
|
+
* looking at rather than the raw dataset.
|
|
23
|
+
*/
|
|
24
|
+
rows: ReadonlyArray<TRow>;
|
|
25
|
+
};
|
|
26
|
+
/** A compiled expression: cheap to call once per row. */
|
|
27
|
+
export type CompiledRowPredicate<TRow> = (row: TRow) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Compile an expression to a row predicate.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` when the expression cannot be compiled (an unknown node kind,
|
|
32
|
+
* an unresolvable column). Must never throw: the grid treats both `null` and a
|
|
33
|
+
* thrown error as "do not filter", because a half-applied filter silently
|
|
34
|
+
* showing the wrong rows is worse than an unapplied one.
|
|
35
|
+
*/
|
|
36
|
+
export type AdvancedFilterEngine = <TRow>(expr: GridPredicateExpr, ctx: AdvancedFilterCompileContext<TRow>) => CompiledRowPredicate<TRow> | null;
|
|
37
|
+
/** Register the engine. Enterprise calls this from `enableAdvancedFilter()`. */
|
|
38
|
+
export declare function registerAdvancedFilterEngine(fn: AdvancedFilterEngine | null): void;
|
|
39
|
+
/** The registered engine, or null when enterprise is not installed. */
|
|
40
|
+
export declare function getAdvancedFilterEngine(): AdvancedFilterEngine | null;
|
|
41
|
+
/** Whether an advanced-filter engine has been registered. */
|
|
42
|
+
export declare function hasAdvancedFilterEngine(): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let engine = $state(null);
|
|
2
|
+
/** Register the engine. Enterprise calls this from `enableAdvancedFilter()`. */
|
|
3
|
+
export function registerAdvancedFilterEngine(fn) {
|
|
4
|
+
engine = fn;
|
|
5
|
+
}
|
|
6
|
+
/** The registered engine, or null when enterprise is not installed. */
|
|
7
|
+
export function getAdvancedFilterEngine() {
|
|
8
|
+
return engine;
|
|
9
|
+
}
|
|
10
|
+
/** Whether an advanced-filter engine has been registered. */
|
|
11
|
+
export function hasAdvancedFilterEngine() {
|
|
12
|
+
return engine != null;
|
|
13
|
+
}
|