@terpjs/react-core 0.6.1 → 0.8.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/README.md +12 -2
- package/package.json +2 -2
- package/src/AppShell.test.tsx +33 -12
- package/src/AppShell.tsx +69 -249
- package/src/Breadcrumbs.test.tsx +24 -0
- package/src/Breadcrumbs.tsx +9 -32
- package/src/ConfirmDialog.tsx +13 -44
- package/src/EmptyState.tsx +8 -36
- package/src/ErrorState.tsx +8 -36
- package/src/Field.test.tsx +57 -0
- package/src/Field.tsx +46 -22
- package/src/HubPage.test.tsx +22 -13
- package/src/HubPage.tsx +25 -97
- package/src/LoadingState.tsx +3 -24
- package/src/ModuleNav.tsx +1 -1
- package/src/PageActions.tsx +5 -10
- package/src/UserMenu.test.tsx +12 -5
- package/src/UserMenu.tsx +33 -62
- package/src/dataview/DataView.test.tsx +109 -5
- package/src/dataview/DataView.tsx +41 -23
- package/src/dataview/DataViewCardList.tsx +14 -60
- package/src/dataview/DataViewColumnSettings.tsx +46 -51
- package/src/dataview/DataViewExpandableRow.tsx +2 -17
- package/src/dataview/DataViewPagination.tsx +2 -32
- package/src/dataview/DataViewRowActions.tsx +13 -33
- package/src/dataview/DataViewTable.tsx +16 -103
- package/src/dataview/DataViewToolbar.tsx +53 -76
- package/src/dataview/README.md +6 -0
- package/src/dataview/index.ts +1 -0
- package/src/dataview/internal.tsx +4 -1
- package/src/dataview/types.ts +13 -0
- package/src/feedback.test.tsx +26 -0
- package/src/files.test.tsx +18 -0
- package/src/files.tsx +13 -4
- package/src/icons.test.tsx +10 -6
- package/src/icons.tsx +33 -37
- package/src/index.ts +0 -3
- package/src/layout.test.tsx +24 -9
- package/src/layout.tsx +24 -21
- package/src/layoutContract.test.tsx +95 -0
- package/src/locale.tsx +27 -4
- package/src/markers.test.ts +468 -0
- package/src/raw.d.ts +15 -1
- package/src/router.tsx +6 -9
- package/src/ssr.test.tsx +1 -3
- package/src/styles.test.ts +823 -6
- package/src/styles.ts +2699 -153
- package/src/theme.test.tsx +39 -0
- package/src/theme.themes.test.ts +124 -0
- package/src/theme.tsx +62 -14
- package/src/toast.tsx +35 -71
- package/src/tokens.guard.test.ts +3 -12
- package/src/ui/Alert.test.tsx +12 -0
- package/src/ui/Alert.tsx +15 -43
- package/src/ui/Badge.test.tsx +14 -3
- package/src/ui/Badge.tsx +13 -25
- package/src/ui/Button.test.tsx +17 -4
- package/src/ui/Button.tsx +10 -63
- package/src/ui/Card.test.tsx +6 -2
- package/src/ui/Card.tsx +11 -39
- package/src/ui/Checkbox.tsx +2 -19
- package/src/ui/Combobox.test.tsx +22 -0
- package/src/ui/Combobox.tsx +31 -80
- package/src/ui/DatePicker.test.tsx +131 -4
- package/src/ui/DatePicker.tsx +158 -106
- package/src/ui/Input.tsx +6 -19
- package/src/ui/Markdown.test.tsx +26 -0
- package/src/ui/Markdown.tsx +28 -2
- package/src/ui/Menu.test.tsx +38 -4
- package/src/ui/Menu.tsx +50 -52
- package/src/ui/Popover.tsx +53 -19
- package/src/ui/Radio.tsx +5 -30
- package/src/ui/Select.tsx +7 -30
- package/src/ui/Switch.tsx +2 -20
- package/src/ui/Tabs.tsx +4 -28
- package/src/ui/Textarea.tsx +6 -17
- package/src/ui/Tooltip.tsx +9 -21
- package/src/uiText.tsx +9 -0
- package/src/ui/controlStyles.ts +0 -9
|
@@ -17,6 +17,7 @@ import { DataViewTextProvider, useDataViewText } from "./internal";
|
|
|
17
17
|
import type {
|
|
18
18
|
DataViewBatchAction,
|
|
19
19
|
DataViewColumn,
|
|
20
|
+
DataViewDensity,
|
|
20
21
|
DataViewQuery,
|
|
21
22
|
DataViewRepository,
|
|
22
23
|
DataViewRowAction,
|
|
@@ -39,6 +40,21 @@ interface DataViewBaseProps<T> {
|
|
|
39
40
|
columns: DataViewColumn<T>[];
|
|
40
41
|
/** "embedded" renders a plain compact view: no view toggle, no page-size selector, no footer. */
|
|
41
42
|
variant?: "full" | "embedded";
|
|
43
|
+
/**
|
|
44
|
+
* How tightly this view packs its cells and controls (default `"comfortable"`).
|
|
45
|
+
*
|
|
46
|
+
* `"compact"` stamps `data-density="compact"` on the root, which re-scopes the live
|
|
47
|
+
* density tokens for the whole subtree — cell padding here, and the control heights
|
|
48
|
+
* `Button`, `Input` and `Select` already read, so the toolbar tightens with the table.
|
|
49
|
+
*
|
|
50
|
+
* `"comfortable"` stamps NO attribute, because comfortable IS the token sheet's
|
|
51
|
+
* `:root` value and an attribute for it would match no rule. The consequence worth
|
|
52
|
+
* knowing: inside an already-compact subtree, `density="comfortable"` does not make
|
|
53
|
+
* this view comfortable again. Expressing that needs a named comfortable copy of
|
|
54
|
+
* each live token, which ADR 0094 defers until something asks — nothing can ask
|
|
55
|
+
* until the shell takes a density of its own.
|
|
56
|
+
*/
|
|
57
|
+
density?: DataViewDensity;
|
|
42
58
|
enableSelection?: boolean;
|
|
43
59
|
batchActions?: DataViewBatchAction<T>[];
|
|
44
60
|
rowActions?: (row: T) => DataViewRowAction<T>[];
|
|
@@ -130,6 +146,13 @@ function useIsMobile(): boolean {
|
|
|
130
146
|
function DataViewInner<T>(props: DataViewProps<T>) {
|
|
131
147
|
const { strings, resolve } = useDataViewText();
|
|
132
148
|
const embedded = props.variant === "embedded";
|
|
149
|
+
// Only the compact value is expressible as an attribute; see the prop's doc comment.
|
|
150
|
+
const densityAttribute = props.density === "compact" ? "compact" : undefined;
|
|
151
|
+
// Hoisted rather than written inline at the two roots, which is what the density
|
|
152
|
+
// attribute above already does and what keeps the marker scanner out of a trap: it
|
|
153
|
+
// reads a whole expression container, so every string literal inside one counts as a
|
|
154
|
+
// marker name and a conditional written at the attribute would report phantoms.
|
|
155
|
+
const variantAttribute = embedded ? "embedded" : "full";
|
|
133
156
|
const serverSide = props.repository.capabilities.serverSide;
|
|
134
157
|
const initialPageSize = embedded
|
|
135
158
|
? EMBEDDED_PAGE_SIZE
|
|
@@ -296,7 +319,7 @@ function DataViewInner<T>(props: DataViewProps<T>) {
|
|
|
296
319
|
return props.renderError !== undefined ? (
|
|
297
320
|
<>{props.renderError(error)}</>
|
|
298
321
|
) : (
|
|
299
|
-
<div
|
|
322
|
+
<div data-terp="dataview-error">
|
|
300
323
|
<ErrorState title={strings.errorTitle} error={error} />
|
|
301
324
|
</div>
|
|
302
325
|
);
|
|
@@ -325,7 +348,7 @@ function DataViewInner<T>(props: DataViewProps<T>) {
|
|
|
325
348
|
);
|
|
326
349
|
}
|
|
327
350
|
return (
|
|
328
|
-
<div
|
|
351
|
+
<div data-terp="dataview-scroll">
|
|
329
352
|
<DataViewTable
|
|
330
353
|
rows={rows}
|
|
331
354
|
columns={state.visibleColumns}
|
|
@@ -366,7 +389,7 @@ function DataViewInner<T>(props: DataViewProps<T>) {
|
|
|
366
389
|
props.toolbarContent !== undefined ||
|
|
367
390
|
props.toolbarTrailing !== undefined;
|
|
368
391
|
return (
|
|
369
|
-
<div data-terp="dataview"
|
|
392
|
+
<div data-terp="dataview" data-variant={variantAttribute} data-density={densityAttribute}>
|
|
370
393
|
{showsEmbeddedToolbar && (
|
|
371
394
|
<DataViewToolbar<T>
|
|
372
395
|
searchEnabled={props.repository.capabilities.search}
|
|
@@ -401,15 +424,7 @@ function DataViewInner<T>(props: DataViewProps<T>) {
|
|
|
401
424
|
}
|
|
402
425
|
|
|
403
426
|
return (
|
|
404
|
-
<div
|
|
405
|
-
data-terp="dataview"
|
|
406
|
-
style={{
|
|
407
|
-
display: "grid",
|
|
408
|
-
background: "var(--color-neutral-0)",
|
|
409
|
-
border: "1px solid var(--color-neutral-200)",
|
|
410
|
-
borderRadius: "var(--radius-lg)",
|
|
411
|
-
}}
|
|
412
|
-
>
|
|
427
|
+
<div data-terp="dataview" data-variant={variantAttribute} data-density={densityAttribute}>
|
|
413
428
|
<DataViewToolbar<T>
|
|
414
429
|
searchEnabled={props.repository.capabilities.search}
|
|
415
430
|
search={state.search}
|
|
@@ -458,20 +473,23 @@ function DataViewInner<T>(props: DataViewProps<T>) {
|
|
|
458
473
|
);
|
|
459
474
|
}
|
|
460
475
|
|
|
461
|
-
/**
|
|
476
|
+
/**
|
|
477
|
+
* Fixed-height placeholder rows so the initial load never causes a layout jump.
|
|
478
|
+
*
|
|
479
|
+
* One consequence of taking those heights from the sheet is worth stating where it
|
|
480
|
+
* bites, because it is the sharpest instance of something the whole migration accepted
|
|
481
|
+
* rather than anything peculiar to this component: the style injector is guarded on
|
|
482
|
+
* `document`, so no migrated rule reaches server-rendered HTML. A server-rendered
|
|
483
|
+
* DataView emits its toolbar and this skeleton and nothing else, which makes this the
|
|
484
|
+
* one place where "no layout jump" is the entire reason the markup exists. Until
|
|
485
|
+
* hydration these five divs are now zero-height. `ssr.test.tsx` asserts the marker
|
|
486
|
+
* string only, so nothing fails — it is a trade, recorded, not an oversight.
|
|
487
|
+
*/
|
|
462
488
|
function DataViewSkeleton({ label }: { label: string }) {
|
|
463
489
|
return (
|
|
464
|
-
<div role="status" aria-label={label}
|
|
490
|
+
<div role="status" aria-label={label} data-terp="dataview-skeleton">
|
|
465
491
|
{Array.from({ length: 5 }, (_, index) => (
|
|
466
|
-
<div
|
|
467
|
-
key={index}
|
|
468
|
-
aria-hidden
|
|
469
|
-
style={{
|
|
470
|
-
height: "2.75rem",
|
|
471
|
-
background: "var(--color-neutral-100)",
|
|
472
|
-
borderRadius: "var(--radius-md)",
|
|
473
|
-
}}
|
|
474
|
-
/>
|
|
492
|
+
<div key={index} aria-hidden />
|
|
475
493
|
))}
|
|
476
494
|
</div>
|
|
477
495
|
);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
import type { BadgeTone } from "../ui/Badge";
|
|
4
|
-
import { toneSoftColors } from "../ui/Badge";
|
|
5
4
|
import type { UiText } from "../uiText";
|
|
6
5
|
|
|
7
6
|
import { DataViewExpandToggle } from "./DataViewExpandableRow";
|
|
@@ -30,18 +29,6 @@ export interface DataViewCardListProps<T> {
|
|
|
30
29
|
rowActions?: (row: T) => DataViewRowAction<T>[];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
const recordButtonStyle: CSSProperties = {
|
|
34
|
-
position: "absolute",
|
|
35
|
-
width: 1,
|
|
36
|
-
height: 1,
|
|
37
|
-
padding: 0,
|
|
38
|
-
margin: -1,
|
|
39
|
-
overflow: "hidden",
|
|
40
|
-
clip: "rect(0 0 0 0)",
|
|
41
|
-
whiteSpace: "nowrap",
|
|
42
|
-
border: 0,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
32
|
function slotValue<T>(
|
|
46
33
|
columns: DataViewColumn<T>[],
|
|
47
34
|
row: T,
|
|
@@ -68,7 +55,7 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
68
55
|
const { strings, resolve, format } = useDataViewText();
|
|
69
56
|
|
|
70
57
|
return (
|
|
71
|
-
<ul
|
|
58
|
+
<ul data-terp="dataview-card-list">
|
|
72
59
|
{props.rows.map((row) => {
|
|
73
60
|
const rowId = props.getRowId(row);
|
|
74
61
|
const expanded = props.isExpanded(rowId);
|
|
@@ -78,18 +65,9 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
78
65
|
<li key={rowId}>
|
|
79
66
|
<div
|
|
80
67
|
onClick={clickable ? () => props.onRowClick?.(row) : undefined}
|
|
81
|
-
data-terp=
|
|
68
|
+
data-terp="dataview-card"
|
|
69
|
+
data-clickable={clickable || undefined}
|
|
82
70
|
data-tone={tone ?? undefined}
|
|
83
|
-
style={{
|
|
84
|
-
display: "grid",
|
|
85
|
-
gap: "var(--space-2)",
|
|
86
|
-
padding: "var(--space-3)",
|
|
87
|
-
background: tone !== null ? toneSoftColors[tone] : "var(--color-neutral-0)",
|
|
88
|
-
border: "1px solid var(--color-neutral-200)",
|
|
89
|
-
borderRadius: "var(--radius-lg)",
|
|
90
|
-
boxShadow: "var(--shadow-sm)",
|
|
91
|
-
cursor: clickable ? "pointer" : undefined,
|
|
92
|
-
}}
|
|
93
71
|
>
|
|
94
72
|
{clickable && (
|
|
95
73
|
<button
|
|
@@ -102,10 +80,9 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
102
80
|
event.stopPropagation();
|
|
103
81
|
props.onRowClick?.(row);
|
|
104
82
|
}}
|
|
105
|
-
style={recordButtonStyle}
|
|
106
83
|
/>
|
|
107
84
|
)}
|
|
108
|
-
<div
|
|
85
|
+
<div data-terp="dataview-card-main">
|
|
109
86
|
{props.renderExpanded !== undefined && (
|
|
110
87
|
<DataViewExpandToggle
|
|
111
88
|
expanded={expanded}
|
|
@@ -113,7 +90,7 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
113
90
|
/>
|
|
114
91
|
)}
|
|
115
92
|
{props.selectionEnabled && (
|
|
116
|
-
<span onClick={(event) => event.stopPropagation()}
|
|
93
|
+
<span onClick={(event) => event.stopPropagation()}>
|
|
117
94
|
<input
|
|
118
95
|
type="checkbox"
|
|
119
96
|
aria-label={resolve(strings.selectRow)}
|
|
@@ -122,7 +99,7 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
122
99
|
/>
|
|
123
100
|
</span>
|
|
124
101
|
)}
|
|
125
|
-
<div
|
|
102
|
+
<div data-terp="dataview-card-body">
|
|
126
103
|
{props.renderCard !== undefined ? (
|
|
127
104
|
props.renderCard(row)
|
|
128
105
|
) : (
|
|
@@ -139,12 +116,7 @@ export function DataViewCardList<T>(props: DataViewCardListProps<T>) {
|
|
|
139
116
|
)}
|
|
140
117
|
</div>
|
|
141
118
|
{expanded && props.renderExpanded !== undefined && (
|
|
142
|
-
<div
|
|
143
|
-
style={{
|
|
144
|
-
borderTop: "1px solid var(--color-neutral-200)",
|
|
145
|
-
paddingTop: "var(--space-2)",
|
|
146
|
-
}}
|
|
147
|
-
>
|
|
119
|
+
<div data-terp="dataview-card-expanded">
|
|
148
120
|
{props.renderExpanded(row)}
|
|
149
121
|
</div>
|
|
150
122
|
)}
|
|
@@ -162,33 +134,15 @@ function DefaultCardBody<T>({ row, columns }: { row: T; columns: DataViewColumn<
|
|
|
162
134
|
const status = slotValue(columns, row, "status");
|
|
163
135
|
const date = slotValue(columns, row, "date");
|
|
164
136
|
return (
|
|
165
|
-
<div
|
|
166
|
-
<div
|
|
167
|
-
<span
|
|
137
|
+
<div data-terp="dataview-card-fields">
|
|
138
|
+
<div data-terp="dataview-card-heading">
|
|
139
|
+
<span data-terp="dataview-card-title">{title}</span>
|
|
168
140
|
{status !== null && (
|
|
169
|
-
<span
|
|
170
|
-
style={{
|
|
171
|
-
fontSize: "var(--font-size-sm)",
|
|
172
|
-
padding: "0 var(--space-2)",
|
|
173
|
-
background: "var(--color-neutral-100)",
|
|
174
|
-
borderRadius: "var(--radius-full)",
|
|
175
|
-
color: "var(--color-neutral-700)",
|
|
176
|
-
}}
|
|
177
|
-
>
|
|
178
|
-
{status}
|
|
179
|
-
</span>
|
|
141
|
+
<span data-terp="dataview-card-status">{status}</span>
|
|
180
142
|
)}
|
|
181
143
|
</div>
|
|
182
|
-
{subtitle !== null &&
|
|
183
|
-
|
|
184
|
-
{subtitle}
|
|
185
|
-
</span>
|
|
186
|
-
)}
|
|
187
|
-
{date !== null && (
|
|
188
|
-
<span style={{ fontSize: "var(--font-size-sm)", color: "var(--color-neutral-500)" }}>
|
|
189
|
-
{date}
|
|
190
|
-
</span>
|
|
191
|
-
)}
|
|
144
|
+
{subtitle !== null && <span data-terp="dataview-card-meta">{subtitle}</span>}
|
|
145
|
+
{date !== null && <span data-terp="dataview-card-meta">{date}</span>}
|
|
192
146
|
</div>
|
|
193
147
|
);
|
|
194
148
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
|
|
3
|
+
import { Popover } from "../ui/Popover";
|
|
1
4
|
import { ArrowDownGlyph, ArrowUpGlyph, ColumnsGlyph } from "./glyphs";
|
|
2
|
-
import {
|
|
5
|
+
import { useDataViewText } from "./internal";
|
|
3
6
|
import type { DataViewColumn } from "./types";
|
|
4
7
|
|
|
5
8
|
export interface DataViewColumnSettingsProps<T> {
|
|
@@ -8,68 +11,72 @@ export interface DataViewColumnSettingsProps<T> {
|
|
|
8
11
|
columnVisibility: Record<string, boolean>;
|
|
9
12
|
onColumnVisibleChange: (columnId: string, visible: boolean) => void;
|
|
10
13
|
onMoveColumn: (columnId: string, direction: -1 | 1) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Render the panel open on mount — the same escape the menu primitive already exposes,
|
|
16
|
+
* and the only way a portalled panel reaches a per-specimen visual lane.
|
|
17
|
+
*/
|
|
18
|
+
defaultOpen?: boolean;
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
/**
|
|
14
|
-
* The "view options"
|
|
22
|
+
* The "view options" panel: per-column show/hide checkboxes and up/down reordering.
|
|
15
23
|
* Only user columns appear — the pinned system columns (select/expand/actions) are
|
|
16
24
|
* never hideable or reorderable, and are skipped when computing reorder targets
|
|
17
25
|
* because this list simply does not contain them.
|
|
26
|
+
*
|
|
27
|
+
* A `Popover` rather than a `Menu`, and the distinction is a correctness one rather than
|
|
28
|
+
* a preference. `Menu` stamps `role="menu"`, which ARIA requires to own only
|
|
29
|
+
* `menuitem` / `menuitemradio` / `menuitemcheckbox` / `group` children — and this panel's
|
|
30
|
+
* content is a heading, labelled checkboxes and paired reorder buttons, which is a small
|
|
31
|
+
* FORM. Feeding it to a menu produced a critical `aria-required-children` violation in
|
|
32
|
+
* every theme, and there is no arrangement that fixes it while keeping the interaction:
|
|
33
|
+
* reorder buttons inside a `menuitemcheckbox` would be interactive descendants of a menu
|
|
34
|
+
* item, which ARIA forbids in turn. The panel is therefore a group labelled by its own
|
|
35
|
+
* heading, navigated with Tab the way a form is, and `Popover` supplies the Escape,
|
|
36
|
+
* outside-click and focus-return behaviour `Menu` was being used for.
|
|
37
|
+
*
|
|
38
|
+
* Nothing had rendered this panel open before, which is why a shipped critical defect was
|
|
39
|
+
* invisible: the screenshot lane clips to the specimen and the panel portals to the body,
|
|
40
|
+
* and no specimen opened it at all.
|
|
18
41
|
*/
|
|
19
42
|
export function DataViewColumnSettings<T>({
|
|
20
43
|
columns,
|
|
21
44
|
columnVisibility,
|
|
22
45
|
onColumnVisibleChange,
|
|
23
46
|
onMoveColumn,
|
|
47
|
+
defaultOpen,
|
|
24
48
|
}: DataViewColumnSettingsProps<T>) {
|
|
25
49
|
const { strings, resolve } = useDataViewText();
|
|
50
|
+
const headingId = useId();
|
|
26
51
|
|
|
27
52
|
return (
|
|
28
|
-
<
|
|
53
|
+
<Popover
|
|
54
|
+
defaultOpen={defaultOpen}
|
|
55
|
+
focusOnOpen
|
|
56
|
+
data-owner="dataview-column-settings"
|
|
29
57
|
trigger={
|
|
30
|
-
|
|
58
|
+
// The trigger keeps the menu-trigger marker, which is styling vocabulary rather than
|
|
59
|
+
// a semantic claim — the sheet describes it as the package's outlined control, and
|
|
60
|
+
// reusing it is what keeps this control's appearance identical. What it does NOT keep
|
|
61
|
+
// is aria-haspopup="menu", because the panel is no longer a menu; Popover supplies
|
|
62
|
+
// aria-expanded and aria-controls, which is the whole contract for a disclosure.
|
|
63
|
+
<button type="button" data-terp="menu-trigger">
|
|
31
64
|
<ColumnsGlyph />
|
|
32
|
-
<span
|
|
33
|
-
|
|
65
|
+
<span>{resolve(strings.viewOptions)}</span>
|
|
66
|
+
</button>
|
|
34
67
|
}
|
|
35
|
-
triggerLabel={resolve(strings.viewOptions)}
|
|
36
68
|
>
|
|
37
69
|
{() => (
|
|
38
|
-
<div
|
|
39
|
-
<div
|
|
40
|
-
style={{
|
|
41
|
-
padding: "var(--space-1) var(--space-2)",
|
|
42
|
-
fontSize: "var(--font-size-sm)",
|
|
43
|
-
fontWeight: "var(--font-weight-medium)" as never,
|
|
44
|
-
color: "var(--color-neutral-500)",
|
|
45
|
-
}}
|
|
46
|
-
>
|
|
70
|
+
<div data-terp="dataview-column-settings" role="group" aria-labelledby={headingId}>
|
|
71
|
+
<div id={headingId} data-terp="dataview-column-settings-title">
|
|
47
72
|
{resolve(strings.columns)}
|
|
48
73
|
</div>
|
|
49
74
|
{columns.map((column, index) => {
|
|
50
75
|
const label = resolve(column.meta?.label ?? column.header);
|
|
51
76
|
const visible = columnVisibility[column.id] !== false;
|
|
52
77
|
return (
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
style={{
|
|
56
|
-
display: "flex",
|
|
57
|
-
alignItems: "center",
|
|
58
|
-
gap: "var(--space-2)",
|
|
59
|
-
padding: "var(--space-1) var(--space-2)",
|
|
60
|
-
borderRadius: "var(--radius-sm)",
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
63
|
-
<label
|
|
64
|
-
style={{
|
|
65
|
-
display: "flex",
|
|
66
|
-
alignItems: "center",
|
|
67
|
-
gap: "var(--space-2)",
|
|
68
|
-
flex: 1,
|
|
69
|
-
cursor: "pointer",
|
|
70
|
-
fontSize: "var(--font-size-sm)",
|
|
71
|
-
}}
|
|
72
|
-
>
|
|
78
|
+
<div key={column.id} data-terp="dataview-column-option">
|
|
79
|
+
<label>
|
|
73
80
|
<input
|
|
74
81
|
type="checkbox"
|
|
75
82
|
checked={visible}
|
|
@@ -80,19 +87,19 @@ export function DataViewColumnSettings<T>({
|
|
|
80
87
|
</label>
|
|
81
88
|
<button
|
|
82
89
|
type="button"
|
|
90
|
+
data-terp="iconbutton"
|
|
83
91
|
aria-label={`${resolve(strings.moveUp)}: ${label}`}
|
|
84
92
|
disabled={index === 0}
|
|
85
93
|
onClick={() => onMoveColumn(column.id, -1)}
|
|
86
|
-
style={reorderButtonStyle(index === 0)}
|
|
87
94
|
>
|
|
88
95
|
<ArrowUpGlyph size={14} />
|
|
89
96
|
</button>
|
|
90
97
|
<button
|
|
91
98
|
type="button"
|
|
99
|
+
data-terp="iconbutton"
|
|
92
100
|
aria-label={`${resolve(strings.moveDown)}: ${label}`}
|
|
93
101
|
disabled={index === columns.length - 1}
|
|
94
102
|
onClick={() => onMoveColumn(column.id, 1)}
|
|
95
|
-
style={reorderButtonStyle(index === columns.length - 1)}
|
|
96
103
|
>
|
|
97
104
|
<ArrowDownGlyph size={14} />
|
|
98
105
|
</button>
|
|
@@ -101,18 +108,6 @@ export function DataViewColumnSettings<T>({
|
|
|
101
108
|
})}
|
|
102
109
|
</div>
|
|
103
110
|
)}
|
|
104
|
-
</
|
|
111
|
+
</Popover>
|
|
105
112
|
);
|
|
106
113
|
}
|
|
107
|
-
|
|
108
|
-
function reorderButtonStyle(disabled: boolean) {
|
|
109
|
-
return {
|
|
110
|
-
display: "inline-flex",
|
|
111
|
-
padding: "var(--space-1)",
|
|
112
|
-
background: "transparent",
|
|
113
|
-
border: "none",
|
|
114
|
-
borderRadius: "var(--radius-sm)",
|
|
115
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
116
|
-
color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
|
|
117
|
-
} as const;
|
|
118
|
-
}
|
|
@@ -20,19 +20,11 @@ export function DataViewExpandToggle({
|
|
|
20
20
|
type="button"
|
|
21
21
|
aria-label={resolve(expanded ? strings.collapseRow : strings.expandRow)}
|
|
22
22
|
aria-expanded={expanded}
|
|
23
|
+
data-terp="iconbutton"
|
|
23
24
|
onClick={(event) => {
|
|
24
25
|
event.stopPropagation();
|
|
25
26
|
onToggle();
|
|
26
27
|
}}
|
|
27
|
-
style={{
|
|
28
|
-
display: "inline-flex",
|
|
29
|
-
padding: "var(--space-1)",
|
|
30
|
-
background: "transparent",
|
|
31
|
-
border: "none",
|
|
32
|
-
borderRadius: "var(--radius-sm)",
|
|
33
|
-
cursor: "pointer",
|
|
34
|
-
color: "var(--color-neutral-500)",
|
|
35
|
-
}}
|
|
36
28
|
>
|
|
37
29
|
{expanded ? <ChevronDownGlyph /> : <ChevronRightGlyph />}
|
|
38
30
|
</button>
|
|
@@ -52,14 +44,7 @@ export function DataViewExpandableRow({
|
|
|
52
44
|
}) {
|
|
53
45
|
return (
|
|
54
46
|
<tr>
|
|
55
|
-
<td
|
|
56
|
-
colSpan={colSpan}
|
|
57
|
-
style={{
|
|
58
|
-
padding: "var(--space-3) var(--space-4)",
|
|
59
|
-
background: "var(--color-neutral-50)",
|
|
60
|
-
borderBottom: "1px solid var(--color-neutral-200)",
|
|
61
|
-
}}
|
|
62
|
-
>
|
|
47
|
+
<td colSpan={colSpan} data-terp="dataview-expanded-cell">
|
|
63
48
|
{children}
|
|
64
49
|
</td>
|
|
65
50
|
</tr>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { CSSProperties } from "react";
|
|
2
|
-
|
|
3
1
|
import { useDataViewText } from "./internal";
|
|
4
2
|
import {
|
|
5
3
|
PageFirstGlyph,
|
|
@@ -15,18 +13,6 @@ export interface DataViewPaginationProps {
|
|
|
15
13
|
onPaginationChange: (pagination: DataViewPaginationState) => void;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
const pagerButtonStyle = (disabled: boolean): CSSProperties => ({
|
|
19
|
-
display: "inline-flex",
|
|
20
|
-
alignItems: "center",
|
|
21
|
-
minHeight: "2rem",
|
|
22
|
-
padding: "var(--space-1) var(--space-2)",
|
|
23
|
-
background: "var(--color-neutral-0)",
|
|
24
|
-
border: "1px solid var(--color-neutral-300)",
|
|
25
|
-
borderRadius: "var(--radius-md)",
|
|
26
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
27
|
-
color: disabled ? "var(--color-neutral-300)" : "var(--color-neutral-700)",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
16
|
/**
|
|
31
17
|
* The footer pagination bar: "X–Y of Z results", the current page / page count, and
|
|
32
18
|
* first / prev / next / last controls (disabled at bounds; page controls hidden when
|
|
@@ -49,22 +35,10 @@ export function DataViewPagination({
|
|
|
49
35
|
const atLast = pageIndex >= pageCount - 1;
|
|
50
36
|
|
|
51
37
|
return (
|
|
52
|
-
<div
|
|
53
|
-
style={{
|
|
54
|
-
display: "flex",
|
|
55
|
-
alignItems: "center",
|
|
56
|
-
justifyContent: "space-between",
|
|
57
|
-
gap: "var(--space-3)",
|
|
58
|
-
flexWrap: "wrap",
|
|
59
|
-
padding: "var(--space-2) var(--space-3)",
|
|
60
|
-
borderTop: "1px solid var(--color-neutral-200)",
|
|
61
|
-
fontSize: "var(--font-size-sm)",
|
|
62
|
-
color: "var(--color-neutral-500)",
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
38
|
+
<div data-terp="dataview-pagination">
|
|
65
39
|
<span>{format(strings.resultsRange, { from, to, total: totalCount })}</span>
|
|
66
40
|
{pageCount > 1 && (
|
|
67
|
-
<span
|
|
41
|
+
<span data-terp="dataview-pager">
|
|
68
42
|
<span>{format(strings.pageOf, { page: pageIndex + 1, pages: pageCount })}</span>
|
|
69
43
|
<button
|
|
70
44
|
type="button"
|
|
@@ -72,7 +46,6 @@ export function DataViewPagination({
|
|
|
72
46
|
disabled={atFirst}
|
|
73
47
|
onClick={() => goTo(0)}
|
|
74
48
|
data-terp="iconbutton"
|
|
75
|
-
style={pagerButtonStyle(atFirst)}
|
|
76
49
|
>
|
|
77
50
|
<PageFirstGlyph />
|
|
78
51
|
</button>
|
|
@@ -82,7 +55,6 @@ export function DataViewPagination({
|
|
|
82
55
|
disabled={atFirst}
|
|
83
56
|
onClick={() => goTo(pageIndex - 1)}
|
|
84
57
|
data-terp="iconbutton"
|
|
85
|
-
style={pagerButtonStyle(atFirst)}
|
|
86
58
|
>
|
|
87
59
|
<PagePrevGlyph />
|
|
88
60
|
</button>
|
|
@@ -92,7 +64,6 @@ export function DataViewPagination({
|
|
|
92
64
|
disabled={atLast}
|
|
93
65
|
onClick={() => goTo(pageIndex + 1)}
|
|
94
66
|
data-terp="iconbutton"
|
|
95
|
-
style={pagerButtonStyle(atLast)}
|
|
96
67
|
>
|
|
97
68
|
<PageNextGlyph />
|
|
98
69
|
</button>
|
|
@@ -102,7 +73,6 @@ export function DataViewPagination({
|
|
|
102
73
|
disabled={atLast}
|
|
103
74
|
onClick={() => goTo(pageCount - 1)}
|
|
104
75
|
data-terp="iconbutton"
|
|
105
|
-
style={pagerButtonStyle(atLast)}
|
|
106
76
|
>
|
|
107
77
|
<PageLastGlyph />
|
|
108
78
|
</button>
|
|
@@ -14,6 +14,12 @@ export interface DataViewRowActionsProps<T> {
|
|
|
14
14
|
layout: DataViewRowActionsLayout;
|
|
15
15
|
/** On mobile all standard actions collapse into the menu regardless of flags. */
|
|
16
16
|
isMobile: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Render the overflow menu open on mount — the same escape `Menu`, `Popover` and both
|
|
19
|
+
* date pickers already expose, and for the same reason: a portalled panel is invisible
|
|
20
|
+
* to a per-specimen visual lane unless something can name its open state.
|
|
21
|
+
*/
|
|
22
|
+
defaultOpen?: boolean;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>; row: T }) {
|
|
@@ -23,30 +29,12 @@ function InlineActionButton<T>({ action, row }: { action: DataViewRowAction<T>;
|
|
|
23
29
|
return (
|
|
24
30
|
<button
|
|
25
31
|
type="button"
|
|
32
|
+
data-terp="dataview-inline-action"
|
|
33
|
+
data-destructive={destructive || undefined}
|
|
26
34
|
disabled={disabled}
|
|
27
35
|
onClick={() => action.onClick?.(row)}
|
|
28
|
-
style={{
|
|
29
|
-
font: "inherit",
|
|
30
|
-
fontSize: "var(--font-size-sm)",
|
|
31
|
-
display: "inline-flex",
|
|
32
|
-
alignItems: "center",
|
|
33
|
-
minHeight: "2rem",
|
|
34
|
-
gap: "var(--space-1)",
|
|
35
|
-
padding: "var(--space-1) var(--space-2)",
|
|
36
|
-
background: "transparent",
|
|
37
|
-
border: "1px solid var(--color-neutral-300)",
|
|
38
|
-
borderRadius: "var(--radius-md)",
|
|
39
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
40
|
-
color: disabled
|
|
41
|
-
? "var(--color-neutral-300)"
|
|
42
|
-
: destructive
|
|
43
|
-
? "var(--color-status-danger)"
|
|
44
|
-
: "var(--color-neutral-700)",
|
|
45
|
-
}}
|
|
46
36
|
>
|
|
47
|
-
{action.icon !== undefined &&
|
|
48
|
-
<span aria-hidden style={{ display: "inline-flex" }}>{action.icon}</span>
|
|
49
|
-
)}
|
|
37
|
+
{action.icon !== undefined && <span aria-hidden>{action.icon}</span>}
|
|
50
38
|
{resolve(action.label)}
|
|
51
39
|
</button>
|
|
52
40
|
);
|
|
@@ -62,6 +50,7 @@ export function DataViewRowActions<T>({
|
|
|
62
50
|
actions,
|
|
63
51
|
layout,
|
|
64
52
|
isMobile,
|
|
53
|
+
defaultOpen,
|
|
65
54
|
}: DataViewRowActionsProps<T>) {
|
|
66
55
|
const { strings, resolve } = useDataViewText();
|
|
67
56
|
|
|
@@ -83,21 +72,11 @@ export function DataViewRowActions<T>({
|
|
|
83
72
|
const stop = (event: MouseEvent) => event.stopPropagation();
|
|
84
73
|
|
|
85
74
|
const renderCustom = (action: DataViewRowAction<T>, index: number): ReactNode => (
|
|
86
|
-
<span key={`custom-${index}`}
|
|
87
|
-
{action.render?.(row)}
|
|
88
|
-
</span>
|
|
75
|
+
<span key={`custom-${index}`}>{action.render?.(row)}</span>
|
|
89
76
|
);
|
|
90
77
|
|
|
91
78
|
return (
|
|
92
|
-
<span
|
|
93
|
-
onClick={stop}
|
|
94
|
-
style={{
|
|
95
|
-
display: "inline-flex",
|
|
96
|
-
alignItems: "center",
|
|
97
|
-
justifyContent: "flex-end",
|
|
98
|
-
gap: "var(--space-1)",
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
79
|
+
<span data-terp="dataview-row-actions" onClick={stop}>
|
|
101
80
|
{custom.map(renderCustom)}
|
|
102
81
|
{inline.map((action, index) => (
|
|
103
82
|
<InlineActionButton key={`inline-${index}`} action={action} row={row} />
|
|
@@ -106,6 +85,7 @@ export function DataViewRowActions<T>({
|
|
|
106
85
|
<DataViewMenu
|
|
107
86
|
trigger={<EllipsisGlyph />}
|
|
108
87
|
triggerLabel={resolve(strings.moreActions)}
|
|
88
|
+
defaultOpen={defaultOpen}
|
|
109
89
|
>
|
|
110
90
|
{(close) => (
|
|
111
91
|
<>
|