@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "studio-shared",
3
3
  "configKey": "studioShared",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -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
- class="flex items-center gap-2 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"
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
- {{ entry.label }}
496
- <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
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
- <slot name="empty">
557
- {{ emptyText ?? t("shared.datatable.empty.default") }}
558
- </slot>
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.1",
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.0",
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",