@revenexx/studio-shared 0.3.1 → 0.3.2
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/module.json
CHANGED
|
@@ -11,8 +11,11 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
|
|
|
11
11
|
data: T[];
|
|
12
12
|
columns: DataTableColumn<T>[];
|
|
13
13
|
rowKey: RowKey<T>;
|
|
14
|
+
/** `label` is the field's accessible name; it defaults to the placeholder
|
|
15
|
+
* where the surface wrote one (see `searchLabel`). */
|
|
14
16
|
search?: boolean | {
|
|
15
17
|
placeholder?: string;
|
|
18
|
+
label?: string;
|
|
16
19
|
};
|
|
17
20
|
columnsMenu?: boolean;
|
|
18
21
|
/** Mount the surface's own filter panel in the toolbar's Filter popover
|
|
@@ -224,6 +224,11 @@ function toggleSort(column) {
|
|
|
224
224
|
}
|
|
225
225
|
sort.value = { column: null, direction: null };
|
|
226
226
|
}
|
|
227
|
+
function ariaSortFor(col) {
|
|
228
|
+
if (!col.sortable) return void 0;
|
|
229
|
+
if (sort.value.column !== col.key) return "none";
|
|
230
|
+
return sort.value.direction === "desc" ? "descending" : "ascending";
|
|
231
|
+
}
|
|
227
232
|
function rowKeyFor(row) {
|
|
228
233
|
if (typeof props.rowKey === "function") return props.rowKey(row);
|
|
229
234
|
return row[props.rowKey];
|
|
@@ -235,6 +240,10 @@ const gridTemplate = computed(() => {
|
|
|
235
240
|
const placeholder = computed(
|
|
236
241
|
() => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
|
|
237
242
|
);
|
|
243
|
+
const searchLabel = computed(() => {
|
|
244
|
+
const configured = typeof props.search === "object" ? props.search.label ?? props.search.placeholder : void 0;
|
|
245
|
+
return configured ?? t("shared.datatable.toolbar.search.aria.label");
|
|
246
|
+
});
|
|
238
247
|
const slots = useSlots();
|
|
239
248
|
const showToolbar = computed(
|
|
240
249
|
() => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"]
|
|
@@ -338,6 +347,7 @@ function defaultCell(col, row) {
|
|
|
338
347
|
v-if="search"
|
|
339
348
|
v-model="query"
|
|
340
349
|
:placeholder="placeholder"
|
|
350
|
+
:label="searchLabel"
|
|
341
351
|
class="w-[21rem] min-w-40 max-w-full shrink"
|
|
342
352
|
>
|
|
343
353
|
<template #icon>
|
|
@@ -445,21 +455,45 @@ function defaultCell(col, row) {
|
|
|
445
455
|
<!-- The header and the rows share one box so their columns cannot drift.
|
|
446
456
|
`display: contents` when scrolling is off makes this element vanish
|
|
447
457
|
from layout entirely, so every table that has not asked for it renders
|
|
448
|
-
exactly as it did.
|
|
458
|
+
exactly as it did.
|
|
459
|
+
|
|
460
|
+
IT IS ALSO THE TABLE. The box that holds the header and the rows and
|
|
461
|
+
nothing else is the only element in this component that can carry the
|
|
462
|
+
role: the root has the toolbar and the footer in it, and a `table` may
|
|
463
|
+
own rows and rowgroups only. Which is why everything below is a `row`
|
|
464
|
+
or is hidden — an empty state or a skeleton left as a bare `div` here
|
|
465
|
+
would be a child the role does not allow, and the whole table's
|
|
466
|
+
semantics are then whatever the browser decides to salvage.
|
|
467
|
+
|
|
468
|
+
`table` and not `grid`. `grid` is the interactive one: it promises
|
|
469
|
+
arrow keys move a focus between cells, and nothing here implements
|
|
470
|
+
that. A static table read with a screen reader's own table commands is
|
|
471
|
+
what these lists actually are, and both roles announce the column
|
|
472
|
+
heading with the cell, which is the thing that was missing. -->
|
|
449
473
|
<div
|
|
474
|
+
:role="showTable ? 'table' : void 0"
|
|
475
|
+
:aria-busy="showTable && loading ? 'true' : void 0"
|
|
450
476
|
:class="scrollBody ? 'min-h-0 overflow-y-auto' : 'contents'"
|
|
451
477
|
:style="scrollStyle"
|
|
452
478
|
>
|
|
453
479
|
<!-- Header row -------------------------------------------------- -->
|
|
454
480
|
<div
|
|
455
481
|
v-if="showTable"
|
|
482
|
+
role="row"
|
|
456
483
|
class="grid items-center gap-2 border-t rounded-t-lg border-gray-200 bg-gray-100 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
|
|
457
484
|
:class="scrollBody ? 'sticky top-0 z-10' : ''"
|
|
458
485
|
:style="gridTemplate"
|
|
459
486
|
>
|
|
487
|
+
<!-- `columnheader` is what names every cell underneath it, so this is
|
|
488
|
+
the half of the fix that does the work. `aria-sort` goes with it:
|
|
489
|
+
a heading that carries a sort button and says nothing about the
|
|
490
|
+
current direction is a control whose state is visible only to
|
|
491
|
+
people who can see the arrow. -->
|
|
460
492
|
<div
|
|
461
493
|
v-for="col in visibleColumns"
|
|
462
494
|
:key="col.key"
|
|
495
|
+
role="columnheader"
|
|
496
|
+
:aria-sort="ariaSortFor(col)"
|
|
463
497
|
:class="col.alignRight ? 'text-right' : ''"
|
|
464
498
|
>
|
|
465
499
|
<!-- `#header-<key>` lets a page take over a header cell (e.g. a
|
|
@@ -487,16 +521,22 @@ function defaultCell(col, row) {
|
|
|
487
521
|
|
|
488
522
|
<!-- Body --------------------------------------------------------- -->
|
|
489
523
|
<template v-for="entry in showTable ? renderRows : []" :key="entry.key">
|
|
490
|
-
<!-- Group header (groupBy mode)
|
|
524
|
+
<!-- Group header (groupBy mode) — a row of one cell that spans the lot.
|
|
525
|
+
The flex moves one element in, onto the cell: a `row` may hold
|
|
526
|
+
cells and the label and the count are neither. -->
|
|
491
527
|
<div
|
|
492
528
|
v-if="entry.kind === 'header'"
|
|
493
|
-
|
|
529
|
+
role="row"
|
|
530
|
+
class="border-t border-black/[0.04] bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
|
|
494
531
|
>
|
|
495
|
-
|
|
496
|
-
|
|
532
|
+
<div role="cell" :aria-colspan="visibleColumns.length" class="flex items-center gap-2">
|
|
533
|
+
{{ entry.label }}
|
|
534
|
+
<span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
|
|
535
|
+
</div>
|
|
497
536
|
</div>
|
|
498
537
|
<div
|
|
499
538
|
v-else
|
|
539
|
+
role="row"
|
|
500
540
|
class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
|
|
501
541
|
:style="gridTemplate"
|
|
502
542
|
@click="emit('rowClick', entry.row)"
|
|
@@ -504,6 +544,7 @@ function defaultCell(col, row) {
|
|
|
504
544
|
<div
|
|
505
545
|
v-for="col in visibleColumns"
|
|
506
546
|
:key="col.key"
|
|
547
|
+
role="cell"
|
|
507
548
|
class="min-w-0"
|
|
508
549
|
:class="col.alignRight ? 'text-right' : ''"
|
|
509
550
|
>
|
|
@@ -527,11 +568,16 @@ function defaultCell(col, row) {
|
|
|
527
568
|
</div>
|
|
528
569
|
</template>
|
|
529
570
|
|
|
530
|
-
<!-- Loading skeleton (only when there are no rows yet) -----------
|
|
571
|
+
<!-- Loading skeleton (only when there are no rows yet) -----------
|
|
572
|
+
Hidden rather than given roles. Six rows of pulsing grey carry no
|
|
573
|
+
information to read out, and announcing them as six empty rows would
|
|
574
|
+
be worse than silence; the table says it is `aria-busy` instead,
|
|
575
|
+
which is the same news in one word. -->
|
|
531
576
|
<template v-if="showTable && loading && !displayedRows.length">
|
|
532
577
|
<div
|
|
533
578
|
v-for="n in 6"
|
|
534
579
|
:key="`sk-${n}`"
|
|
580
|
+
aria-hidden="true"
|
|
535
581
|
class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
|
|
536
582
|
:style="gridTemplate"
|
|
537
583
|
>
|
|
@@ -551,11 +597,14 @@ function defaultCell(col, row) {
|
|
|
551
597
|
<!-- Empty -------------------------------------------------------- -->
|
|
552
598
|
<div
|
|
553
599
|
v-if="showTable && !displayedRows.length && !loading"
|
|
600
|
+
role="row"
|
|
554
601
|
class="px-3 py-8 text-center text-sm text-muted-foreground"
|
|
555
602
|
>
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
603
|
+
<div role="cell" :aria-colspan="visibleColumns.length">
|
|
604
|
+
<slot name="empty">
|
|
605
|
+
{{ emptyText ?? t("shared.datatable.empty.default") }}
|
|
606
|
+
</slot>
|
|
607
|
+
</div>
|
|
559
608
|
</div>
|
|
560
609
|
|
|
561
610
|
</div>
|
|
@@ -11,8 +11,11 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
|
|
|
11
11
|
data: T[];
|
|
12
12
|
columns: DataTableColumn<T>[];
|
|
13
13
|
rowKey: RowKey<T>;
|
|
14
|
+
/** `label` is the field's accessible name; it defaults to the placeholder
|
|
15
|
+
* where the surface wrote one (see `searchLabel`). */
|
|
14
16
|
search?: boolean | {
|
|
15
17
|
placeholder?: string;
|
|
18
|
+
label?: string;
|
|
16
19
|
};
|
|
17
20
|
columnsMenu?: boolean;
|
|
18
21
|
/** Mount the surface's own filter panel in the toolbar's Filter popover
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revenexx/studio-shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared revenexx Studio shell primitives (PageHeader, SidebarNav, StateCard, DataTable, …) + the platform provider contract (usePlatformAuth/usePlatformTenant). A Nuxt module — add it to `modules`.",
|
|
6
6
|
"main": "./dist/module.mjs",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@nuxt/kit": "^4.0.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@revenexx/studio": "^0.2.
|
|
31
|
+
"@revenexx/studio": "^0.2.3",
|
|
32
32
|
"@revenexx/talkback-js": "^0.3.0",
|
|
33
33
|
"@solar-icons/nuxt": "^1.2.0",
|
|
34
34
|
"centrifuge": ">=5.2.0",
|