Skip to content

Commit 9fbbb17

Browse files
fix(components): RefreshIndicator requires its accessible name; four views pass a translated one (objectui#10580) (#10635)
Fixes #10580 Clause-②: yes `RefreshIndicator`'s published `ariaLabel` prop loses its English default ("Refreshing") and becomes required, so a caller that passes no name is a type error instead of an English accessible name. This is the "declaration enforces" option the triage allowed on this PR (triage comment `5831898614`), taken by the seat's claim (`5832044126`). The four views that passed no name now pass their translated one. It is the objectui#3546 class: text a user meets that bypasses the translation layer. ## What changes - `packages/components/src/custom/refresh-indicator.tsx`: `ariaLabel: string` is required, the `= "Refreshing"` default is gone, and the docblock says why. - `ObjectGrid` passes `t('grid.refreshing')` and `ListView` passes `t('list.refreshing')`: the same keys, through the same hooks, that their pull-to-refresh text already reads. No new grid or list key. - `ObjectChart` passes `t('chart.refreshing')`. The `t` comes from the `useObjectTranslation()` call the file already makes for `language`. It is deliberately not the file's `tt` (`useSafeTranslate`), because `tt` takes an English fallback string at every call, and the ruling is no English fallback anywhere. - `ObjectDataTable` passes `t('dashboard.refreshing')` from the `useObjectTranslation()` call it already makes. - `packages/i18n/src/locales/*.ts`: `chart.refreshing` in the `chart` block and `dashboard.refreshing` at the `dashboard` block's top level, each placed after that block's `loading` key, in all ten packs. Each pack's value is the translation its own `grid.refreshing` already carries. - `.changeset/10580-refresh-indicator-translated-name.md`: `minor` for `@object-ui/components`, with the break stated as BREAKING (this repo marks its own breaking changes `minor`), and `patch` for `plugin-grid`, `plugin-list`, `plugin-charts`, `plugin-dashboard` and `i18n`. The changeset prose states the contract change, so a contract review is owed. ## Caller census (H4) `git grep -n 'RefreshIndicator'` over the whole tree, re-run on the merged head `641ed9f`, finds five render sites. After this PR every one passes a translated name: `ObjectGrid`, `ListView`, `ObjectChart`, `ObjectDataTable`, and `ObjectGantt`, which already passed `t('gantt.aria.refreshing')` and is unchanged. Before this PR no other package, app, example, docs snippet or test rendered it. `RefreshIndicatorProps` has no other code reader; its one other mention is a docblock in `MetricWidget.tsx`. No docs page renders the component, so `check:doc-snippets` and `check:doc-types` have nothing of it to read and were not run. The sibling objectstack checkout has zero `RefreshIndicator` hits. The merge of `origin/main` brought in no new render site. ## H5 `ariaLabel=""` still type-checks, because the prop is a plain `string`. As ruled, no runtime check was added: the type is the gate. ## Pins | File | What it holds | |---|---| | `packages/components/src/__tests__/refresh-indicator-required-name-10580.test.tsx` | TYPE: `RefreshIndicator` with no `ariaLabel` sits under `@ts-expect-error`, checked by the package's `tsc -p tsconfig.test.json`. DOM: the bar is named with exactly the passed string, and a caller that casts past the type gets no `aria-label` at all, so there is no English fallback left. | | `packages/plugin-grid/src/__tests__/ObjectGrid.refreshIndicatorLocale-10580.test.tsx` | `de`: the bar is named `Wird aktualisiert…` during a bus-driven re-read. `en` control: `Refreshing…`. | | `packages/plugin-list/src/__tests__/ListView.refreshIndicatorLocale-10580.test.tsx` | `zh`: `刷新中…` during a toolbar-refresh re-read. `en` control: `Refreshing…`. | | `packages/plugin-charts/src/ObjectChart.refreshIndicatorLocale-10580.test.tsx` | `zh`: `刷新中…` during a bus-driven re-read. `en` control: `Refreshing…`. | | `packages/plugin-dashboard/src/__tests__/ObjectDataTable.refreshIndicatorLocale-10580.test.tsx` | `ja`: `更新中…` during a filter-change re-read. `en` control: `Refreshing…`. | In every view pin the second query is held open by hand, so the bar is read while the query really is in flight. `ObjectGantt.refreshInPlace-7237.test.tsx` was run unchanged and stays green. ## Reverse verification (each leg ran from a committed state, with trap restore and restore proven by hash) 1. **Base leg.** The implementation files (the component, the four callers, the ten packs) were checked out at BASE `adeecd6`, with the pins taken from HEAD `7a1b619`. The 5 pin files then gave exit **1**, `Tests 9 failed | 3 passed (12)`. The failure dump reads `aria-label="Refreshing"` on the progress bar. On-disk proof of the mutation: the default literal counted 1 and the grid caller's name counted 0. After the restore (`git checkout HEAD --`), `git diff HEAD` was 0 bytes and all 15 paths were hash-equal to their HEAD blobs. The same 5 files at HEAD gave exit **0**, `Tests 12 passed (12)`. 2. **Type leg.** `ariaLabel?: string` was written back on disk (optional count 1, required count 0). `tsc -p tsconfig.test.json` in `packages/components` then gave exit **2**, `error TS2578: Unused '@ts-expect-error' directive.` at the pin. After the restore it gave exit **0**. `--listFiles` shows the pin file in that program (1 hit). 3. **Consumer leg** (proves the plugins read the rebuilt `dist/*.d.ts`, whose `refresh-indicator.d.ts` declares `ariaLabel: string;`). With `ObjectGrid`'s `ariaLabel` dropped on disk, `tsc --noEmit` in `packages/plugin-grid` gave exit **2**, `TS2741: Property 'ariaLabel' is missing in type '{ active: boolean; }' but required in type 'RefreshIndicatorProps'.` It was restored hash-equal. ## Gates Pre-merge head `b240863`: - `pnpm exec vitest run --maxWorkers=2 packages/plugin-grid/`: exit 0, `157 passed (157)` files, `1489 passed` tests. - `… packages/plugin-list/ packages/plugin-charts/`: exit 0, `172 passed (172)`, `1982 passed`. - `… packages/plugin-dashboard/ packages/i18n/`: exit 0, `201 passed (201)`, `2424 passed`. - `… packages/components/src/custom/` plus the components pin plus `ObjectGantt.refreshInPlace-7237.test.tsx`: exit 0, `2 passed (2)`, `11 passed`. `src/custom/` holds no test file. Merged head `641ed9f`, after the merge of `origin/main` `6516320`, which moved all ten packs and `packages/components`: - `pnpm exec vitest run --maxWorkers=2 packages/i18n/` plus the 5 pins plus the gantt pin: exit 0, `77 passed (77)`, `1197 passed`. - `pnpm turbo run type-check --filter @object-ui/components --filter @object-ui/i18n --filter @object-ui/plugin-grid --filter @object-ui/plugin-list --filter @object-ui/plugin-charts --filter @object-ui/plugin-dashboard --filter @object-ui/plugin-gantt --concurrency=2`: exit 0, `22 successful, 22 total`, with all 7 type-check tasks executed (cache miss). - `pnpm check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys` (no `refreshing` candidate), `check:control-bytes`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape`, `check:new-line-citations`, `check:changeset-claims`, `check:pending-changeset-literals`, `changeset:check`, `node scripts/check-changeset-presence.mjs`: all exit 0. - `pnpm exec eslint` over the 20 changed `.ts`/`.tsx` files: exit 0, 0 errors. **Lint, narrowed and declared.** The repo-wide `pnpm lint` was not run locally; CI owns it. Evidence that the narrowing excludes nothing: (1) `eslint.config.js` applies to `**/*.{ts,tsx}`, and every one of this diff's 20 `.ts`/`.tsx` files is in that population; (2) `--format json` reports 20 files linted, 0 errors; (3) the config has no `parserOptions`, `projectService` or `project:` (0 hits) and no import plugin (0 hits), so linting is not type-aware and this diff cannot move the verdict of any file it does not touch. - Round 2: the branch merged `origin/main` `4a3d500` as `1a89050` (merge only; this PR's own diff is unchanged at 21 files, +543 / −8). There, the `plugin-list` suite plus the five pins and the `ObjectGantt` 7237 pin (104 files, 1166 tests), and the seven type-checks, re-ran green. The merge-queue eviction at 13:55Z was the objectui#10645 test, which is not this PR's (os-dev-report round 2, comment `5834284916`). - Round 3: merged `origin/main` `7baede3` (it carries PR objectui#10677, the change for objectui#10645) at `a6512a7`, with no conflicts; the five pins, the `ObjectGantt` 7237 pin and the seven type-checks re-ran green there (os-dev-report round 3, comment `5836351479`). ## Acceptance notes - **Hosts with no i18next instance.** A throwaway probe, not committed, rendered `ObjectChart` with no `I18nProvider`: the bar is named `chart.refreshing`, the key itself. `ObjectDataTable` reads its name the same way (`useObjectTranslation().t`) and was not measured separately. Grid, list and gantt still resolve English there through their own `createSafeTranslation` defaults maps. This is what "no English fallback" means at the two new call sites. The console mounts `I18nProvider` in `apps/console/src/main.tsx`, so it does not reach the console. The changeset says so. - `RefreshIndicator` spreads `...props` after `aria-label`, so a caller's own `aria-label` attribute still overrides `ariaLabel`. That was true before this PR and is unchanged. - The comment on the `chart` block in `en.ts` still describes the pack as budgeted inside the `framework` chunk. `scripts/check-eager-closure-budget.mjs` now gives the pack its own `i18n-locale-en` ceiling. The comment has drifted and is not touched here. - In `de`, `grid.refreshing` reads `Wird aktualisiert…` while `list.refreshing` reads `Aktualisiere…`. The two new keys use the former. This is polish and is not touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d435e96 commit 9fbbb17

21 files changed

Lines changed: 543 additions & 8 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/components': minor
3+
'@object-ui/plugin-grid': patch
4+
'@object-ui/plugin-list': patch
5+
'@object-ui/plugin-charts': patch
6+
'@object-ui/plugin-dashboard': patch
7+
'@object-ui/i18n': patch
8+
---
9+
10+
BREAKING (`@object-ui/components`): `RefreshIndicator`'s `ariaLabel` prop is now required and has no default. It used to default to the English literal "Refreshing", so the progress bar on every view that passed no name was announced in English to screen-reader users in every locale (objectui#10580). A `RefreshIndicator` rendered without `ariaLabel` now fails to type-check.
11+
12+
(The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)
13+
14+
Migration: pass the bar's accessible name from your own translation layer, for example `ariaLabel={t('grid.refreshing')}`. The component renders the string you pass as the bar's `aria-label` and does not translate it.
15+
16+
`ObjectGrid`, `ListView`, `ObjectChart` and `ObjectDataTable` now name their refresh bar in the active locale. The grid and the list read the `grid.refreshing` and `list.refreshing` keys their pull-to-refresh text already uses. The chart reads `chart.refreshing` and the dashboard data table reads `dashboard.refreshing`, both with no English literal behind them, so a host that renders either one with no i18next instance at all gets the key itself as the bar's name.
17+
18+
`@object-ui/i18n`: new keys `chart.refreshing` and `dashboard.refreshing` in every built-in locale pack.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* `RefreshIndicator` names its progress bar only with what the caller passes
11+
* (objectui#10580).
12+
*
13+
* `ariaLabel` used to be optional with the English default "Refreshing", so
14+
* four data views that passed nothing announced English to every non-English
15+
* screen-reader user. The prop is now required and the default is gone: a
16+
* caller that names nothing is a compile error, not an English name.
17+
*
18+
* Two halves:
19+
* - TYPE: erased at runtime, so vitest proves nothing about it. It is
20+
* checked by `tsc -p tsconfig.test.json`, which this package's
21+
* `type-check` script chains.
22+
* - DOM: the rendered bar carries exactly the passed name, and a caller that
23+
* slips past the type (a cast) gets no English name either.
24+
*/
25+
26+
import { describe, it, expect } from 'vitest';
27+
import * as React from 'react';
28+
import { render, screen } from '@testing-library/react';
29+
import { RefreshIndicator, type RefreshIndicatorProps } from '../custom/refresh-indicator';
30+
31+
describe('RefreshIndicator takes its accessible name from the caller (objectui#10580)', () => {
32+
it('a caller that passes no name does not compile', () => {
33+
// Built, never rendered: these two elements are here for `tsc` to judge.
34+
const accepted = <RefreshIndicator active ariaLabel="Actualizando…" />;
35+
const refused = (
36+
// @ts-expect-error `ariaLabel` is required — with an optional prop this directive goes unused (TS2578)
37+
<RefreshIndicator active />
38+
);
39+
expect(React.isValidElement(accepted) && React.isValidElement(refused)).toBe(true);
40+
});
41+
42+
it('names the progress bar with the passed string', () => {
43+
render(<RefreshIndicator active ariaLabel="Wird aktualisiert…" />);
44+
const bar = screen.getByRole('progressbar', { name: 'Wird aktualisiert…' });
45+
expect(bar.getAttribute('aria-busy')).toBe('true');
46+
});
47+
48+
it('renders nothing while inactive', () => {
49+
render(<RefreshIndicator active={false} ariaLabel="Wird aktualisiert…" />);
50+
expect(screen.queryByTestId('refresh-indicator')).toBeNull();
51+
});
52+
53+
it('has no English name to fall back to when a caller casts past the type', () => {
54+
const props = { active: true } as unknown as RefreshIndicatorProps;
55+
render(<RefreshIndicator {...props} />);
56+
const bar = screen.getByTestId('refresh-indicator');
57+
expect(bar.hasAttribute('aria-label')).toBe(false);
58+
});
59+
});

‎packages/components/src/custom/refresh-indicator.tsx‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ function ensureKeyframe() {
3535
export interface RefreshIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
3636
/** Whether the indicator is active. When false, nothing is rendered. */
3737
active: boolean
38-
/** Accessible label. Defaults to "Refreshing". */
39-
ariaLabel?: string
38+
/**
39+
* Accessible name of the progress bar, rendered as its `aria-label`.
40+
* Required, with no default: the caller passes a string from its own
41+
* translation layer, so the name a screen reader announces is in the active
42+
* locale. An English default here reached every non-English screen-reader
43+
* user through the callers that passed nothing (objectui#10580).
44+
*/
45+
ariaLabel: string
4046
}
4147

4248
/**
@@ -50,7 +56,7 @@ export interface RefreshIndicatorProps extends React.HTMLAttributes<HTMLDivEleme
5056
*/
5157
export const RefreshIndicator: React.FC<RefreshIndicatorProps> = ({
5258
active,
53-
ariaLabel = "Refreshing",
59+
ariaLabel,
5460
className,
5561
...props
5662
}) => {

‎packages/i18n/src/locales/ar.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,7 @@ const ar = {
11491149
},
11501150
chart: {
11511151
loading: "جارٍ تحميل الرسم البياني…",
1152+
refreshing: "جارٍ التحديث…",
11521153
nullCategory: "(غير محدد)",
11531154
scatterOneMeasure: "المخطط المبعثر يرسم مقياسًا واحدًا فقط. أبقِ سلسلة واحدة:",
11541155
unconfigured: {
@@ -1170,6 +1171,7 @@ const ar = {
11701171
sourceLabel: "مصدر البيانات:",
11711172
},
11721173
loading: "جارٍ التحميل…",
1174+
refreshing: "جارٍ التحديث…",
11731175
pickMeasures: "اختر المقاييس (القيم) لأداة مجموعة البيانات هذه.",
11741176
datasetUnsupported: "مصدر البيانات هذا لا يدعم استعلامات مجموعات البيانات.",
11751177
details: "التفاصيل",

‎packages/i18n/src/locales/de.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,7 @@ const de = {
11291129
},
11301130
chart: {
11311131
loading: "Diagramm wird geladen…",
1132+
refreshing: "Wird aktualisiert…",
11321133
nullCategory: "(Ohne Angabe)",
11331134
scatterOneMeasure: "Ein Streudiagramm zeichnet genau eine Kennzahl. Behalten Sie nur eine Datenreihe:",
11341135
unconfigured: {
@@ -1150,6 +1151,7 @@ const de = {
11501151
sourceLabel: "Datenquelle:",
11511152
},
11521153
loading: "Wird geladen…",
1154+
refreshing: "Wird aktualisiert…",
11531155
pickMeasures: "Wählen Sie Kennzahlen (Werte) für dieses Dataset-Widget.",
11541156
datasetUnsupported: "Diese Datenquelle unterstützt keine Dataset-Abfragen.",
11551157
details: "Details",

‎packages/i18n/src/locales/en.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1337,6 +1337,7 @@ const en = {
13371337
},
13381338
chart: {
13391339
loading: 'Loading chart…',
1340+
refreshing: 'Refreshing…',
13401341
nullCategory: '(None)',
13411342
// The refusal a scatter renders when handed more than one series
13421343
// (objectui#7194): it binds ONE measure, so a second series was painted at
@@ -1421,6 +1422,7 @@ const en = {
14211422
sourceLabel: 'Source:',
14221423
},
14231424
loading: 'Loading…',
1425+
refreshing: 'Refreshing…',
14241426
pickMeasures: 'Pick measures (values) for this dataset widget.',
14251427
datasetUnsupported: 'This data source does not support dataset queries.',
14261428
details: 'Details',

‎packages/i18n/src/locales/es.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,7 @@ const es = {
11331133
},
11341134
chart: {
11351135
loading: "Cargando gráfico…",
1136+
refreshing: "Actualizando…",
11361137
nullCategory: "(Sin especificar)",
11371138
scatterOneMeasure: "Un gráfico de dispersión traza una sola medida. Conserve una sola serie:",
11381139
unconfigured: {
@@ -1154,6 +1155,7 @@ const es = {
11541155
sourceLabel: "Origen de datos:",
11551156
},
11561157
loading: "Cargando…",
1158+
refreshing: "Actualizando…",
11571159
pickMeasures: "Elija medidas (valores) para este widget de dataset.",
11581160
datasetUnsupported: "Esta fuente de datos no admite consultas de dataset.",
11591161
details: "Detalles",

‎packages/i18n/src/locales/fr.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,7 @@ const fr = {
11311131
},
11321132
chart: {
11331133
loading: "Chargement du graphique…",
1134+
refreshing: "Actualisation…",
11341135
nullCategory: "(Non défini)",
11351136
scatterOneMeasure: "Un nuage de points trace une seule mesure. Ne conservez qu’une série :",
11361137
unconfigured: {
@@ -1152,6 +1153,7 @@ const fr = {
11521153
sourceLabel: "Source de données :",
11531154
},
11541155
loading: "Chargement…",
1156+
refreshing: "Actualisation…",
11551157
pickMeasures: "Choisissez des mesures (valeurs) pour ce widget de dataset.",
11561158
datasetUnsupported: "Cette source de données ne prend pas en charge les requêtes de dataset.",
11571159
details: "Détails",

‎packages/i18n/src/locales/ja.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,7 @@ const ja = {
11291129
},
11301130
chart: {
11311131
loading: "チャート読み込み中…",
1132+
refreshing: "更新中…",
11321133
nullCategory: "(未設定)",
11331134
scatterOneMeasure: "散布図は1つの指標だけを描画します。系列は1つだけ残してください:",
11341135
unconfigured: {
@@ -1150,6 +1151,7 @@ const ja = {
11501151
sourceLabel: "データソース:",
11511152
},
11521153
loading: "読み込み中…",
1154+
refreshing: "更新中…",
11531155
pickMeasures: "このデータセットウィジェットの指標(値)を選択してください。",
11541156
datasetUnsupported: "このデータソースはデータセットクエリに対応していません。",
11551157
details: "詳細",

‎packages/i18n/src/locales/ko.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,7 @@ const ko = {
11291129
},
11301130
chart: {
11311131
loading: "차트 로딩 중…",
1132+
refreshing: "새로고침 중…",
11321133
nullCategory: "(미지정)",
11331134
scatterOneMeasure: "산점도는 측정값 하나만 그립니다. 계열을 하나만 남기세요:",
11341135
unconfigured: {
@@ -1150,6 +1151,7 @@ const ko = {
11501151
sourceLabel: "데이터 소스:",
11511152
},
11521153
loading: "로딩 중…",
1154+
refreshing: "새로고침 중…",
11531155
pickMeasures: "이 데이터셋 위젯의 측정값(값)을 선택하세요.",
11541156
datasetUnsupported: "이 데이터 소스는 데이터셋 쿼리를 지원하지 않습니다.",
11551157
details: "세부 정보",

0 commit comments

Comments
 (0)