@ui-organized/svelte-table 0.0.0-stage → 6.0.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.
- package/LICENSE +201 -0
- package/README.md +99 -2
- package/dist/DataTable/DataTable.svelte +62 -0
- package/dist/DataTable/DataTable.svelte.d.ts +29 -0
- package/dist/DataTable/DataTable.svelte.d.ts.map +1 -0
- package/dist/DataTable/DataTable.test.svelte.d.ts +2 -0
- package/dist/DataTable/DataTable.test.svelte.d.ts.map +1 -0
- package/dist/DataTable/DataTable.test.svelte.js +360 -0
- package/dist/DataTable/index.d.ts +2 -0
- package/dist/DataTable/index.d.ts.map +1 -0
- package/dist/DataTable/index.js +1 -0
- package/dist/components/Table/CellEditor.svelte +29 -0
- package/dist/components/Table/CellEditor.svelte.d.ts +8 -0
- package/dist/components/Table/CellEditor.svelte.d.ts.map +1 -0
- package/dist/components/Table/MoveButtons.svelte +60 -0
- package/dist/components/Table/MoveButtons.svelte.d.ts +8 -0
- package/dist/components/Table/MoveButtons.svelte.d.ts.map +1 -0
- package/dist/components/Table/ResizeHandle.svelte +82 -0
- package/dist/components/Table/ResizeHandle.svelte.d.ts +28 -0
- package/dist/components/Table/ResizeHandle.svelte.d.ts.map +1 -0
- package/dist/components/Table/Table.svelte +17 -0
- package/dist/components/Table/Table.svelte.d.ts +9 -0
- package/dist/components/Table/Table.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableBody.svelte +78 -0
- package/dist/components/Table/TableBody.svelte.d.ts +30 -0
- package/dist/components/Table/TableBody.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableCell.svelte +106 -0
- package/dist/components/Table/TableCell.svelte.d.ts +34 -0
- package/dist/components/Table/TableCell.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableFooter.svelte +34 -0
- package/dist/components/Table/TableFooter.svelte.d.ts +9 -0
- package/dist/components/Table/TableFooter.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableHeadCell.svelte +157 -0
- package/dist/components/Table/TableHeadCell.svelte.d.ts +29 -0
- package/dist/components/Table/TableHeadCell.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableHeader.svelte +22 -0
- package/dist/components/Table/TableHeader.svelte.d.ts +7 -0
- package/dist/components/Table/TableHeader.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableRow.svelte +84 -0
- package/dist/components/Table/TableRow.svelte.d.ts +32 -0
- package/dist/components/Table/TableRow.svelte.d.ts.map +1 -0
- package/dist/components/Table/TableViewport.svelte +69 -0
- package/dist/components/Table/TableViewport.svelte.d.ts +10 -0
- package/dist/components/Table/TableViewport.svelte.d.ts.map +1 -0
- package/dist/components/Table/index.d.ts +23 -0
- package/dist/components/Table/index.d.ts.map +1 -0
- package/dist/components/Table/index.js +22 -0
- package/dist/components/TableCards/TableCard.svelte +96 -0
- package/dist/components/TableCards/TableCard.svelte.d.ts +31 -0
- package/dist/components/TableCards/TableCard.svelte.d.ts.map +1 -0
- package/dist/components/TableCards/TableCards.svelte +61 -0
- package/dist/components/TableCards/TableCards.svelte.d.ts +7 -0
- package/dist/components/TableCards/TableCards.svelte.d.ts.map +1 -0
- package/dist/components/TableCards/index.d.ts +3 -0
- package/dist/components/TableCards/index.d.ts.map +1 -0
- package/dist/components/TableCards/index.js +2 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte +130 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte.d.ts +8 -0
- package/dist/components/TableDetailSheet/TableDetailSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableDetailSheet/index.d.ts +2 -0
- package/dist/components/TableDetailSheet/index.d.ts.map +1 -0
- package/dist/components/TableDetailSheet/index.js +1 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte +89 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte.d.ts +15 -0
- package/dist/components/TableFilters/TableFilterAdd.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterChip.svelte +164 -0
- package/dist/components/TableFilters/TableFilterChip.svelte.d.ts +9 -0
- package/dist/components/TableFilters/TableFilterChip.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte +40 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte.d.ts +9 -0
- package/dist/components/TableFilters/TableFilterEditor.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte +209 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte.d.ts +10 -0
- package/dist/components/TableFilters/TableFilterValueEditor.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/TableFilters.svelte +51 -0
- package/dist/components/TableFilters/TableFilters.svelte.d.ts +7 -0
- package/dist/components/TableFilters/TableFilters.svelte.d.ts.map +1 -0
- package/dist/components/TableFilters/index.d.ts +7 -0
- package/dist/components/TableFilters/index.d.ts.map +1 -0
- package/dist/components/TableFilters/index.js +6 -0
- package/dist/components/TableFilters/useAutoFocusField.d.ts +30 -0
- package/dist/components/TableFilters/useAutoFocusField.d.ts.map +1 -0
- package/dist/components/TableFilters/useAutoFocusField.js +59 -0
- package/dist/components/TablePagination/TablePagination.svelte +61 -0
- package/dist/components/TablePagination/TablePagination.svelte.d.ts +9 -0
- package/dist/components/TablePagination/TablePagination.svelte.d.ts.map +1 -0
- package/dist/components/TablePagination/index.d.ts +2 -0
- package/dist/components/TablePagination/index.d.ts.map +1 -0
- package/dist/components/TablePagination/index.js +1 -0
- package/dist/components/TableRowActions/TableRowActions.svelte +68 -0
- package/dist/components/TableRowActions/TableRowActions.svelte.d.ts +37 -0
- package/dist/components/TableRowActions/TableRowActions.svelte.d.ts.map +1 -0
- package/dist/components/TableRowActions/index.d.ts +2 -0
- package/dist/components/TableRowActions/index.d.ts.map +1 -0
- package/dist/components/TableRowActions/index.js +1 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte +88 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte.d.ts +30 -0
- package/dist/components/TableSelectionBar/TableSelectionBar.svelte.d.ts.map +1 -0
- package/dist/components/TableSelectionBar/index.d.ts +2 -0
- package/dist/components/TableSelectionBar/index.d.ts.map +1 -0
- package/dist/components/TableSelectionBar/index.js +1 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte +38 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte.d.ts +7 -0
- package/dist/components/TableSelectionFooter/TableSelectionFooter.svelte.d.ts.map +1 -0
- package/dist/components/TableSelectionFooter/index.d.ts +2 -0
- package/dist/components/TableSelectionFooter/index.d.ts.map +1 -0
- package/dist/components/TableSelectionFooter/index.js +1 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte +150 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte.d.ts +8 -0
- package/dist/components/TableSheets/TableFilterSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte +53 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte.d.ts +9 -0
- package/dist/components/TableSheets/TableFilterSheetCondition.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableSheetButton.svelte +44 -0
- package/dist/components/TableSheets/TableSheetButton.svelte.d.ts +15 -0
- package/dist/components/TableSheets/TableSheetButton.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/TableSortSheet.svelte +100 -0
- package/dist/components/TableSheets/TableSortSheet.svelte.d.ts +8 -0
- package/dist/components/TableSheets/TableSortSheet.svelte.d.ts.map +1 -0
- package/dist/components/TableSheets/index.d.ts +4 -0
- package/dist/components/TableSheets/index.d.ts.map +1 -0
- package/dist/components/TableSheets/index.js +3 -0
- package/dist/components/TableStates/TableEmpty.svelte +35 -0
- package/dist/components/TableStates/TableEmpty.svelte.d.ts +11 -0
- package/dist/components/TableStates/TableEmpty.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/TableError.svelte +26 -0
- package/dist/components/TableStates/TableError.svelte.d.ts +10 -0
- package/dist/components/TableStates/TableError.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/TableLoading.svelte +33 -0
- package/dist/components/TableStates/TableLoading.svelte.d.ts +8 -0
- package/dist/components/TableStates/TableLoading.svelte.d.ts.map +1 -0
- package/dist/components/TableStates/index.d.ts +4 -0
- package/dist/components/TableStates/index.d.ts.map +1 -0
- package/dist/components/TableStates/index.js +3 -0
- package/dist/components/TableSubBar/TableSubBar.svelte +34 -0
- package/dist/components/TableSubBar/TableSubBar.svelte.d.ts +7 -0
- package/dist/components/TableSubBar/TableSubBar.svelte.d.ts.map +1 -0
- package/dist/components/TableSubBar/index.d.ts +2 -0
- package/dist/components/TableSubBar/index.d.ts.map +1 -0
- package/dist/components/TableSubBar/index.js +1 -0
- package/dist/components/TableToolbar/TableActions.svelte +40 -0
- package/dist/components/TableToolbar/TableActions.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableActions.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte +60 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableExportMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte +27 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte.d.ts +7 -0
- package/dist/components/TableToolbar/TableReorderToggle.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte +54 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableScrollButtons.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableSearch.svelte +24 -0
- package/dist/components/TableToolbar/TableSearch.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableSearch.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte +97 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableSortMenu.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableToolbar.svelte +113 -0
- package/dist/components/TableToolbar/TableToolbar.svelte.d.ts +9 -0
- package/dist/components/TableToolbar/TableToolbar.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte +69 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte.d.ts +8 -0
- package/dist/components/TableToolbar/TableViewOptions.svelte.d.ts.map +1 -0
- package/dist/components/TableToolbar/index.d.ts +9 -0
- package/dist/components/TableToolbar/index.d.ts.map +1 -0
- package/dist/components/TableToolbar/index.js +8 -0
- package/dist/core/ActionsHeader.svelte +8 -0
- package/dist/core/ActionsHeader.svelte.d.ts +27 -0
- package/dist/core/ActionsHeader.svelte.d.ts.map +1 -0
- package/dist/core/SelectAllCell.svelte +21 -0
- package/dist/core/SelectAllCell.svelte.d.ts +19 -0
- package/dist/core/SelectAllCell.svelte.d.ts.map +1 -0
- package/dist/core/SelectCell.svelte +37 -0
- package/dist/core/SelectCell.svelte.d.ts +8 -0
- package/dist/core/SelectCell.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.svelte.d.ts +4 -0
- package/dist/core/createDataTable.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.svelte.js +1152 -0
- package/dist/core/createDataTable.test.svelte.d.ts +2 -0
- package/dist/core/createDataTable.test.svelte.d.ts.map +1 -0
- package/dist/core/createDataTable.test.svelte.js +156 -0
- package/dist/core/createDeferred.svelte.d.ts +38 -0
- package/dist/core/createDeferred.svelte.d.ts.map +1 -0
- package/dist/core/createDeferred.svelte.js +81 -0
- package/dist/core/elementRef.d.ts +17 -0
- package/dist/core/elementRef.d.ts.map +1 -0
- package/dist/core/elementRef.js +20 -0
- package/dist/core/props.d.ts +3 -0
- package/dist/core/props.d.ts.map +1 -0
- package/dist/core/props.js +46 -0
- package/dist/core/systemColumns.d.ts +16 -0
- package/dist/core/systemColumns.d.ts.map +1 -0
- package/dist/core/systemColumns.js +53 -0
- package/dist/core/tableContext.d.ts +7 -0
- package/dist/core/tableContext.d.ts.map +1 -0
- package/dist/core/tableContext.js +30 -0
- package/dist/core/trackStore.svelte.d.ts +24 -0
- package/dist/core/trackStore.svelte.d.ts.map +1 -0
- package/dist/core/trackStore.svelte.js +28 -0
- package/dist/core/types.d.ts +397 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/styles.css +895 -0
- package/package.json +77 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One condition, as a segmented chip (Figma 2462:2636): "Name | is greater than
|
|
3
|
+
| 30". Each segment opens the control for the one parameter it shows — the
|
|
4
|
+
field menu, the operator menu, the value editor — so changing a filter is one
|
|
5
|
+
click on the part you mean rather than a trip through a form.
|
|
6
|
+
|
|
7
|
+
Removal lives in the field menu and at the foot of the value editor, not in a
|
|
8
|
+
dismiss button: a fourth 24px target in a row of segments is a coin-flip on
|
|
9
|
+
touch, and an operator that takes no value ("is empty") has no value editor to
|
|
10
|
+
put it in.
|
|
11
|
+
-->
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import { clsx } from "clsx";
|
|
14
|
+
import {
|
|
15
|
+
Chip,
|
|
16
|
+
ChipSegment,
|
|
17
|
+
Menu,
|
|
18
|
+
MenuContent,
|
|
19
|
+
MenuItem,
|
|
20
|
+
MenuRadioGroup,
|
|
21
|
+
MenuRadioItem,
|
|
22
|
+
MenuSeparator,
|
|
23
|
+
MenuTrigger,
|
|
24
|
+
Popover,
|
|
25
|
+
PopoverContent,
|
|
26
|
+
PopoverTitle,
|
|
27
|
+
PopoverTrigger,
|
|
28
|
+
} from "@ui-organized/svelte";
|
|
29
|
+
import { afterRowEntrance, type TableFilterCondition } from "@ui-organized/table-core";
|
|
30
|
+
import { getTable } from "../../core/tableContext.js";
|
|
31
|
+
import { autoFocusField, focusAfterRemoval } from "./useAutoFocusField.js";
|
|
32
|
+
import TableFilterEditor from "./TableFilterEditor.svelte";
|
|
33
|
+
import { internalGlyph as glyph } from "@ui-organized/svelte";
|
|
34
|
+
|
|
35
|
+
let { condition, class: className }: { condition: TableFilterCondition; class?: string } =
|
|
36
|
+
$props();
|
|
37
|
+
const table = getTable();
|
|
38
|
+
|
|
39
|
+
// Which chip's editor is open is filter state, not chip state: the chip that
|
|
40
|
+
// opens is often the one the *header* just created, and removing an open
|
|
41
|
+
// condition has to close its editor rather than leave a dangling id behind.
|
|
42
|
+
const editing = $derived(table.filters.editing === condition.id);
|
|
43
|
+
const setOpen = (next: boolean) => table.filters.setEditing(next ? condition.id : null);
|
|
44
|
+
// The first chip arrives inside a row that is still animating open, so the
|
|
45
|
+
// editor waits for the row to settle rather than open against a moving
|
|
46
|
+
// anchor and jump. An existing chip settles at once.
|
|
47
|
+
let settled = $state(false);
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!editing) {
|
|
50
|
+
settled = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const chip = document.querySelector(`[data-filter-id="${CSS.escape(condition.id)}"]`);
|
|
54
|
+
return afterRowEntrance(chip, () => {
|
|
55
|
+
settled = true;
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
const open = $derived(editing && settled);
|
|
59
|
+
// The two menus are this chip's alone, so they are local.
|
|
60
|
+
let menu = $state<"field" | "operator" | null>(null);
|
|
61
|
+
const setMenu = (name: "field" | "operator") => (next: boolean) => {
|
|
62
|
+
menu = next ? name : null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// The condition as it was when the editor opened, so Escape can revert it.
|
|
66
|
+
let snapshot = $state<TableFilterCondition | null>(null);
|
|
67
|
+
let editorEl = $state<HTMLElement | null>(null);
|
|
68
|
+
|
|
69
|
+
$effect(() => autoFocusField(editorEl, open));
|
|
70
|
+
|
|
71
|
+
$effect(() => {
|
|
72
|
+
// Only when the editor opens — capturing on every change would defeat it.
|
|
73
|
+
if (open) snapshot = condition;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const description = $derived(table.filters.describe(condition));
|
|
77
|
+
const takesValue = $derived(table.filters.arityOf(condition) !== "none");
|
|
78
|
+
|
|
79
|
+
const remove = () => {
|
|
80
|
+
table.filters.remove(condition.id);
|
|
81
|
+
focusAfterRemoval(condition.id);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
85
|
+
if (event.key !== "Escape" || !snapshot) return;
|
|
86
|
+
// Ark closes on Escape anyway; this puts the value back first, so Escape
|
|
87
|
+
// abandons the edit rather than committing it.
|
|
88
|
+
table.filters.restore(snapshot);
|
|
89
|
+
};
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<!--
|
|
93
|
+
The group's name is the whole sentence, so a screen reader moving into any one
|
|
94
|
+
segment hears what the filter *is* before what the button does.
|
|
95
|
+
-->
|
|
96
|
+
<Chip
|
|
97
|
+
class={clsx("data-table__filter-chip", className)}
|
|
98
|
+
aria-label={`Filter: ${description.text}`}
|
|
99
|
+
selected={editing || menu !== null}
|
|
100
|
+
incomplete={!description.complete}
|
|
101
|
+
data-column-id={condition.columnId}
|
|
102
|
+
data-filter-id={condition.id}
|
|
103
|
+
>
|
|
104
|
+
<Menu open={menu === "field"} onOpenChange={setMenu("field")}>
|
|
105
|
+
<MenuTrigger>
|
|
106
|
+
{#snippet asChild(props)}
|
|
107
|
+
<ChipSegment {...(props() as Record<string, never>)} part="label">{description.field}</ChipSegment>
|
|
108
|
+
{/snippet}
|
|
109
|
+
</MenuTrigger>
|
|
110
|
+
<MenuContent class="data-table__filter-picker">
|
|
111
|
+
<MenuRadioGroup
|
|
112
|
+
value={condition.columnId}
|
|
113
|
+
onValueChange={(columnId) => table.filters.changeField(condition.id, columnId)}
|
|
114
|
+
>
|
|
115
|
+
{#each table.filters.fields as field (field.columnId)}
|
|
116
|
+
<MenuRadioItem value={field.columnId}>{field.label}</MenuRadioItem>
|
|
117
|
+
{/each}
|
|
118
|
+
</MenuRadioGroup>
|
|
119
|
+
<MenuSeparator />
|
|
120
|
+
<MenuItem value="remove" icon={glyph("trash")} onSelect={remove}>Remove filter</MenuItem>
|
|
121
|
+
</MenuContent>
|
|
122
|
+
</Menu>
|
|
123
|
+
|
|
124
|
+
<Menu open={menu === "operator"} onOpenChange={setMenu("operator")}>
|
|
125
|
+
<MenuTrigger>
|
|
126
|
+
{#snippet asChild(props)}
|
|
127
|
+
<ChipSegment {...(props() as Record<string, never>)} part="operator">{description.relative}</ChipSegment>
|
|
128
|
+
{/snippet}
|
|
129
|
+
</MenuTrigger>
|
|
130
|
+
<MenuContent class="data-table__filter-picker">
|
|
131
|
+
<MenuRadioGroup
|
|
132
|
+
value={condition.operator}
|
|
133
|
+
onValueChange={(operator) => table.filters.update(condition.id, { operator })}
|
|
134
|
+
>
|
|
135
|
+
{#each table.filters.operatorsFor(condition) as operator (operator.id)}
|
|
136
|
+
<MenuRadioItem value={operator.id}>{operator.label}</MenuRadioItem>
|
|
137
|
+
{/each}
|
|
138
|
+
</MenuRadioGroup>
|
|
139
|
+
</MenuContent>
|
|
140
|
+
</Menu>
|
|
141
|
+
|
|
142
|
+
{#if takesValue}
|
|
143
|
+
<Popover {open} onOpenChange={setOpen}>
|
|
144
|
+
<PopoverTrigger>
|
|
145
|
+
{#snippet asChild(props)}
|
|
146
|
+
<ChipSegment {...(props() as Record<string, never>)} part="value">{description.value}</ChipSegment>
|
|
147
|
+
{/snippet}
|
|
148
|
+
</PopoverTrigger>
|
|
149
|
+
<PopoverContent
|
|
150
|
+
class="data-table__filter-editor-popup"
|
|
151
|
+
side="bottom"
|
|
152
|
+
align="start"
|
|
153
|
+
onkeydown={onKeyDown}
|
|
154
|
+
>
|
|
155
|
+
<div bind:this={editorEl} class="data-table__filter-editor">
|
|
156
|
+
<PopoverTitle class="data-table__filter-editor-title"
|
|
157
|
+
>{description.field} {description.relative}</PopoverTitle
|
|
158
|
+
>
|
|
159
|
+
<TableFilterEditor {condition} portalContainer={editorEl} />
|
|
160
|
+
</div>
|
|
161
|
+
</PopoverContent>
|
|
162
|
+
</Popover>
|
|
163
|
+
{/if}
|
|
164
|
+
</Chip>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type TableFilterCondition } from "@ui-organized/table-core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
condition: TableFilterCondition;
|
|
4
|
+
class?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const TableFilterChip: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type TableFilterChip = ReturnType<typeof TableFilterChip>;
|
|
8
|
+
export default TableFilterChip;
|
|
9
|
+
//# sourceMappingURL=TableFilterChip.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableFilterChip.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/TableFilterChip.svelte.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAoB,KAAK,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAMtF,KAAK,gBAAgB,GAAI;IAAE,SAAS,EAAE,oBAAoB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAyH9E,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
One condition's value editor (Figma 2298:726): the value, then a rule and a
|
|
3
|
+
remove. The field and the operator have segments of their own on the chip, so
|
|
4
|
+
the editor carries only what the value segment opens.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { Button, Divider } from "@ui-organized/svelte";
|
|
8
|
+
import type { TableFilterCondition } from "@ui-organized/table-core";
|
|
9
|
+
import { getTable } from "../../core/tableContext.js";
|
|
10
|
+
import { focusAfterRemoval } from "./useAutoFocusField.js";
|
|
11
|
+
import TableFilterValueEditor from "./TableFilterValueEditor.svelte";
|
|
12
|
+
import { internalGlyph as glyph } from "@ui-organized/svelte";
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
condition,
|
|
16
|
+
portalContainer,
|
|
17
|
+
}: { condition: TableFilterCondition; portalContainer?: HTMLElement | null } = $props();
|
|
18
|
+
|
|
19
|
+
const table = getTable();
|
|
20
|
+
const arity = $derived(table.filters.arityOf(condition));
|
|
21
|
+
|
|
22
|
+
const remove = () => {
|
|
23
|
+
table.filters.remove(condition.id);
|
|
24
|
+
focusAfterRemoval(condition.id);
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<TableFilterValueEditor {condition} {arity} {portalContainer} />
|
|
29
|
+
|
|
30
|
+
<Divider class="data-table__filter-editor-rule" />
|
|
31
|
+
|
|
32
|
+
<Button
|
|
33
|
+
intent="ghost"
|
|
34
|
+
size={table.size}
|
|
35
|
+
icon={glyph("trash")}
|
|
36
|
+
class="data-table__filter-editor-remove"
|
|
37
|
+
onclick={remove}
|
|
38
|
+
>
|
|
39
|
+
Remove
|
|
40
|
+
</Button>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TableFilterCondition } from "@ui-organized/table-core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
condition: TableFilterCondition;
|
|
4
|
+
portalContainer?: HTMLElement | null;
|
|
5
|
+
};
|
|
6
|
+
declare const TableFilterEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type TableFilterEditor = ReturnType<typeof TableFilterEditor>;
|
|
8
|
+
export default TableFilterEditor;
|
|
9
|
+
//# sourceMappingURL=TableFilterEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableFilterEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/TableFilterEditor.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAMpE,KAAK,gBAAgB,GAAI;IAAE,SAAS,EAAE,oBAAoB,CAAC;IAAC,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAAC;AAkCpG,QAAA,MAAM,iBAAiB,sDAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The value half of a condition editor.
|
|
3
|
+
|
|
4
|
+
Which control appears follows from **type and operator together**, never from
|
|
5
|
+
the type alone — that is the whole point of separating the two. A date column
|
|
6
|
+
shows one date field for "is before", two for "is between", and a number plus a
|
|
7
|
+
unit for "is in the last".
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { clsx } from "clsx";
|
|
11
|
+
import {
|
|
12
|
+
DateInput,
|
|
13
|
+
Input,
|
|
14
|
+
Listbox,
|
|
15
|
+
NumberField,
|
|
16
|
+
SearchInput,
|
|
17
|
+
SegmentedControl,
|
|
18
|
+
Select,
|
|
19
|
+
} from "@ui-organized/svelte";
|
|
20
|
+
import {
|
|
21
|
+
searchOptions,
|
|
22
|
+
type FilterOperatorArity,
|
|
23
|
+
type TableFilterCondition,
|
|
24
|
+
type TableFilterValue,
|
|
25
|
+
} from "@ui-organized/table-core";
|
|
26
|
+
import { getTable } from "../../core/tableContext.js";
|
|
27
|
+
|
|
28
|
+
/** Options above this many get a search box — the article's "search in the panel". */
|
|
29
|
+
const SEARCHABLE_ABOVE = 8;
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
condition,
|
|
33
|
+
arity,
|
|
34
|
+
portalContainer,
|
|
35
|
+
}: {
|
|
36
|
+
condition: TableFilterCondition;
|
|
37
|
+
arity: FilterOperatorArity;
|
|
38
|
+
portalContainer?: HTMLElement | null;
|
|
39
|
+
} = $props();
|
|
40
|
+
|
|
41
|
+
const table = getTable();
|
|
42
|
+
let query = $state("");
|
|
43
|
+
const type = $derived(table.filters.typeOf(condition));
|
|
44
|
+
const size = $derived(table.size);
|
|
45
|
+
|
|
46
|
+
const set = (values: TableFilterValue[]) => table.filters.update(condition.id, { values });
|
|
47
|
+
const setAt = (index: number, value: TableFilterValue) => {
|
|
48
|
+
const values = [...condition.values];
|
|
49
|
+
while (values.length <= index) values.push(null);
|
|
50
|
+
values[index] = value;
|
|
51
|
+
set(values);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const booleanValue = $derived.by(() => {
|
|
55
|
+
const first = condition.values[0];
|
|
56
|
+
return first === null || first === undefined ? undefined : String(first);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const options = $derived(table.filters.optionsFor(condition));
|
|
60
|
+
const searchable = $derived(options.length > SEARCHABLE_ABOVE);
|
|
61
|
+
const visible = $derived(searchOptions(options, query));
|
|
62
|
+
const selected = $derived(condition.values.map(String));
|
|
63
|
+
|
|
64
|
+
const numberAt = (index: number) =>
|
|
65
|
+
typeof condition.values[index] === "number" ? (condition.values[index] as number) : null;
|
|
66
|
+
const stringAt = (index: number) => String(condition.values[index] ?? "");
|
|
67
|
+
|
|
68
|
+
const UNITS = [
|
|
69
|
+
{ value: "day", label: "days" },
|
|
70
|
+
{ value: "week", label: "weeks" },
|
|
71
|
+
{ value: "month", label: "months" },
|
|
72
|
+
{ value: "year", label: "years" },
|
|
73
|
+
];
|
|
74
|
+
const relative = $derived(
|
|
75
|
+
condition.operator === "in-last" || condition.operator === "in-next",
|
|
76
|
+
);
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<!-- `is empty` and `is today` take nothing, so there is nothing to render. -->
|
|
80
|
+
{#if arity === "none"}
|
|
81
|
+
<!-- nothing -->
|
|
82
|
+
{:else if type === "boolean"}
|
|
83
|
+
<SegmentedControl
|
|
84
|
+
{size}
|
|
85
|
+
aria-label="Value"
|
|
86
|
+
items={[
|
|
87
|
+
{ value: "true", label: "Yes" },
|
|
88
|
+
{ value: "false", label: "No" },
|
|
89
|
+
]}
|
|
90
|
+
value={booleanValue}
|
|
91
|
+
onValueChange={(value) => set([value === "true"])}
|
|
92
|
+
/>
|
|
93
|
+
{:else if type === "enum"}
|
|
94
|
+
{#if searchable}
|
|
95
|
+
<SearchInput
|
|
96
|
+
{size}
|
|
97
|
+
aria-label="Search values"
|
|
98
|
+
value={query}
|
|
99
|
+
oninput={(event) => (query = event.currentTarget.value)}
|
|
100
|
+
onClear={() => (query = "")}
|
|
101
|
+
/>
|
|
102
|
+
{/if}
|
|
103
|
+
<!--
|
|
104
|
+
`label`, not `aria-label`: Listbox has a closed prop API, so an `aria-label`
|
|
105
|
+
is silently dropped and the listbox ships unnamed — an
|
|
106
|
+
`aria-input-field-name` violation that only shows up with the popover open,
|
|
107
|
+
which the story-level axe gate never sees.
|
|
108
|
+
|
|
109
|
+
The count rides in the label because `ListboxOption.label` is a string —
|
|
110
|
+
which is also exactly what a screen reader reads out. A zero-count option
|
|
111
|
+
would filter to nothing, and is never disabled while selected or the user
|
|
112
|
+
could not untick it.
|
|
113
|
+
-->
|
|
114
|
+
<Listbox
|
|
115
|
+
{size}
|
|
116
|
+
class="data-table__filter-field data-table__filter-field--unlabelled"
|
|
117
|
+
selectionMode={arity === "many" ? "multiple" : "single"}
|
|
118
|
+
label="Value"
|
|
119
|
+
options={visible.map((option) => ({
|
|
120
|
+
value: option.value,
|
|
121
|
+
label: option.count > 0 ? `${option.label} · ${option.count}` : option.label,
|
|
122
|
+
disabled: option.disabled,
|
|
123
|
+
}))}
|
|
124
|
+
value={selected}
|
|
125
|
+
emptyMessage="No matching values"
|
|
126
|
+
onValueChange={(values) => set(arity === "many" ? values : values.slice(0, 1))}
|
|
127
|
+
/>
|
|
128
|
+
{:else if type === "date"}
|
|
129
|
+
<!-- "in the last 7 days" — a count and a unit, not a date. -->
|
|
130
|
+
{#if relative}
|
|
131
|
+
<div class="data-table__filter-editor-value">
|
|
132
|
+
<NumberField
|
|
133
|
+
{size}
|
|
134
|
+
label="Amount"
|
|
135
|
+
min={1}
|
|
136
|
+
value={numberAt(0)}
|
|
137
|
+
onValueChange={(value) => setAt(0, value)}
|
|
138
|
+
/>
|
|
139
|
+
<Select
|
|
140
|
+
{size}
|
|
141
|
+
label="Unit"
|
|
142
|
+
{portalContainer}
|
|
143
|
+
value={condition.unit ?? "day"}
|
|
144
|
+
options={UNITS}
|
|
145
|
+
onValueChange={(unit) =>
|
|
146
|
+
table.filters.update(condition.id, {
|
|
147
|
+
unit: unit as "day" | "week" | "month" | "year",
|
|
148
|
+
})}
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
{:else}
|
|
152
|
+
<div class="data-table__filter-editor-value">
|
|
153
|
+
<!-- Into the editor, like the Select above: the calendar portalled to
|
|
154
|
+
<body> sits on the same popover tier as this editor, and the editor,
|
|
155
|
+
mounted later, painted over it. -->
|
|
156
|
+
<DateInput
|
|
157
|
+
{size}
|
|
158
|
+
label={arity === "two" ? "From" : "Date"}
|
|
159
|
+
{portalContainer}
|
|
160
|
+
class={clsx(
|
|
161
|
+
"data-table__filter-field",
|
|
162
|
+
arity !== "two" && "data-table__filter-field--unlabelled",
|
|
163
|
+
)}
|
|
164
|
+
value={stringAt(0)}
|
|
165
|
+
oninput={(event) => setAt(0, event.currentTarget.value || null)}
|
|
166
|
+
/>
|
|
167
|
+
{#if arity === "two"}
|
|
168
|
+
<DateInput
|
|
169
|
+
{size}
|
|
170
|
+
label="To"
|
|
171
|
+
{portalContainer}
|
|
172
|
+
value={stringAt(1)}
|
|
173
|
+
oninput={(event) => setAt(1, event.currentTarget.value || null)}
|
|
174
|
+
/>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
{/if}
|
|
178
|
+
{:else if type === "number"}
|
|
179
|
+
<div class="data-table__filter-editor-value">
|
|
180
|
+
<NumberField
|
|
181
|
+
{size}
|
|
182
|
+
label={arity === "two" ? "Minimum" : "Value"}
|
|
183
|
+
class={clsx(
|
|
184
|
+
"data-table__filter-field",
|
|
185
|
+
arity !== "two" && "data-table__filter-field--unlabelled",
|
|
186
|
+
)}
|
|
187
|
+
placeholder={arity === "two" ? undefined : "Data"}
|
|
188
|
+
value={numberAt(0)}
|
|
189
|
+
onValueChange={(value) => setAt(0, value)}
|
|
190
|
+
/>
|
|
191
|
+
{#if arity === "two"}
|
|
192
|
+
<NumberField
|
|
193
|
+
{size}
|
|
194
|
+
label="Maximum"
|
|
195
|
+
value={numberAt(1)}
|
|
196
|
+
onValueChange={(value) => setAt(1, value)}
|
|
197
|
+
/>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
{:else}
|
|
201
|
+
<Input
|
|
202
|
+
{size}
|
|
203
|
+
label="Value"
|
|
204
|
+
class="data-table__filter-field data-table__filter-field--unlabelled"
|
|
205
|
+
placeholder="Data"
|
|
206
|
+
value={stringAt(0)}
|
|
207
|
+
oninput={(event) => setAt(0, event.currentTarget.value)}
|
|
208
|
+
/>
|
|
209
|
+
{/if}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type FilterOperatorArity, type TableFilterCondition } from "@ui-organized/table-core";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
condition: TableFilterCondition;
|
|
4
|
+
arity: FilterOperatorArity;
|
|
5
|
+
portalContainer?: HTMLElement | null;
|
|
6
|
+
};
|
|
7
|
+
declare const TableFilterValueEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type TableFilterValueEditor = ReturnType<typeof TableFilterValueEditor>;
|
|
9
|
+
export default TableFilterValueEditor;
|
|
10
|
+
//# sourceMappingURL=TableFilterValueEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableFilterValueEditor.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/TableFilterValueEditor.svelte.ts"],"names":[],"mappings":"AAaA,OAAO,EAEH,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAE1B,MAAM,0BAA0B,CAAC;AAGnC,KAAK,gBAAgB,GAAI;IACtB,SAAS,EAAE,oBAAoB,CAAC;IAChC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACtC,CAAC;AA+GJ,QAAA,MAAM,sBAAsB,sDAAwC,CAAC;AACrE,KAAK,sBAAsB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACxE,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
The applied filters, on their own line under the header (Figma 2298:362).
|
|
3
|
+
|
|
4
|
+
A *summary*, not a control panel: the filter button lives in the header above,
|
|
5
|
+
and this line does not exist until the user has actually filtered something. A
|
|
6
|
+
bar that is always present and usually empty is chrome that teaches people to
|
|
7
|
+
stop looking at it.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { clsx } from "clsx";
|
|
11
|
+
import { Button } from "@ui-organized/svelte";
|
|
12
|
+
import { getTable } from "../../core/tableContext.js";
|
|
13
|
+
import TableFilterAdd from "./TableFilterAdd.svelte";
|
|
14
|
+
import TableFilterChip from "./TableFilterChip.svelte";
|
|
15
|
+
import { internalGlyph as glyph } from "@ui-organized/svelte";
|
|
16
|
+
|
|
17
|
+
let { class: className }: { class?: string } = $props();
|
|
18
|
+
const table = getTable();
|
|
19
|
+
const conditions = $derived(table.filters.conditions);
|
|
20
|
+
const render = $derived(table.options.filterable !== false && conditions.length > 0);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<!--
|
|
24
|
+
`role="group"`, not `role="toolbar"`: toolbar promises arrow-key navigation
|
|
25
|
+
between its controls, and until that lands the promise would be a lie to
|
|
26
|
+
assistive tech.
|
|
27
|
+
-->
|
|
28
|
+
{#if render}
|
|
29
|
+
<div class={clsx("data-table__filters", className)} role="group" aria-label="Filters">
|
|
30
|
+
<span class="data-table__filters-label">Filters</span>
|
|
31
|
+
|
|
32
|
+
{#each conditions as condition (condition.id)}
|
|
33
|
+
<TableFilterChip {condition} />
|
|
34
|
+
{/each}
|
|
35
|
+
|
|
36
|
+
<TableFilterAdd label="Add" />
|
|
37
|
+
|
|
38
|
+
<Button intent="ghost" size="sm" icon={glyph("rotate-ccw")} onclick={() => table.filters.clear()}>
|
|
39
|
+
Reset
|
|
40
|
+
</Button>
|
|
41
|
+
|
|
42
|
+
<!--
|
|
43
|
+
One polite region for add / remove / change / clear. Deliberately not a
|
|
44
|
+
visible count: that changes on every keystroke elsewhere, and a live region
|
|
45
|
+
that re-announces per keystroke is a firehose.
|
|
46
|
+
-->
|
|
47
|
+
<span class="data-table__sr-only" role="status" aria-live="polite">
|
|
48
|
+
{table.filters.announcement}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
{/if}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
class?: string;
|
|
3
|
+
};
|
|
4
|
+
declare const TableFilters: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
5
|
+
type TableFilters = ReturnType<typeof TableFilters>;
|
|
6
|
+
export default TableFilters;
|
|
7
|
+
//# sourceMappingURL=TableFilters.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableFilters.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/TableFilters.svelte.ts"],"names":[],"mappings":"AAUC,KAAK,gBAAgB,GAAI;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAwC7C,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as TableFilters } from "./TableFilters.svelte";
|
|
2
|
+
export { default as TableFilterAdd } from "./TableFilterAdd.svelte";
|
|
3
|
+
export { default as TableFilterChip } from "./TableFilterChip.svelte";
|
|
4
|
+
export { default as TableFilterEditor } from "./TableFilterEditor.svelte";
|
|
5
|
+
export { default as TableFilterValueEditor } from "./TableFilterValueEditor.svelte";
|
|
6
|
+
export { autoFocusField, focusAfterRemoval } from "./useAutoFocusField.js";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AACpF,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { default as TableFilters } from "./TableFilters.svelte";
|
|
2
|
+
export { default as TableFilterAdd } from "./TableFilterAdd.svelte";
|
|
3
|
+
export { default as TableFilterChip } from "./TableFilterChip.svelte";
|
|
4
|
+
export { default as TableFilterEditor } from "./TableFilterEditor.svelte";
|
|
5
|
+
export { default as TableFilterValueEditor } from "./TableFilterValueEditor.svelte";
|
|
6
|
+
export { autoFocusField, focusAfterRemoval } from "./useAutoFocusField.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put focus in the first typeable field of a popover once it opens.
|
|
3
|
+
*
|
|
4
|
+
* The pattern analysis is explicit about this — "enable autofocus … it's
|
|
5
|
+
* possible for the user to start typing right away" — and the obvious spelling,
|
|
6
|
+
* an `autofocus` attribute, does not work here: Ark moves focus into the popover
|
|
7
|
+
* content *after* mount, so the attribute is overwritten and focus lands on a
|
|
8
|
+
* button.
|
|
9
|
+
*
|
|
10
|
+
* **Two** frames, not one. A single frame is not enough: a focus trace in the
|
|
11
|
+
* React adapter showed this landing correctly and then Ark's `Select` trigger
|
|
12
|
+
* taking focus one millisecond later, because the operator picker is the first
|
|
13
|
+
* focusable child. Waiting a second frame puts this last, which is the only
|
|
14
|
+
* thing that sticks — and it is Ark's behaviour, not React's, so it is the same
|
|
15
|
+
* here.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately only an `<input>`: a text, number, date or search field is
|
|
18
|
+
* something you type into immediately. An enum list without a search box has
|
|
19
|
+
* nothing to type into, so its focus is left exactly where Ark put it.
|
|
20
|
+
*
|
|
21
|
+
* A plain function rather than a rune-bearing helper, so the caller decides
|
|
22
|
+
* which `$effect` it runs in — the chip owns both the element and the open flag.
|
|
23
|
+
*/
|
|
24
|
+
export declare function autoFocusField(container: HTMLElement | null, open: boolean): (() => void) | void;
|
|
25
|
+
/**
|
|
26
|
+
* Focus has to land somewhere deliberate when a chip disappears, or it falls to
|
|
27
|
+
* `<body>` and a keyboard user loses their place entirely.
|
|
28
|
+
*/
|
|
29
|
+
export declare function focusAfterRemoval(removedId: string): void;
|
|
30
|
+
//# sourceMappingURL=useAutoFocusField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAutoFocusField.d.ts","sourceRoot":"","sources":["../../../src/lib/components/TableFilters/useAutoFocusField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAmBhG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CASzD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put focus in the first typeable field of a popover once it opens.
|
|
3
|
+
*
|
|
4
|
+
* The pattern analysis is explicit about this — "enable autofocus … it's
|
|
5
|
+
* possible for the user to start typing right away" — and the obvious spelling,
|
|
6
|
+
* an `autofocus` attribute, does not work here: Ark moves focus into the popover
|
|
7
|
+
* content *after* mount, so the attribute is overwritten and focus lands on a
|
|
8
|
+
* button.
|
|
9
|
+
*
|
|
10
|
+
* **Two** frames, not one. A single frame is not enough: a focus trace in the
|
|
11
|
+
* React adapter showed this landing correctly and then Ark's `Select` trigger
|
|
12
|
+
* taking focus one millisecond later, because the operator picker is the first
|
|
13
|
+
* focusable child. Waiting a second frame puts this last, which is the only
|
|
14
|
+
* thing that sticks — and it is Ark's behaviour, not React's, so it is the same
|
|
15
|
+
* here.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately only an `<input>`: a text, number, date or search field is
|
|
18
|
+
* something you type into immediately. An enum list without a search box has
|
|
19
|
+
* nothing to type into, so its focus is left exactly where Ark put it.
|
|
20
|
+
*
|
|
21
|
+
* A plain function rather than a rune-bearing helper, so the caller decides
|
|
22
|
+
* which `$effect` it runs in — the chip owns both the element and the open flag.
|
|
23
|
+
*/
|
|
24
|
+
export function autoFocusField(container, open) {
|
|
25
|
+
if (!open || !container || typeof requestAnimationFrame !== "function")
|
|
26
|
+
return;
|
|
27
|
+
let inner = 0;
|
|
28
|
+
const outer = requestAnimationFrame(() => {
|
|
29
|
+
inner = requestAnimationFrame(() => {
|
|
30
|
+
const field = container.querySelector('input:not([type="hidden"]):not([disabled])');
|
|
31
|
+
// Already there — Ark's own initial focus can land on the field, and
|
|
32
|
+
// the user may be typing by now. Selecting would eat what they typed.
|
|
33
|
+
if (!field || field === document.activeElement)
|
|
34
|
+
return;
|
|
35
|
+
field.focus();
|
|
36
|
+
field.select();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
return () => {
|
|
40
|
+
cancelAnimationFrame(outer);
|
|
41
|
+
cancelAnimationFrame(inner);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Focus has to land somewhere deliberate when a chip disappears, or it falls to
|
|
46
|
+
* `<body>` and a keyboard user loses their place entirely.
|
|
47
|
+
*/
|
|
48
|
+
export function focusAfterRemoval(removedId) {
|
|
49
|
+
if (typeof requestAnimationFrame !== "function")
|
|
50
|
+
return;
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
const bar = document.querySelector(".data-table__filters");
|
|
53
|
+
if (!bar)
|
|
54
|
+
return;
|
|
55
|
+
const chips = [...bar.querySelectorAll("[data-filter-id]")];
|
|
56
|
+
const next = chips.find((element) => element.dataset.filterId !== removedId);
|
|
57
|
+
(next?.querySelector("button") ?? bar.querySelector("button"))?.focus();
|
|
58
|
+
});
|
|
59
|
+
}
|