@mendylanda/ui 0.4.3-dev.70.eaaf58b8af54 → 0.4.3-dev.72.7ee3a6f295d2
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/filters/filter-bar.d.ts +2 -0
- package/dist/filters/filter-bar.js +19 -3
- package/dist/filters/filter-controller-actions.d.ts +12 -0
- package/dist/filters/filter-controller-actions.js +44 -0
- package/dist/filters/use-buffered-filters.d.ts +6 -0
- package/dist/filters/use-buffered-filters.js +61 -0
- package/dist/filters/use-filter-search.d.ts +10 -3
- package/dist/filters/use-filter-search.js +9 -41
- package/dist/filters/use-filters.js +10 -38
- package/dist/filters/use-search-draft.d.ts +13 -0
- package/dist/filters/use-search-draft.js +71 -0
- package/dist/table/table-view.js +2 -1
- package/dist/table/use-table-interaction.js +30 -4
- package/package.json +2 -2
|
@@ -12,6 +12,8 @@ export interface FilterMenuGroup {
|
|
|
12
12
|
export interface FilterRootProps {
|
|
13
13
|
classNames?: FilterClassNames;
|
|
14
14
|
filters: FilterController;
|
|
15
|
+
/** Apply main search after a typing pause. Set 0 for immediate writes or an existing draft adapter. */
|
|
16
|
+
searchDebounceMs?: number;
|
|
15
17
|
summary?: SummaryPolicy;
|
|
16
18
|
/** Keep the filter menu open for applying more filters by default. */
|
|
17
19
|
closeMenuOnApply?: boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useMendyLocale } from "../locale-context.js";
|
|
4
4
|
import { FilterCollection } from "./filter-collection.js";
|
|
5
|
+
import { useBufferedFilters } from "./use-buffered-filters.js";
|
|
5
6
|
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useId, useRef, useState, useSyncExternalStore, } from "react";
|
|
6
7
|
import { ListFilter, Search, X, Circle, ListChecks, Type, Hash, CalendarDays, SlidersHorizontal, Layers, } from "lucide-react";
|
|
7
8
|
import { Button } from "../customization.js";
|
|
@@ -39,7 +40,8 @@ function useRoot() {
|
|
|
39
40
|
export function FilterRoot({ classNames, ...props }) {
|
|
40
41
|
return (_jsx(MendyUIProvider, { classNames: classNames, children: _jsx(FilterRootContent, { ...props }) }));
|
|
41
42
|
}
|
|
42
|
-
function FilterRootContent({ filters, summary = defaultSummary, suggestions = "always", closeMenuOnApply = false, groups = defaultGroups, disabled: disabledProp = false, className, children, }) {
|
|
43
|
+
function FilterRootContent({ filters: appliedFilters, searchDebounceMs = 300, summary = defaultSummary, suggestions = "always", closeMenuOnApply = false, groups = defaultGroups, disabled: disabledProp = false, className, children, }) {
|
|
44
|
+
const { filters, flushSearch } = useBufferedFilters(appliedFilters, searchDebounceMs);
|
|
43
45
|
const { classNames } = useMendyUI();
|
|
44
46
|
const locale = useMendyLocale();
|
|
45
47
|
// Server HTML must not accept edits that React cannot handle yet. Client-only
|
|
@@ -61,6 +63,7 @@ function FilterRootContent({ filters, summary = defaultSummary, suggestions = "a
|
|
|
61
63
|
const context = useMemo(() => ({
|
|
62
64
|
direction,
|
|
63
65
|
filters,
|
|
66
|
+
flushSearch,
|
|
64
67
|
summary,
|
|
65
68
|
suggestions,
|
|
66
69
|
closeMenuOnApply,
|
|
@@ -70,14 +73,24 @@ function FilterRootContent({ filters, summary = defaultSummary, suggestions = "a
|
|
|
70
73
|
editingField,
|
|
71
74
|
ambiguous,
|
|
72
75
|
setAmbiguous,
|
|
73
|
-
}), [
|
|
76
|
+
}), [
|
|
77
|
+
filters,
|
|
78
|
+
flushSearch,
|
|
79
|
+
summary,
|
|
80
|
+
suggestions,
|
|
81
|
+
closeMenuOnApply,
|
|
82
|
+
groups,
|
|
83
|
+
disabled,
|
|
84
|
+
ambiguous,
|
|
85
|
+
direction,
|
|
86
|
+
]);
|
|
74
87
|
return (_jsx(FilterOptionCache.Provider, { value: cache, children: _jsx(Context.Provider, { value: context, children: _jsx("div", { "data-mendy-ui": "", dir: locale.configured ? locale.direction : undefined, lang: locale.code, "aria-busy": !hydrated || undefined, className: cn("mui-222f930b8752 mui-faa8a23c68f6 mui-71556df3b421 mui-074569488cca", classNames?.root, className), children: children }) }) }));
|
|
75
88
|
}
|
|
76
89
|
export function FilterBar({ showClear = true, ...props }) {
|
|
77
90
|
return (_jsxs(FilterRoot, { ...props, children: [_jsx(FilterSearch, { label: props.searchLabel, placeholder: props.searchPlaceholder }), _jsx(FilterList, { showClear: showClear }), _jsx(FilterFeedback, {})] }));
|
|
78
91
|
}
|
|
79
92
|
export function FilterSearch({ label, placeholder }) {
|
|
80
|
-
const { filters, trigger, disabled, setAmbiguous, direction } = useRoot();
|
|
93
|
+
const { filters, flushSearch, trigger, disabled, setAmbiguous, direction } = useRoot();
|
|
81
94
|
const { t } = useMendyLocale();
|
|
82
95
|
const { classNames } = useMendyUI();
|
|
83
96
|
const anchor = useRef(null);
|
|
@@ -107,6 +120,9 @@ export function FilterSearch({ label, placeholder }) {
|
|
|
107
120
|
const result = filters.paste(event.currentTarget.value, { before: "", after: "" });
|
|
108
121
|
setAmbiguous(result.ambiguous);
|
|
109
122
|
}
|
|
123
|
+
else {
|
|
124
|
+
flushSearch();
|
|
125
|
+
}
|
|
110
126
|
}
|
|
111
127
|
}, onKeyUp: (event) => {
|
|
112
128
|
shift.current = event.shiftKey;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FilterController } from "./use-filters.js";
|
|
2
|
+
/** Shared operations for an applied controller and the search draft shown by FilterRoot. */
|
|
3
|
+
export declare function filterControllerActions(controller: Pick<FilterController, "entries" | "search" | "batch" | "edit" | "setMenuOpen" | "setOpenField">): {
|
|
4
|
+
commit: (id: string, value: unknown, source?: Parameters<FilterController["commit"]>[2]) => string | undefined;
|
|
5
|
+
remove(id: string): void;
|
|
6
|
+
clear(): void;
|
|
7
|
+
setSearch: (text: string) => void;
|
|
8
|
+
paste(text: string, remainder?: {
|
|
9
|
+
before: string;
|
|
10
|
+
after: string;
|
|
11
|
+
}): import("./filter-state.js").PasteResult;
|
|
12
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { classifyPaste } from "./filter-state.js";
|
|
2
|
+
/** Shared operations for an applied controller and the search draft shown by FilterRoot. */
|
|
3
|
+
export function filterControllerActions(controller) {
|
|
4
|
+
const { entries, search, batch, edit, setMenuOpen, setOpenField } = controller;
|
|
5
|
+
return {
|
|
6
|
+
commit: (id, value, source) => batch({ [id]: value }, undefined, source),
|
|
7
|
+
remove(id) {
|
|
8
|
+
const entry = entries.find((item) => item.id === id);
|
|
9
|
+
if (entry?.field.removable !== false) {
|
|
10
|
+
batch({ [id]: entry?.field.clearValue }, undefined, "remove");
|
|
11
|
+
edit(null);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
clear() {
|
|
15
|
+
batch(Object.fromEntries(entries.flatMap((entry) => entry.field.removable !== false && !entry.field.hidden && !entry.field.disabled
|
|
16
|
+
? [[entry.id, entry.field.clearValue]]
|
|
17
|
+
: [])), "", "clear");
|
|
18
|
+
edit(null);
|
|
19
|
+
setMenuOpen(false);
|
|
20
|
+
setOpenField(null);
|
|
21
|
+
},
|
|
22
|
+
setSearch: (text) => {
|
|
23
|
+
batch({}, text, "search");
|
|
24
|
+
},
|
|
25
|
+
paste(text, remainder) {
|
|
26
|
+
const result = classifyPaste(text, entries);
|
|
27
|
+
const unmatched = [...result.unmatched, ...result.ambiguous.map((item) => item.token)].join(" ");
|
|
28
|
+
const nextSearch = remainder
|
|
29
|
+
? [remainder.before, unmatched, remainder.after].filter(Boolean).join(" ")
|
|
30
|
+
: [search, unmatched].filter(Boolean).join(" ");
|
|
31
|
+
let cursor = [remainder ? remainder.before : search, result.unmatched.join(" ")]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" ").length;
|
|
34
|
+
for (const item of result.ambiguous) {
|
|
35
|
+
if (cursor)
|
|
36
|
+
cursor++;
|
|
37
|
+
item.searchRange = { start: cursor, end: cursor + item.token.length };
|
|
38
|
+
cursor += item.token.length;
|
|
39
|
+
}
|
|
40
|
+
batch(result.changes, nextSearch, "paste");
|
|
41
|
+
return result;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FilterController } from "./use-filters.js";
|
|
2
|
+
/** Root-local draft; the caller's controller always exposes the applied search. */
|
|
3
|
+
export declare function useBufferedFilters(filters: FilterController, delay: number): {
|
|
4
|
+
filters: FilterController<unknown>;
|
|
5
|
+
flushSearch: () => void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { filterControllerActions } from "./filter-controller-actions.js";
|
|
3
|
+
import { useSearchDraft } from "./use-search-draft.js";
|
|
4
|
+
const trim = (search) => search.trim();
|
|
5
|
+
function contextValue(field, value) {
|
|
6
|
+
return value == null ? null : field.codec.serialize(value);
|
|
7
|
+
}
|
|
8
|
+
function filterContext(entries, changes = {}) {
|
|
9
|
+
return JSON.stringify(entries.map(({ id, field, value }) => [
|
|
10
|
+
id,
|
|
11
|
+
contextValue(field, id in changes && !field.disabled ? field.normalize(changes[id]) : value),
|
|
12
|
+
]));
|
|
13
|
+
}
|
|
14
|
+
function filterAcknowledgement(entries, changes) {
|
|
15
|
+
const expected = entries.flatMap(({ id, field }) => id in changes && !field.disabled
|
|
16
|
+
? [[id, contextValue(field, field.normalize(changes[id]))]]
|
|
17
|
+
: []);
|
|
18
|
+
if (!expected.length)
|
|
19
|
+
return undefined;
|
|
20
|
+
return (context) => {
|
|
21
|
+
const applied = new Map(JSON.parse(context));
|
|
22
|
+
return (applied.size === entries.length &&
|
|
23
|
+
entries.every(({ id }) => applied.has(id)) &&
|
|
24
|
+
expected.every(([id, value]) => applied.get(id) === value));
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Root-local draft; the caller's controller always exposes the applied search. */
|
|
28
|
+
export function useBufferedFilters(filters, delay) {
|
|
29
|
+
const draft = useSearchDraft({
|
|
30
|
+
value: filters.search,
|
|
31
|
+
context: filterContext(filters.entries),
|
|
32
|
+
onApply: filters.setSearch,
|
|
33
|
+
normalize: trim,
|
|
34
|
+
delay,
|
|
35
|
+
});
|
|
36
|
+
const batch = (changes, search, source = "edit") => {
|
|
37
|
+
if (source === "search") {
|
|
38
|
+
draft.setValue(search ?? draft.value);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Validation belongs to the applied controller. Invalid editors keep their draft.
|
|
42
|
+
const result = filters.batch(changes, trim(search ?? draft.value), source);
|
|
43
|
+
if (result)
|
|
44
|
+
return result;
|
|
45
|
+
// Bindings can update related fields in the same patch. Match the requested
|
|
46
|
+
// fields while allowing those domain updates in the acknowledgement.
|
|
47
|
+
draft.apply(search ?? draft.value, () => { }, filterContext(filters.entries, changes), filterAcknowledgement(filters.entries, changes));
|
|
48
|
+
};
|
|
49
|
+
if (delay <= 0)
|
|
50
|
+
return { filters, flushSearch: () => { } };
|
|
51
|
+
return {
|
|
52
|
+
filters: {
|
|
53
|
+
...filters,
|
|
54
|
+
...filterControllerActions({ ...filters, search: draft.value, batch }),
|
|
55
|
+
search: draft.value,
|
|
56
|
+
setSearch: draft.setValue,
|
|
57
|
+
batch,
|
|
58
|
+
},
|
|
59
|
+
flushSearch: draft.flush,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export interface FilterSearchOptions {
|
|
2
|
+
/** Applied filters or another scope whose external changes discard unfinished typing. */
|
|
3
|
+
context?: string;
|
|
4
|
+
/** Normalize applied queries while preserving the exact text in the input. */
|
|
5
|
+
normalize?: (search: string) => string;
|
|
6
|
+
}
|
|
7
|
+
/** Buffer standalone search inputs; FilterRoot and FilterBar do this automatically. */
|
|
8
|
+
export declare function useFilterSearch(value: string | null | undefined, onChange: (value: string | null) => void, delay?: number, options?: FilterSearchOptions): {
|
|
3
9
|
value: string;
|
|
4
|
-
setValue: (
|
|
10
|
+
setValue: (text: string) => void;
|
|
11
|
+
flush: () => void;
|
|
5
12
|
};
|
|
@@ -1,44 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
4
|
-
export function useFilterSearch(value, onChange, delay = 300) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
useLayoutEffect(() => {
|
|
11
|
-
change.current = onChange;
|
|
2
|
+
import { useSearchDraft } from "./use-search-draft.js";
|
|
3
|
+
/** Buffer standalone search inputs; FilterRoot and FilterBar do this automatically. */
|
|
4
|
+
export function useFilterSearch(value, onChange, delay = 300, options = {}) {
|
|
5
|
+
const draft = useSearchDraft({
|
|
6
|
+
...options,
|
|
7
|
+
value: value ?? "",
|
|
8
|
+
onApply: (search) => onChange(search || null),
|
|
9
|
+
delay,
|
|
12
10
|
});
|
|
13
|
-
|
|
14
|
-
setState({
|
|
15
|
-
committed,
|
|
16
|
-
draft: pending.current.includes(committed) ? state.draft : committed,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
useLayoutEffect(() => {
|
|
20
|
-
const acknowledgement = pending.current.includes(committed);
|
|
21
|
-
pending.current = pending.current.filter((request) => request !== committed);
|
|
22
|
-
if (!acknowledgement && timer.current)
|
|
23
|
-
clearTimeout(timer.current);
|
|
24
|
-
}, [committed]);
|
|
25
|
-
useEffect(() => () => {
|
|
26
|
-
if (timer.current)
|
|
27
|
-
clearTimeout(timer.current);
|
|
28
|
-
}, []);
|
|
29
|
-
function setValue(next) {
|
|
30
|
-
if (timer.current)
|
|
31
|
-
clearTimeout(timer.current);
|
|
32
|
-
setState((current) => ({ ...current, draft: next }));
|
|
33
|
-
function commit() {
|
|
34
|
-
if (next !== committed)
|
|
35
|
-
pending.current.push(next);
|
|
36
|
-
change.current(next || null);
|
|
37
|
-
}
|
|
38
|
-
if (!next)
|
|
39
|
-
commit();
|
|
40
|
-
else
|
|
41
|
-
timer.current = setTimeout(commit, delay);
|
|
42
|
-
}
|
|
43
|
-
return { value: state.draft, setValue };
|
|
11
|
+
return { value: draft.value, setValue: draft.setValue, flush: draft.flush };
|
|
44
12
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useMendyLocale } from "../locale-context.js";
|
|
3
3
|
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { initialValues } from "./filter-state.js";
|
|
5
|
+
import { filterControllerActions } from "./filter-controller-actions.js";
|
|
5
6
|
export function useFilterController(options) {
|
|
6
7
|
const { t, messages } = useMendyLocale();
|
|
7
8
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -57,43 +58,14 @@ export function useFilterController(options) {
|
|
|
57
58
|
setError(null);
|
|
58
59
|
},
|
|
59
60
|
batch,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
clear() {
|
|
69
|
-
batch(Object.fromEntries(options.entries.flatMap((entry) => entry.field.removable !== false && !entry.field.hidden && !entry.field.disabled
|
|
70
|
-
? [[entry.id, entry.field.clearValue]]
|
|
71
|
-
: [])), "", "clear");
|
|
72
|
-
edit(null);
|
|
73
|
-
setMenuOpen(false);
|
|
74
|
-
setOpenField(null);
|
|
75
|
-
},
|
|
76
|
-
setSearch: (search) => {
|
|
77
|
-
batch({}, search, "search");
|
|
78
|
-
},
|
|
79
|
-
paste(text, remainder) {
|
|
80
|
-
const result = classifyPaste(text, options.entries);
|
|
81
|
-
const unmatched = [...result.unmatched, ...result.ambiguous.map((item) => item.token)].join(" ");
|
|
82
|
-
const search = remainder
|
|
83
|
-
? [remainder.before, unmatched, remainder.after].filter(Boolean).join(" ")
|
|
84
|
-
: [options.search, unmatched].filter(Boolean).join(" ");
|
|
85
|
-
let cursor = [remainder ? remainder.before : options.search, result.unmatched.join(" ")]
|
|
86
|
-
.filter(Boolean)
|
|
87
|
-
.join(" ").length;
|
|
88
|
-
for (const item of result.ambiguous) {
|
|
89
|
-
if (cursor)
|
|
90
|
-
cursor++;
|
|
91
|
-
item.searchRange = { start: cursor, end: cursor + item.token.length };
|
|
92
|
-
cursor += item.token.length;
|
|
93
|
-
}
|
|
94
|
-
batch(result.changes, search, "paste");
|
|
95
|
-
return result;
|
|
96
|
-
},
|
|
61
|
+
...filterControllerActions({
|
|
62
|
+
entries: options.entries,
|
|
63
|
+
search: options.search,
|
|
64
|
+
batch,
|
|
65
|
+
edit,
|
|
66
|
+
setMenuOpen,
|
|
67
|
+
setOpenField,
|
|
68
|
+
}),
|
|
97
69
|
error,
|
|
98
70
|
shareable: true,
|
|
99
71
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Keep keystrokes local while an applied query or URL update is in flight. */
|
|
2
|
+
export declare function useSearchDraft({ value, onApply, context, delay, normalize, }: {
|
|
3
|
+
value: string;
|
|
4
|
+
onApply: (search: string) => void;
|
|
5
|
+
context?: string;
|
|
6
|
+
delay?: number;
|
|
7
|
+
normalize?: (search: string) => string;
|
|
8
|
+
}): {
|
|
9
|
+
value: string;
|
|
10
|
+
setValue: (text: string) => void;
|
|
11
|
+
apply: (text: string, action?: (search: string) => void, nextContext?: string, acknowledgeContext?: (context: string) => boolean) => void;
|
|
12
|
+
flush: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { startTransition, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
const identity = (value) => value;
|
|
4
|
+
/** Keep keystrokes local while an applied query or URL update is in flight. */
|
|
5
|
+
export function useSearchDraft({ value, onApply, context = "", delay = 300, normalize = identity, }) {
|
|
6
|
+
const key = (search, scope) => JSON.stringify([normalize(search), scope]);
|
|
7
|
+
const appliedKey = key(value, context);
|
|
8
|
+
const [state, setState] = useState(() => ({
|
|
9
|
+
appliedKey,
|
|
10
|
+
draft: value,
|
|
11
|
+
pending: [],
|
|
12
|
+
reset: 0,
|
|
13
|
+
}));
|
|
14
|
+
const timer = useRef(null);
|
|
15
|
+
if (state.appliedKey !== appliedKey) {
|
|
16
|
+
const acknowledged = state.pending.findIndex((request) => request.key === appliedKey ||
|
|
17
|
+
(request.search === normalize(value) && request.acknowledgeContext?.(context)));
|
|
18
|
+
setState({
|
|
19
|
+
appliedKey,
|
|
20
|
+
draft: acknowledged < 0 ? value : state.draft,
|
|
21
|
+
pending: acknowledged < 0 ? [] : state.pending.slice(acknowledged + 1),
|
|
22
|
+
reset: state.reset + (acknowledged < 0 ? 1 : 0),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function cancel() {
|
|
26
|
+
if (timer.current !== null)
|
|
27
|
+
clearTimeout(timer.current);
|
|
28
|
+
timer.current = null;
|
|
29
|
+
}
|
|
30
|
+
function apply(text, action = onApply, nextContext = context, acknowledgeContext) {
|
|
31
|
+
const search = normalize(text);
|
|
32
|
+
action(search);
|
|
33
|
+
cancel();
|
|
34
|
+
const nextKey = key(search, nextContext);
|
|
35
|
+
setState((current) => ({
|
|
36
|
+
...current,
|
|
37
|
+
draft: text,
|
|
38
|
+
pending: nextKey === current.appliedKey && current.pending.length === 0
|
|
39
|
+
? current.pending
|
|
40
|
+
: [
|
|
41
|
+
...current.pending.filter((request) => request.key !== nextKey),
|
|
42
|
+
{ key: nextKey, search, acknowledgeContext },
|
|
43
|
+
],
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
const applyLatest = useRef(apply);
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
applyLatest.current = apply;
|
|
49
|
+
});
|
|
50
|
+
useLayoutEffect(() => cancel(), [state.reset]);
|
|
51
|
+
useEffect(() => cancel, []);
|
|
52
|
+
function setValue(text) {
|
|
53
|
+
cancel();
|
|
54
|
+
setState((current) => ({ ...current, draft: text }));
|
|
55
|
+
const search = normalize(text);
|
|
56
|
+
if ((search === normalize(value) && state.pending.length === 0) ||
|
|
57
|
+
state.pending.at(-1)?.search === search)
|
|
58
|
+
return;
|
|
59
|
+
if (!text || delay <= 0)
|
|
60
|
+
apply(text);
|
|
61
|
+
else {
|
|
62
|
+
timer.current = setTimeout(() => {
|
|
63
|
+
timer.current = null;
|
|
64
|
+
startTransition(() => {
|
|
65
|
+
applyLatest.current(text);
|
|
66
|
+
});
|
|
67
|
+
}, delay);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { value: state.draft, setValue, apply, flush: () => apply(state.draft) };
|
|
71
|
+
}
|
package/dist/table/table-view.js
CHANGED
|
@@ -54,7 +54,8 @@ export function TableView({ table, label, height = "auto", layout: layoutMode =
|
|
|
54
54
|
const { totalWidth, pinningActive } = layout;
|
|
55
55
|
const { inlineSelection, selectLoaded } = useInlineRowSelection(table, layout.columns, rowSelectionControls);
|
|
56
56
|
const focused = table.getFocusedCell();
|
|
57
|
-
const
|
|
57
|
+
const selectionFocusColumn = table.atoms.cellSelection.get().at(-1)?.focusColumnId;
|
|
58
|
+
const { columns, columnGaps, cellStyle } = useColumnWindow(layout, container, viewportWidth, selectionFocusColumn ?? focused?.column.id, !autoRowHeight);
|
|
58
59
|
const columnIndexes = useMemo(() => new Map(layout.columns.map((column, index) => [column.id, index])), [layout.columns]);
|
|
59
60
|
const { announcement, copied, onKeyDown } = useTableInteraction({
|
|
60
61
|
table,
|
|
@@ -139,11 +139,37 @@ export function useTableInteraction({ table, pinningActive, container, queryKey,
|
|
|
139
139
|
const direction = directions[event.key];
|
|
140
140
|
if (direction) {
|
|
141
141
|
event.preventDefault();
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
const hadFocusedCell = Boolean(table.getFocusedCell());
|
|
143
|
+
if (!hadFocusedCell && rows[0]) {
|
|
144
|
+
const firstSelectable = columns.find((column) => column.columnDef.enableCellSelection !== false);
|
|
145
|
+
if (firstSelectable)
|
|
146
|
+
table.setFocusedCell(rows[0].id, firstSelectable.id);
|
|
147
|
+
}
|
|
148
|
+
if (event.shiftKey && (event.metaKey || event.ctrlKey)) {
|
|
149
|
+
const selectableColumns = columns.filter((column) => column.columnDef.enableCellSelection !== false);
|
|
150
|
+
const targetRow = direction === "up" ? rows[0] : rows.at(-1);
|
|
151
|
+
const targetColumn = direction === "left" ? selectableColumns[0] : selectableColumns.at(-1);
|
|
152
|
+
if (rows.length && selectableColumns.length) {
|
|
153
|
+
table.setCellSelection((ranges) => {
|
|
154
|
+
const active = ranges.at(-1);
|
|
155
|
+
if (!active)
|
|
156
|
+
return ranges;
|
|
157
|
+
return [
|
|
158
|
+
...ranges.slice(0, -1),
|
|
159
|
+
{
|
|
160
|
+
...active,
|
|
161
|
+
focusRowId: direction === "up" || direction === "down" ? targetRow.id : active.focusRowId,
|
|
162
|
+
focusColumnId: direction === "left" || direction === "right"
|
|
163
|
+
? targetColumn.id
|
|
164
|
+
: active.focusColumnId,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else if (hadFocusedCell && event.shiftKey)
|
|
145
171
|
table.extendCellSelection(direction);
|
|
146
|
-
else
|
|
172
|
+
else if (hadFocusedCell)
|
|
147
173
|
table.moveCellSelection(direction);
|
|
148
174
|
const range = table.atoms.cellSelection.get().at(-1);
|
|
149
175
|
if (range)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mendylanda/ui",
|
|
3
|
-
"version": "0.4.3-dev.
|
|
3
|
+
"version": "0.4.3-dev.72.7ee3a6f295d2",
|
|
4
4
|
"description": "Mendy Landa's reusable React components. Tables, filters, and sheets with shared interactions and customizable UI.",
|
|
5
5
|
"homepage": "https://ui.mendylanda.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"build": "tsc -p tsconfig.build.json && node scripts/build-css.mjs",
|
|
127
127
|
"typecheck": "tsc -p tsconfig.build.json --noEmit"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "7ee3a6f295d2aa60fcdb858e39aca730b473a50e"
|
|
130
130
|
}
|