vastgrid 0.1.1 → 0.2.1
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/README.md +175 -13
- package/dist/canvas/geometry.d.ts +92 -0
- package/dist/canvas/geometry.d.ts.map +1 -0
- package/dist/canvas/geometry.js +197 -0
- package/dist/canvas/geometry.js.map +1 -0
- package/dist/canvas/grid-canvas.d.ts +161 -0
- package/dist/canvas/grid-canvas.d.ts.map +1 -0
- package/dist/canvas/grid-canvas.js +1146 -0
- package/dist/canvas/grid-canvas.js.map +1 -0
- package/dist/canvas/hooks.d.ts +61 -0
- package/dist/canvas/hooks.d.ts.map +1 -0
- package/dist/canvas/hooks.js +2 -0
- package/dist/canvas/hooks.js.map +1 -0
- package/dist/canvas/keys.d.ts +26 -0
- package/dist/canvas/keys.d.ts.map +1 -0
- package/dist/canvas/keys.js +133 -0
- package/dist/canvas/keys.js.map +1 -0
- package/dist/canvas/layout.d.ts +26 -0
- package/dist/canvas/layout.d.ts.map +1 -0
- package/dist/canvas/layout.js +73 -0
- package/dist/canvas/layout.js.map +1 -0
- package/dist/canvas/scroll.d.ts +31 -0
- package/dist/canvas/scroll.d.ts.map +1 -0
- package/dist/canvas/scroll.js +111 -0
- package/dist/canvas/scroll.js.map +1 -0
- package/dist/canvas/text.d.ts +23 -0
- package/dist/canvas/text.d.ts.map +1 -0
- package/dist/canvas/text.js +142 -0
- package/dist/canvas/text.js.map +1 -0
- package/dist/{theme.d.ts → canvas/theme.d.ts} +8 -0
- package/dist/canvas/theme.d.ts.map +1 -0
- package/dist/canvas/theme.js +62 -0
- package/dist/canvas/theme.js.map +1 -0
- package/dist/core/cache.d.ts +48 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +222 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checked.d.ts +20 -0
- package/dist/core/checked.d.ts.map +1 -0
- package/dist/core/checked.js +61 -0
- package/dist/core/checked.js.map +1 -0
- package/dist/core/client.d.ts +33 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +103 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/conditions.d.ts +48 -0
- package/dist/core/conditions.d.ts.map +1 -0
- package/dist/core/conditions.js +105 -0
- package/dist/core/conditions.js.map +1 -0
- package/dist/core/evaluate.d.ts +20 -0
- package/dist/core/evaluate.d.ts.map +1 -0
- package/dist/core/evaluate.js +210 -0
- package/dist/core/evaluate.js.map +1 -0
- package/dist/core/filters.d.ts +51 -0
- package/dist/core/filters.d.ts.map +1 -0
- package/dist/core/filters.js +303 -0
- package/dist/core/filters.js.map +1 -0
- package/dist/core/format.d.ts +14 -0
- package/dist/core/format.d.ts.map +1 -0
- package/dist/core/format.js +303 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/measures.d.ts +26 -0
- package/dist/core/measures.d.ts.map +1 -0
- package/dist/core/measures.js +105 -0
- package/dist/core/measures.js.map +1 -0
- package/dist/core/request-ids.d.ts +20 -0
- package/dist/core/request-ids.d.ts.map +1 -0
- package/dist/core/request-ids.js +55 -0
- package/dist/core/request-ids.js.map +1 -0
- package/dist/core/rows.d.ts +79 -0
- package/dist/core/rows.d.ts.map +1 -0
- package/dist/core/rows.js +173 -0
- package/dist/core/rows.js.map +1 -0
- package/dist/core/store.d.ts +233 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +1162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/totals.d.ts +24 -0
- package/dist/core/totals.d.ts.map +1 -0
- package/dist/core/totals.js +49 -0
- package/dist/core/totals.js.map +1 -0
- package/dist/core/trace.d.ts +7 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +26 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/transports.d.ts +50 -0
- package/dist/core/transports.d.ts.map +1 -0
- package/dist/core/transports.js +160 -0
- package/dist/core/transports.js.map +1 -0
- package/dist/core/types.d.ts +151 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +45 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core.d.ts +12 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +11 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -7
- package/dist/index.js.map +1 -1
- package/dist/react/accessible-rows.d.ts +13 -0
- package/dist/react/accessible-rows.d.ts.map +1 -0
- package/dist/react/accessible-rows.js +320 -0
- package/dist/react/accessible-rows.js.map +1 -0
- package/dist/react/cell-editor.d.ts +22 -0
- package/dist/react/cell-editor.d.ts.map +1 -0
- package/dist/react/cell-editor.js +431 -0
- package/dist/react/cell-editor.js.map +1 -0
- package/dist/react/cell-menu.d.ts +18 -0
- package/dist/react/cell-menu.d.ts.map +1 -0
- package/dist/react/cell-menu.js +299 -0
- package/dist/react/cell-menu.js.map +1 -0
- package/dist/react/column-menu.d.ts +16 -0
- package/dist/react/column-menu.d.ts.map +1 -0
- package/dist/react/column-menu.js +1311 -0
- package/dist/react/column-menu.js.map +1 -0
- package/dist/react/columns-menu.d.ts +7 -0
- package/dist/react/columns-menu.d.ts.map +1 -0
- package/dist/react/columns-menu.js +249 -0
- package/dist/react/columns-menu.js.map +1 -0
- package/dist/react/conditions-menu.d.ts +10 -0
- package/dist/react/conditions-menu.d.ts.map +1 -0
- package/dist/react/conditions-menu.js +795 -0
- package/dist/react/conditions-menu.js.map +1 -0
- package/dist/react/context.d.ts +29 -0
- package/dist/react/context.d.ts.map +1 -0
- package/dist/react/context.js +18 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/data-grid.d.ts +79 -0
- package/dist/react/data-grid.d.ts.map +1 -0
- package/dist/react/data-grid.js +653 -0
- package/dist/react/data-grid.js.map +1 -0
- package/dist/react/detail-panel.d.ts +22 -0
- package/dist/react/detail-panel.d.ts.map +1 -0
- package/dist/react/detail-panel.js +242 -0
- package/dist/react/detail-panel.js.map +1 -0
- package/dist/react/export-menu.d.ts +11 -0
- package/dist/react/export-menu.d.ts.map +1 -0
- package/dist/react/export-menu.js +304 -0
- package/dist/react/export-menu.js.map +1 -0
- package/dist/react/find-bar.d.ts +11 -0
- package/dist/react/find-bar.d.ts.map +1 -0
- package/dist/react/find-bar.js +303 -0
- package/dist/react/find-bar.js.map +1 -0
- package/dist/react/format-form.d.ts +12 -0
- package/dist/react/format-form.d.ts.map +1 -0
- package/dist/react/format-form.js +863 -0
- package/dist/react/format-form.js.map +1 -0
- package/dist/react/formula-menu.d.ts +6 -0
- package/dist/react/formula-menu.d.ts.map +1 -0
- package/dist/react/formula-menu.js +343 -0
- package/dist/react/formula-menu.js.map +1 -0
- package/dist/react/grid-body.d.ts +33 -0
- package/dist/react/grid-body.d.ts.map +1 -0
- package/dist/react/grid-body.js +1389 -0
- package/dist/react/grid-body.js.map +1 -0
- package/dist/react/group-menu.d.ts +7 -0
- package/dist/react/group-menu.d.ts.map +1 -0
- package/dist/react/group-menu.js +508 -0
- package/dist/react/group-menu.js.map +1 -0
- package/dist/react/hooks.d.ts +33 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +161 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/messages.d.ts +180 -0
- package/dist/react/messages.d.ts.map +1 -0
- package/dist/react/messages.js +414 -0
- package/dist/react/messages.js.map +1 -0
- package/dist/react/save.d.ts +9 -0
- package/dist/react/save.d.ts.map +1 -0
- package/dist/react/save.js +102 -0
- package/dist/react/save.js.map +1 -0
- package/dist/react/static-rows.d.ts +9 -0
- package/dist/react/static-rows.d.ts.map +1 -0
- package/dist/react/static-rows.js +176 -0
- package/dist/react/static-rows.js.map +1 -0
- package/dist/react/status-bar.d.ts +12 -0
- package/dist/react/status-bar.d.ts.map +1 -0
- package/dist/react/status-bar.js +413 -0
- package/dist/react/status-bar.js.map +1 -0
- package/dist/react/toolbar-popover.d.ts +14 -0
- package/dist/react/toolbar-popover.d.ts.map +1 -0
- package/dist/react/toolbar-popover.js +143 -0
- package/dist/react/toolbar-popover.js.map +1 -0
- package/dist/react/toolbar.d.ts +13 -0
- package/dist/react/toolbar.d.ts.map +1 -0
- package/dist/react/toolbar.js +344 -0
- package/dist/react/toolbar.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +496 -316
- package/package.json +42 -17
- package/src/canvas/geometry.ts +197 -0
- package/src/canvas/grid-canvas.ts +1222 -0
- package/src/canvas/hooks.ts +61 -0
- package/src/canvas/keys.ts +96 -0
- package/src/canvas/layout.ts +77 -0
- package/src/canvas/scroll.ts +120 -0
- package/src/canvas/text.ts +155 -0
- package/src/{theme.ts → canvas/theme.ts} +14 -1
- package/src/core/cache.ts +247 -0
- package/src/core/checked.ts +64 -0
- package/src/core/client.ts +103 -0
- package/src/core/conditions.ts +89 -0
- package/src/core/evaluate.ts +221 -0
- package/src/core/filters.ts +268 -0
- package/src/core/format.ts +309 -0
- package/src/core/measures.ts +99 -0
- package/src/core/request-ids.ts +54 -0
- package/src/core/rows.ts +213 -0
- package/src/core/store.ts +1122 -0
- package/src/core/totals.ts +59 -0
- package/src/core/trace.ts +44 -0
- package/src/core/transports.ts +177 -0
- package/src/core/types.ts +169 -0
- package/src/core.ts +39 -0
- package/src/index.ts +24 -12
- package/src/react/accessible-rows.tsx +242 -0
- package/src/react/cell-editor.tsx +216 -0
- package/src/react/cell-menu.tsx +166 -0
- package/src/react/column-menu.tsx +619 -0
- package/src/react/columns-menu.tsx +116 -0
- package/src/react/conditions-menu.tsx +289 -0
- package/src/react/context.ts +33 -0
- package/src/react/data-grid.tsx +286 -0
- package/src/react/detail-panel.tsx +92 -0
- package/src/react/export-menu.tsx +112 -0
- package/src/react/find-bar.tsx +112 -0
- package/src/react/format-form.tsx +421 -0
- package/src/react/formula-menu.tsx +163 -0
- package/src/react/grid-body.tsx +744 -0
- package/src/react/group-menu.tsx +214 -0
- package/src/react/hooks.ts +104 -0
- package/src/react/messages.ts +556 -0
- package/src/react/save.ts +42 -0
- package/src/react/static-rows.tsx +59 -0
- package/src/react/status-bar.tsx +175 -0
- package/src/react/toolbar-popover.tsx +69 -0
- package/src/react/toolbar.tsx +152 -0
- package/src/server.ts +58 -0
- package/src/styles.css +496 -316
- package/dist/components/bars.d.ts +0 -14
- package/dist/components/bars.d.ts.map +0 -1
- package/dist/components/bars.js +0 -68
- package/dist/components/bars.js.map +0 -1
- package/dist/components/cell-inspector.d.ts +0 -11
- package/dist/components/cell-inspector.d.ts.map +0 -1
- package/dist/components/cell-inspector.js +0 -37
- package/dist/components/cell-inspector.js.map +0 -1
- package/dist/components/columns-panel.d.ts +0 -12
- package/dist/components/columns-panel.d.ts.map +0 -1
- package/dist/components/columns-panel.js +0 -11
- package/dist/components/columns-panel.js.map +0 -1
- package/dist/components/filter-panel.d.ts +0 -22
- package/dist/components/filter-panel.d.ts.map +0 -1
- package/dist/components/filter-panel.js +0 -169
- package/dist/components/filter-panel.js.map +0 -1
- package/dist/components/find-bar.d.ts +0 -9
- package/dist/components/find-bar.d.ts.map +0 -1
- package/dist/components/find-bar.js +0 -46
- package/dist/components/find-bar.js.map +0 -1
- package/dist/components/popover.d.ts +0 -38
- package/dist/components/popover.d.ts.map +0 -1
- package/dist/components/popover.js +0 -104
- package/dist/components/popover.js.map +0 -1
- package/dist/components/stats-panel.d.ts +0 -12
- package/dist/components/stats-panel.d.ts.map +0 -1
- package/dist/components/stats-panel.js +0 -40
- package/dist/components/stats-panel.js.map +0 -1
- package/dist/controller.d.ts +0 -227
- package/dist/controller.d.ts.map +0 -1
- package/dist/controller.js +0 -845
- package/dist/controller.js.map +0 -1
- package/dist/data-grid.d.ts +0 -51
- package/dist/data-grid.d.ts.map +0 -1
- package/dist/data-grid.js +0 -704
- package/dist/data-grid.js.map +0 -1
- package/dist/labels.d.ts +0 -9
- package/dist/labels.d.ts.map +0 -1
- package/dist/labels.js +0 -38
- package/dist/labels.js.map +0 -1
- package/dist/messages.d.ts +0 -99
- package/dist/messages.d.ts.map +0 -1
- package/dist/messages.js +0 -276
- package/dist/messages.js.map +0 -1
- package/dist/renderer.d.ts +0 -52
- package/dist/renderer.d.ts.map +0 -1
- package/dist/renderer.js +0 -522
- package/dist/renderer.js.map +0 -1
- package/dist/text.d.ts +0 -14
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js +0 -63
- package/dist/text.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -54
- package/dist/theme.js.map +0 -1
- package/src/components/bars.tsx +0 -180
- package/src/components/cell-inspector.tsx +0 -70
- package/src/components/columns-panel.tsx +0 -59
- package/src/components/filter-panel.tsx +0 -293
- package/src/components/find-bar.tsx +0 -104
- package/src/components/popover.tsx +0 -194
- package/src/components/stats-panel.tsx +0 -153
- package/src/controller.ts +0 -945
- package/src/data-grid.tsx +0 -1035
- package/src/labels.ts +0 -46
- package/src/messages.ts +0 -390
- package/src/renderer.ts +0 -535
- package/src/text.ts +0 -55
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FormEvent,
|
|
3
|
+
type KeyboardEvent,
|
|
4
|
+
type ToggleEvent,
|
|
5
|
+
use,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
useTransition,
|
|
12
|
+
} from "react";
|
|
13
|
+
import {
|
|
14
|
+
type Column,
|
|
15
|
+
type Expr,
|
|
16
|
+
type GridCommand,
|
|
17
|
+
isBlank,
|
|
18
|
+
type JsonValue,
|
|
19
|
+
joinParts,
|
|
20
|
+
measureTakes,
|
|
21
|
+
type SortDirection,
|
|
22
|
+
textOf,
|
|
23
|
+
} from "vastgrid-spec";
|
|
24
|
+
import {
|
|
25
|
+
type ColumnFilter,
|
|
26
|
+
conditionsOf,
|
|
27
|
+
type FilterOperator,
|
|
28
|
+
FilterTextError,
|
|
29
|
+
filterParts,
|
|
30
|
+
operatorsFor,
|
|
31
|
+
readFilter,
|
|
32
|
+
takesNoValue,
|
|
33
|
+
} from "../core/filters.js";
|
|
34
|
+
import { measureName, offers, statisticsOf, TOTALS } from "../core/measures.js";
|
|
35
|
+
import { LocaleContext } from "./context.js";
|
|
36
|
+
import { FormatForm } from "./format-form.js";
|
|
37
|
+
import { useGridSelector, useGridStore } from "./hooks.js";
|
|
38
|
+
|
|
39
|
+
/** Where the column menu opens: a column of the view, at a point of the page. */
|
|
40
|
+
export interface MenuTarget {
|
|
41
|
+
column: string;
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The key of a value in the list; blank values (§3.2) share one. */
|
|
47
|
+
const BLANK = "\u0000blank";
|
|
48
|
+
|
|
49
|
+
function keyOf(value: JsonValue, column: Column) {
|
|
50
|
+
return isBlank(value, column) ? BLANK : JSON.stringify(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `filter.column` with the parts of a filter, or null when its text is not a value of the column. */
|
|
54
|
+
function filterCommand(filter: ColumnFilter | null, column: Column): GridCommand | null {
|
|
55
|
+
let parts: Expr[] = [];
|
|
56
|
+
try {
|
|
57
|
+
if (filter) parts = filterParts(filter, column);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
if (cause instanceof FilterTextError) return null;
|
|
60
|
+
throw cause;
|
|
61
|
+
}
|
|
62
|
+
return { command: "filter.column", column: column.name, where: joinParts(parts) ?? null };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How long typing in the values' search box must pause before the list is read again. */
|
|
66
|
+
const SEARCH_DELAY = 250;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The filter of one column (§6.2.1): a list of its values with how many rows have each, read under the other
|
|
70
|
+
* filters (`GridStore.values`) when the engine groups rows, and a condition on typed text. Applying dispatches
|
|
71
|
+
* `filter.column` with the parts it makes, replacing the column's conditions.
|
|
72
|
+
*/
|
|
73
|
+
function FilterForm({ column, onDone }: { column: Column; onDone: () => void }) {
|
|
74
|
+
const store = useGridStore();
|
|
75
|
+
const { messages, number } = use(LocaleContext);
|
|
76
|
+
const id = useId();
|
|
77
|
+
const parts = useGridSelector((snapshot) => snapshot.document.plan?.where);
|
|
78
|
+
const lists = useGridSelector((snapshot) => {
|
|
79
|
+
const capabilities = snapshot.table?.capabilities;
|
|
80
|
+
return capabilities?.plan?.aggregate !== false && (capabilities?.measures?.includes("count") ?? true);
|
|
81
|
+
});
|
|
82
|
+
const [initial] = useState(() => readFilter(conditionsOf(parts, column.name), column.name));
|
|
83
|
+
const read = initial === "other" ? null : initial;
|
|
84
|
+
const operators = operatorsFor(column.type);
|
|
85
|
+
const [search, setSearch] = useState("");
|
|
86
|
+
const [list, setList] = useState<{ values: { value: JsonValue; count: number }[]; truncated: boolean } | null>(null);
|
|
87
|
+
const [failure, setFailure] = useState<string | null>(null);
|
|
88
|
+
// Values not toggled follow `base`; `toggled` holds the values changed from it.
|
|
89
|
+
const [base, setBase] = useState(() => !(read?.kind === "values" && !read.exclude));
|
|
90
|
+
const [toggled, setToggled] = useState(() => {
|
|
91
|
+
const map = new Map<string, JsonValue>();
|
|
92
|
+
if (read?.kind !== "values") return map;
|
|
93
|
+
for (const value of read.values) map.set(keyOf(value, column), value);
|
|
94
|
+
if (read.blank) map.set(BLANK, null);
|
|
95
|
+
return map;
|
|
96
|
+
});
|
|
97
|
+
const [operator, setOperator] = useState<FilterOperator | "none">(
|
|
98
|
+
read?.kind === "condition" && operators.includes(read.operator) ? read.operator : "none",
|
|
99
|
+
);
|
|
100
|
+
const [first, setFirst] = useState(read?.kind === "condition" ? read.first : "");
|
|
101
|
+
const [second, setSecond] = useState(read?.kind === "condition" ? read.second : "");
|
|
102
|
+
const [error, setError] = useState<string | null>(null);
|
|
103
|
+
const [pending, startApply] = useTransition();
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!lists) return;
|
|
107
|
+
const abort = new AbortController();
|
|
108
|
+
const timer = setTimeout(
|
|
109
|
+
() => {
|
|
110
|
+
store
|
|
111
|
+
.values(column.name, { search }, abort.signal)
|
|
112
|
+
.then((result) => {
|
|
113
|
+
setList(result);
|
|
114
|
+
setFailure(null);
|
|
115
|
+
})
|
|
116
|
+
.catch((cause: unknown) => {
|
|
117
|
+
if (!abort.signal.aborted) setFailure(cause instanceof Error ? cause.message : String(cause));
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
search ? SEARCH_DELAY : 0,
|
|
121
|
+
);
|
|
122
|
+
return () => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
abort.abort();
|
|
125
|
+
};
|
|
126
|
+
}, [store, lists, column.name, search]);
|
|
127
|
+
|
|
128
|
+
// Blank values (null and the empty string) are one item.
|
|
129
|
+
const items: { key: string; value: JsonValue; count: number }[] = [];
|
|
130
|
+
for (const item of list?.values ?? []) {
|
|
131
|
+
const key = keyOf(item.value, column);
|
|
132
|
+
const same = items.find((other) => other.key === key);
|
|
133
|
+
if (same) same.count += item.count;
|
|
134
|
+
else items.push({ key, value: item.value, count: item.count });
|
|
135
|
+
}
|
|
136
|
+
const checked = (key: string) => (toggled.has(key) ? !base : base);
|
|
137
|
+
const conditionOn = operator !== "none";
|
|
138
|
+
const allChecked = toggled.size === 0 ? base : base && items.every((item) => checked(item.key));
|
|
139
|
+
|
|
140
|
+
const toggle = (key: string, value: JsonValue) =>
|
|
141
|
+
setToggled((current) => {
|
|
142
|
+
const next = new Map(current);
|
|
143
|
+
if (next.has(key)) next.delete(key);
|
|
144
|
+
else next.set(key, value);
|
|
145
|
+
return next;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const filterOf = (): ColumnFilter | null => {
|
|
149
|
+
if (conditionOn) return { kind: "condition", operator, first, second };
|
|
150
|
+
if (base && toggled.size === 0) return null;
|
|
151
|
+
const values = [...toggled].flatMap(([key, value]) => (key === BLANK ? [] : [value]));
|
|
152
|
+
return { kind: "values", values, blank: toggled.has(BLANK), exclude: base };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const apply = (filter: ColumnFilter | null) => {
|
|
156
|
+
const command = filterCommand(filter, column);
|
|
157
|
+
if (!command) {
|
|
158
|
+
setError(messages.invalidValue(column.name));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
startApply(async () => {
|
|
162
|
+
const outcome = await store.dispatch(command);
|
|
163
|
+
if (!outcome.ok) {
|
|
164
|
+
setError(outcome.error.message);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
onDone();
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const submit = (event: FormEvent<HTMLFormElement>) => {
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
apply(filterOf());
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const label = (value: JsonValue) => (isBlank(value, column) ? messages.blank : (textOf(value, column) ?? ""));
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<form className="vg-filter" onSubmit={submit} aria-labelledby={`${id}-title`}>
|
|
180
|
+
<p className="vg-filter__title" id={`${id}-title`}>
|
|
181
|
+
{messages.filterBy(column.name)}
|
|
182
|
+
</p>
|
|
183
|
+
{initial === "other" && <p className="vg-filter__note">{messages.otherConditions}</p>}
|
|
184
|
+
{lists && (
|
|
185
|
+
<>
|
|
186
|
+
<input
|
|
187
|
+
type="search"
|
|
188
|
+
className="vg-input"
|
|
189
|
+
aria-label={messages.valuesSearch}
|
|
190
|
+
placeholder={messages.valuesSearch}
|
|
191
|
+
value={search}
|
|
192
|
+
disabled={conditionOn}
|
|
193
|
+
onChange={(event) => setSearch(event.target.value)}
|
|
194
|
+
/>
|
|
195
|
+
<fieldset
|
|
196
|
+
className="vg-filter__values"
|
|
197
|
+
aria-labelledby={`${id}-title`}
|
|
198
|
+
aria-busy={list === null}
|
|
199
|
+
disabled={conditionOn}
|
|
200
|
+
>
|
|
201
|
+
{!search && (
|
|
202
|
+
<label className="vg-filter__value">
|
|
203
|
+
<input
|
|
204
|
+
type="checkbox"
|
|
205
|
+
checked={allChecked}
|
|
206
|
+
onChange={(event) => {
|
|
207
|
+
setBase(event.target.checked);
|
|
208
|
+
setToggled(new Map());
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
<span>{messages.selectAll}</span>
|
|
212
|
+
</label>
|
|
213
|
+
)}
|
|
214
|
+
{items.map((item) => (
|
|
215
|
+
<label className="vg-filter__value" key={item.key}>
|
|
216
|
+
<input type="checkbox" checked={checked(item.key)} onChange={() => toggle(item.key, item.value)} />
|
|
217
|
+
<span className="vg-filter__label">{label(item.value)}</span>
|
|
218
|
+
<span className="vg-filter__count">{number.format(item.count)}</span>
|
|
219
|
+
</label>
|
|
220
|
+
))}
|
|
221
|
+
{list === null && failure === null && <p className="vg-filter__note">{messages.loading}</p>}
|
|
222
|
+
{failure !== null && <p className="vg-filter__error">{messages.failed(failure)}</p>}
|
|
223
|
+
</fieldset>
|
|
224
|
+
{list?.truncated && (
|
|
225
|
+
<p className="vg-filter__note">{messages.valuesTruncated(number.format(list.values.length))}</p>
|
|
226
|
+
)}
|
|
227
|
+
</>
|
|
228
|
+
)}
|
|
229
|
+
<fieldset className="vg-filter__condition">
|
|
230
|
+
<legend>{messages.condition}</legend>
|
|
231
|
+
<select
|
|
232
|
+
className="vg-input"
|
|
233
|
+
aria-label={messages.condition}
|
|
234
|
+
value={operator}
|
|
235
|
+
onChange={(event) => setOperator(event.target.value as FilterOperator | "none")}
|
|
236
|
+
>
|
|
237
|
+
<option value="none">{messages.conditionNone}</option>
|
|
238
|
+
{operators.map((item) => (
|
|
239
|
+
<option key={item} value={item}>
|
|
240
|
+
{messages.operators[item]}
|
|
241
|
+
</option>
|
|
242
|
+
))}
|
|
243
|
+
</select>
|
|
244
|
+
{conditionOn && !takesNoValue(operator) && (
|
|
245
|
+
<input
|
|
246
|
+
className="vg-input"
|
|
247
|
+
aria-label={operator === "between" ? messages.minimum : messages.value}
|
|
248
|
+
placeholder={operator === "between" ? messages.minimum : messages.value}
|
|
249
|
+
aria-invalid={error !== null}
|
|
250
|
+
value={first}
|
|
251
|
+
onChange={(event) => setFirst(event.target.value)}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
{operator === "between" && (
|
|
255
|
+
<input
|
|
256
|
+
className="vg-input"
|
|
257
|
+
aria-label={messages.maximum}
|
|
258
|
+
placeholder={messages.maximum}
|
|
259
|
+
aria-invalid={error !== null}
|
|
260
|
+
value={second}
|
|
261
|
+
onChange={(event) => setSecond(event.target.value)}
|
|
262
|
+
/>
|
|
263
|
+
)}
|
|
264
|
+
</fieldset>
|
|
265
|
+
{error !== null && (
|
|
266
|
+
<p role="alert" className="vg-filter__error">
|
|
267
|
+
{error}
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
<div className="vg-filter__actions">
|
|
271
|
+
<button type="button" className="vg-button" disabled={pending} onClick={() => apply(null)}>
|
|
272
|
+
{messages.clearFilter}
|
|
273
|
+
</button>
|
|
274
|
+
<button type="submit" className="vg-button vg-button--primary" disabled={pending}>
|
|
275
|
+
{messages.apply}
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
</form>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The statistics of a column over the rows of the view (`GridStore.aggregate`): the rows, the values and the empty
|
|
284
|
+
* ones, the distinct values, and the measures that take its type, read by the engine when the panel opens.
|
|
285
|
+
*/
|
|
286
|
+
function Statistics({ column }: { column: Column }) {
|
|
287
|
+
const store = useGridStore();
|
|
288
|
+
const { messages, number } = use(LocaleContext);
|
|
289
|
+
const capabilities = useGridSelector((snapshot) => snapshot.table?.capabilities);
|
|
290
|
+
const [result, setResult] = useState<{
|
|
291
|
+
values: Record<string, JsonValue>;
|
|
292
|
+
columns: Record<string, Column>;
|
|
293
|
+
} | null>(null);
|
|
294
|
+
const [failure, setFailure] = useState<string | null>(null);
|
|
295
|
+
const statistics = statisticsOf(column, capabilities);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const abort = new AbortController();
|
|
299
|
+
store
|
|
300
|
+
.aggregate(
|
|
301
|
+
statisticsOf(column, capabilities).map((statistic) => statistic.measure),
|
|
302
|
+
abort.signal,
|
|
303
|
+
)
|
|
304
|
+
.then((read) => {
|
|
305
|
+
setResult(read);
|
|
306
|
+
setFailure(null);
|
|
307
|
+
})
|
|
308
|
+
.catch((cause: unknown) => {
|
|
309
|
+
if (!abort.signal.aborted) setFailure(cause instanceof Error ? cause.message : String(cause));
|
|
310
|
+
});
|
|
311
|
+
return () => abort.abort();
|
|
312
|
+
}, [store, column, capabilities]);
|
|
313
|
+
|
|
314
|
+
const text = (name: string) => {
|
|
315
|
+
const value = result?.values[name];
|
|
316
|
+
if (value === undefined || value === null) return "";
|
|
317
|
+
if (typeof value === "number") return number.format(value);
|
|
318
|
+
return textOf(value, result?.columns[name] ?? column) ?? "";
|
|
319
|
+
};
|
|
320
|
+
const rows = Number(result?.values.rows ?? 0);
|
|
321
|
+
const values = Number(result?.values.values ?? 0);
|
|
322
|
+
return (
|
|
323
|
+
<section className="vg-statistics" aria-label={messages.statisticsOf(column.name)} aria-busy={result === null}>
|
|
324
|
+
<p className="vg-filter__title">{messages.statisticsOf(column.name)}</p>
|
|
325
|
+
{failure !== null && <p className="vg-filter__error">{messages.failed(failure)}</p>}
|
|
326
|
+
{failure === null && result === null && <p className="vg-filter__note">{messages.loading}</p>}
|
|
327
|
+
{result && (
|
|
328
|
+
<dl className="vg-statistics__list">
|
|
329
|
+
{statistics.flatMap((statistic) => [
|
|
330
|
+
<div key={statistic.kind}>
|
|
331
|
+
<dt>{messages.statisticNames[statistic.kind]}</dt>
|
|
332
|
+
<dd>{text(statistic.kind)}</dd>
|
|
333
|
+
</div>,
|
|
334
|
+
// The empty values (null) follow the values.
|
|
335
|
+
...(statistic.kind === "values"
|
|
336
|
+
? [
|
|
337
|
+
<div key="nulls">
|
|
338
|
+
<dt>{messages.statisticNames.nulls}</dt>
|
|
339
|
+
<dd>{number.format(rows - values)}</dd>
|
|
340
|
+
</div>,
|
|
341
|
+
]
|
|
342
|
+
: []),
|
|
343
|
+
])}
|
|
344
|
+
</dl>
|
|
345
|
+
)}
|
|
346
|
+
</section>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The menu of a column of the view, opened from its header (the arrow at its right, a right click, or Alt+↓ on the
|
|
352
|
+
* active cell): sorting by it (§6.2), its filter, and its pin, width and visibility (§6.3), all as commands. It is
|
|
353
|
+
* a popover: Esc and a click outside close it, and the focus goes back to the grid.
|
|
354
|
+
*/
|
|
355
|
+
export function ColumnMenu({ target, onClose }: { target: MenuTarget | null; onClose: () => void }) {
|
|
356
|
+
const store = useGridStore();
|
|
357
|
+
const { messages } = use(LocaleContext);
|
|
358
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
359
|
+
const [panel, setPanel] = useState<"filter" | "statistics" | "total" | "format" | null>(null);
|
|
360
|
+
const [shown, setShown] = useState(target);
|
|
361
|
+
if (shown !== target) {
|
|
362
|
+
setShown(target);
|
|
363
|
+
setPanel(null);
|
|
364
|
+
}
|
|
365
|
+
const name = target?.column;
|
|
366
|
+
const item = useGridSelector((snapshot) => snapshot.displayed.find((entry) => entry.column.name === name));
|
|
367
|
+
const displayed = useGridSelector((snapshot) => snapshot.displayed.length);
|
|
368
|
+
const hidden = useGridSelector((snapshot) => (snapshot.view?.columns.length ?? 0) > snapshot.displayed.length);
|
|
369
|
+
const sort = useGridSelector((snapshot) => snapshot.document.plan?.sort);
|
|
370
|
+
const aggregate = useGridSelector((snapshot) => snapshot.document.plan?.aggregate);
|
|
371
|
+
const grouped = aggregate !== undefined;
|
|
372
|
+
const capabilities = useGridSelector((snapshot) => snapshot.table?.capabilities);
|
|
373
|
+
const ready = useGridSelector((snapshot) => snapshot.table?.status === "ready");
|
|
374
|
+
|
|
375
|
+
useLayoutEffect(() => {
|
|
376
|
+
const menu = ref.current;
|
|
377
|
+
if (!menu || !target) return;
|
|
378
|
+
if (!menu.matches(":popover-open")) menu.showPopover();
|
|
379
|
+
menu.querySelector<HTMLElement>("button:not(:disabled), input, select")?.focus();
|
|
380
|
+
}, [target]);
|
|
381
|
+
|
|
382
|
+
// The menu opens at its point, kept inside the window.
|
|
383
|
+
useLayoutEffect(() => {
|
|
384
|
+
const menu = ref.current;
|
|
385
|
+
if (!menu || !target) return;
|
|
386
|
+
const rect = menu.getBoundingClientRect();
|
|
387
|
+
menu.style.left = `${Math.max(8, Math.min(target.x, window.innerWidth - rect.width - 8))}px`;
|
|
388
|
+
menu.style.top = `${Math.max(8, Math.min(target.y, window.innerHeight - rect.height - 8))}px`;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const onToggle = (event: ToggleEvent<HTMLDivElement>) => {
|
|
392
|
+
if (event.newState === "closed") onClose();
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (!target || !item) {
|
|
396
|
+
return <div ref={ref} popover="auto" className="vg-menu vg-column-menu" onToggle={onToggle} />;
|
|
397
|
+
}
|
|
398
|
+
const column = item.column;
|
|
399
|
+
const close = () => ref.current?.hidePopover();
|
|
400
|
+
const run = (command: GridCommand) => {
|
|
401
|
+
close();
|
|
402
|
+
void store.dispatch(command);
|
|
403
|
+
};
|
|
404
|
+
const sorts = capabilities?.plan?.sort !== false;
|
|
405
|
+
const own = sort?.find((key) => key.column === column.name);
|
|
406
|
+
const others = (sort ?? []).filter((key) => key.column !== column.name);
|
|
407
|
+
const sortBy = (direction: SortDirection, add: boolean) =>
|
|
408
|
+
run({ command: "sort.set", keys: [...(add ? others : []), { column: column.name, direction }] });
|
|
409
|
+
const filters = ready && !grouped && capabilities?.plan?.where !== false;
|
|
410
|
+
const pin = item.setting.pin;
|
|
411
|
+
const measures = ready && !grouped && offers(capabilities, "count");
|
|
412
|
+
const totals = TOTALS.filter((fn) => offers(capabilities, fn) && measureTakes(fn, column));
|
|
413
|
+
const keys = aggregate?.keys ?? [];
|
|
414
|
+
const groupBy = () =>
|
|
415
|
+
run({
|
|
416
|
+
command: "group.set",
|
|
417
|
+
aggregate: {
|
|
418
|
+
keys: [column.name],
|
|
419
|
+
measures: [{ name: measureName("count", undefined, new Set([column.name])), fn: "count" }],
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
const ungroupBy = () => {
|
|
423
|
+
const rest = keys.filter((key) => key !== column.name);
|
|
424
|
+
run(
|
|
425
|
+
rest.length === 0 || !aggregate
|
|
426
|
+
? { command: "group.clear" }
|
|
427
|
+
: { command: "group.set", aggregate: { ...aggregate, keys: rest } },
|
|
428
|
+
);
|
|
429
|
+
};
|
|
430
|
+
const back = (
|
|
431
|
+
<button type="button" className="vg-menu__item" onClick={() => setPanel(null)}>
|
|
432
|
+
← {messages.back}
|
|
433
|
+
</button>
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<div
|
|
438
|
+
ref={ref}
|
|
439
|
+
popover="auto"
|
|
440
|
+
role="dialog"
|
|
441
|
+
className="vg-menu vg-column-menu"
|
|
442
|
+
aria-label={messages.columnMenu(column.name)}
|
|
443
|
+
onToggle={onToggle}
|
|
444
|
+
>
|
|
445
|
+
{panel === "filter" ? (
|
|
446
|
+
<>
|
|
447
|
+
{back}
|
|
448
|
+
<FilterForm key={column.name} column={column} onDone={close} />
|
|
449
|
+
</>
|
|
450
|
+
) : panel === "format" ? (
|
|
451
|
+
<>
|
|
452
|
+
{back}
|
|
453
|
+
<FormatForm key={column.name} column={column} setting={item.setting} onDone={close} />
|
|
454
|
+
</>
|
|
455
|
+
) : panel === "statistics" ? (
|
|
456
|
+
<>
|
|
457
|
+
{back}
|
|
458
|
+
<Statistics key={column.name} column={column} />
|
|
459
|
+
</>
|
|
460
|
+
) : panel === "total" ? (
|
|
461
|
+
<>
|
|
462
|
+
{back}
|
|
463
|
+
<div role="menu" aria-label={messages.total} onKeyDown={moveFocus}>
|
|
464
|
+
<MenuItem
|
|
465
|
+
radio
|
|
466
|
+
checked={item.setting.total === undefined}
|
|
467
|
+
onClick={() => run({ command: "column.total", name: column.name, total: null })}
|
|
468
|
+
>
|
|
469
|
+
{messages.noTotal}
|
|
470
|
+
</MenuItem>
|
|
471
|
+
{totals.map((fn) => (
|
|
472
|
+
<MenuItem
|
|
473
|
+
key={fn}
|
|
474
|
+
radio
|
|
475
|
+
checked={item.setting.total?.fn === fn}
|
|
476
|
+
onClick={() => run({ command: "column.total", name: column.name, total: { fn } })}
|
|
477
|
+
>
|
|
478
|
+
{messages.measureNames[fn]}
|
|
479
|
+
</MenuItem>
|
|
480
|
+
))}
|
|
481
|
+
</div>
|
|
482
|
+
</>
|
|
483
|
+
) : (
|
|
484
|
+
<div role="menu" aria-label={messages.columnMenu(column.name)} onKeyDown={moveFocus}>
|
|
485
|
+
{sorts && (
|
|
486
|
+
<>
|
|
487
|
+
<MenuItem checked={own?.direction === "asc" && others.length === 0} onClick={() => sortBy("asc", false)}>
|
|
488
|
+
{messages.sortAscending}
|
|
489
|
+
</MenuItem>
|
|
490
|
+
<MenuItem
|
|
491
|
+
checked={own?.direction === "desc" && others.length === 0}
|
|
492
|
+
onClick={() => sortBy("desc", false)}
|
|
493
|
+
>
|
|
494
|
+
{messages.sortDescending}
|
|
495
|
+
</MenuItem>
|
|
496
|
+
{others.length > 0 && (
|
|
497
|
+
<>
|
|
498
|
+
<MenuItem onClick={() => sortBy("asc", true)}>{messages.thenSortAscending}</MenuItem>
|
|
499
|
+
<MenuItem onClick={() => sortBy("desc", true)}>{messages.thenSortDescending}</MenuItem>
|
|
500
|
+
</>
|
|
501
|
+
)}
|
|
502
|
+
{sort && sort.length > 0 && (
|
|
503
|
+
<MenuItem onClick={() => run({ command: "sort.clear" })}>{messages.clearSort}</MenuItem>
|
|
504
|
+
)}
|
|
505
|
+
<hr className="vg-menu__line" />
|
|
506
|
+
</>
|
|
507
|
+
)}
|
|
508
|
+
{filters && <MenuItem onClick={() => setPanel("filter")}>{messages.filter}</MenuItem>}
|
|
509
|
+
{measures && <MenuItem onClick={() => setPanel("statistics")}>{messages.statistics}</MenuItem>}
|
|
510
|
+
{!grouped && totals.length > 0 && (
|
|
511
|
+
<MenuItem onClick={() => setPanel("total")}>
|
|
512
|
+
{item.setting.total
|
|
513
|
+
? `${messages.total}: ${messages.measureNames[item.setting.total.fn]}`
|
|
514
|
+
: `${messages.total}…`}
|
|
515
|
+
</MenuItem>
|
|
516
|
+
)}
|
|
517
|
+
{measures && capabilities?.plan?.aggregate !== false && (
|
|
518
|
+
<MenuItem onClick={groupBy}>{messages.groupByColumn}</MenuItem>
|
|
519
|
+
)}
|
|
520
|
+
{keys.includes(column.name) && <MenuItem onClick={ungroupBy}>{messages.removeFromGroups}</MenuItem>}
|
|
521
|
+
{grouped && <MenuItem onClick={() => run({ command: "group.clear" })}>{messages.ungroup}</MenuItem>}
|
|
522
|
+
{(filters || measures || grouped || totals.length > 0) && <hr className="vg-menu__line" />}
|
|
523
|
+
<MenuItem onClick={() => setPanel("format")}>{messages.format}</MenuItem>
|
|
524
|
+
<MenuItem onClick={() => run({ command: "column.resize", name: column.name, width: null })}>
|
|
525
|
+
{messages.autoFit}
|
|
526
|
+
</MenuItem>
|
|
527
|
+
{pin !== "left" && (
|
|
528
|
+
<MenuItem onClick={() => run({ command: "column.pin", name: column.name, side: "left" })}>
|
|
529
|
+
{messages.pinLeft}
|
|
530
|
+
</MenuItem>
|
|
531
|
+
)}
|
|
532
|
+
{pin !== "right" && (
|
|
533
|
+
<MenuItem onClick={() => run({ command: "column.pin", name: column.name, side: "right" })}>
|
|
534
|
+
{messages.pinRight}
|
|
535
|
+
</MenuItem>
|
|
536
|
+
)}
|
|
537
|
+
{pin !== undefined && (
|
|
538
|
+
<MenuItem onClick={() => run({ command: "column.pin", name: column.name, side: null })}>
|
|
539
|
+
{messages.unpin}
|
|
540
|
+
</MenuItem>
|
|
541
|
+
)}
|
|
542
|
+
<MenuItem disabled={displayed <= 1} onClick={() => run({ command: "column.hide", names: [column.name] })}>
|
|
543
|
+
{messages.hideColumn}
|
|
544
|
+
</MenuItem>
|
|
545
|
+
{hidden && (
|
|
546
|
+
<MenuItem onClick={() => run({ command: "column.show", all: true })}>{messages.showAllColumns}</MenuItem>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Arrows, Home and End move the focus among the items of a menu (the menu pattern of the ARIA Authoring Practices). */
|
|
555
|
+
function moveFocus(event: KeyboardEvent<HTMLDivElement>) {
|
|
556
|
+
const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>("button:not(:disabled)")];
|
|
557
|
+
const at = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
558
|
+
const next =
|
|
559
|
+
event.key === "ArrowDown"
|
|
560
|
+
? (at + 1) % items.length
|
|
561
|
+
: event.key === "ArrowUp"
|
|
562
|
+
? (at - 1 + items.length) % items.length
|
|
563
|
+
: event.key === "Home"
|
|
564
|
+
? 0
|
|
565
|
+
: event.key === "End"
|
|
566
|
+
? items.length - 1
|
|
567
|
+
: -1;
|
|
568
|
+
if (next < 0 || items.length === 0) return;
|
|
569
|
+
event.preventDefault();
|
|
570
|
+
items[next].focus();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function MenuItem({
|
|
574
|
+
checked,
|
|
575
|
+
radio,
|
|
576
|
+
disabled,
|
|
577
|
+
onClick,
|
|
578
|
+
children,
|
|
579
|
+
}: {
|
|
580
|
+
checked?: boolean;
|
|
581
|
+
radio?: boolean;
|
|
582
|
+
disabled?: boolean;
|
|
583
|
+
onClick: () => void;
|
|
584
|
+
children: string;
|
|
585
|
+
}) {
|
|
586
|
+
if (checked === undefined) {
|
|
587
|
+
return (
|
|
588
|
+
<button type="button" role="menuitem" className="vg-menu__item" disabled={disabled} onClick={onClick}>
|
|
589
|
+
{children}
|
|
590
|
+
</button>
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (radio) {
|
|
594
|
+
return (
|
|
595
|
+
<button
|
|
596
|
+
type="button"
|
|
597
|
+
role="menuitemradio"
|
|
598
|
+
aria-checked={checked}
|
|
599
|
+
className="vg-menu__item"
|
|
600
|
+
disabled={disabled}
|
|
601
|
+
onClick={onClick}
|
|
602
|
+
>
|
|
603
|
+
{children}
|
|
604
|
+
</button>
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
return (
|
|
608
|
+
<button
|
|
609
|
+
type="button"
|
|
610
|
+
role="menuitemcheckbox"
|
|
611
|
+
aria-checked={checked}
|
|
612
|
+
className="vg-menu__item"
|
|
613
|
+
disabled={disabled}
|
|
614
|
+
onClick={onClick}
|
|
615
|
+
>
|
|
616
|
+
{children}
|
|
617
|
+
</button>
|
|
618
|
+
);
|
|
619
|
+
}
|