Skip to content

Commit 7a2818c

Browse files
fix(app-shell): name Studio's group-title, Description, Help text and nav-item inputs (#26926)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbXRpscK3dszbSuQtCmGe4R8)_ Fixes #26732 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:Studio 里五个输入框以前读屏软件读不出名字:表单设计器里分组标题的行内输入框、字段属性面板的「描述」和「帮助文本」、界面导航项属性面板的「链接到对象」下拉和「标签」输入框。现在每个都有可读的名字,点击标签也会聚焦到对应输入框。外观和保存内容都不变。 - **为什么改**:QA 读无障碍树时发现这几处是无名编辑框(studio-qa-2026-10-27,U7),自动化无障碍检查也会报错。 - **风险与回滚**:很低。只加了 `id`/`htmlFor` 关联和一个 `aria-label`,没有新文案键,不动元数据和 spec。一个旧测试钉住了「对象下拉没有名字」,本 PR 把它改为钉住新名字。回滚即 revert。 - **先看**:`StudioDesignSurface.tsx` 里 `StudioNavItemInspector` 的两处 `htmlFor`;`ObjectFieldInspector.tsx` 的 `TextareaField`;`StudioDesignSurface.sharedSelect-11865.test.tsx` 改掉的那条断言。 - **两分钟验证**:在 Studio 打开一个应用的导航项,点「链接到对象」这几个字,预期下拉获得焦点;或在浏览器无障碍面板里看到该下拉名为 "Link to object"。 - **需要你做的**:无,席位落地。 ## What changes - **Form designer group title** (`ObjectFormDesigner.tsx`): a declared group's inline title input element had no label, `aria-label` or `id`. No visible label sits beside it, so it takes `aria-label` from the label the group inspector already gives the same value, `engine.studio.designer.group.nameLabel` ("Group name" / 「分组名称」). No new key. - **Field inspector Description and Help text** (`ObjectFieldInspector.tsx`): the file-local `TextareaField` drew its `Label` beside the textarea element without closing the pair. It now mints a per-instance `React.useId()` and sets `htmlFor` ⇄ `id`, the way `_shared.tsx`'s form atoms (`InspectorTextField`, `InspectorNumberField`) close theirs. Every `TextareaField` in the inspector gets its name from this one change. - **Interfaces nav-item inspector** (`StudioDesignSurface.tsx`, `StudioNavItemInspector` only): the *Link to object* label and the *Label* label were drawn above their controls without `htmlFor`. Both now close the pair with a `useId()`, as the same inspector's target, URL, *Open in* and icon fields already did. The object picker is the shared `StudioPicker`, which already forwards `id` to its Radix trigger, so no component changed; its header comment now says the object picker is named like the others. ## Tests - `ObjectFieldInspector.textareaNames-26732.test.tsx`: Description and Help text found by `getByRole('textbox', { name })` and `getByLabelText` in en; a zh case awaits `loadStudioCatalogue('zh')` at module scope (the #26800 pattern) and finds both by 「描述」 / 「帮助文本」. - `ObjectFormDesigner.groupTitleName-26732.test.tsx`: the group-title input found by `getByRole('textbox', { name: 'Group name' })`, holding the group's label. - `StudioDesignSurface.navItemInspectorNames-26732.test.tsx`: the object picker found by `getByRole('combobox', { name: 'Link to object' })` (and it is the `nav-link-object` trigger), the Label input by `getByRole('textbox', { name: 'Label' })`. - `StudioDesignSurface.sharedSelect-11865.test.tsx`: its pin "the object picker had none" now reads the object picker's name as `Link to object`. - Ablation (one-off, not kept), each with the fix committed, through `node scripts/ablation-replace.mjs` (anchor 1 → 0, restored blob equal to HEAD each time): dropping the group input's `aria-label` reds its test (1 failed of 1); dropping `htmlFor={id}` from `TextareaField` reds both textarea cases (2 failed of 2); dropping the object picker's `id` reds the nav test, and so does dropping the Label input's `id` (1 failed of 1 each). `git diff HEAD` empty after all four. ## Acceptance notes - **Sweep of the three files for the same hole.** `ObjectFormDesigner.tsx` has no other unnamed input (its icon buttons all carry `aria-label`). In `ObjectFieldInspector.tsx`, every control other than `TextareaField` is a `_shared.tsx` atom that already closes its pair, an `InspectorComboField` with a label or `ariaLabel`, or a checkbox inside its label element; the picklist option rows' value and label inputs are named by their placeholders ("Value", "Label") and the color swatch by its `title`, so they are not unnamed and are left as they are. In the nav-item inspector, the type radios sit inside their labels and the remaining fields were already tied. - **A pinned behaviour is reversed on purpose.** `StudioDesignSurface.sharedSelect-11865` pinned that the object picker kept the native control's lack of a name; this card asks for that name, so the pin now reads it. ## Verification All readings at head `67406db6` (base `2720874a`), under `scripts/pm/os-verify-lock.sh` with `--maxWorkers=1`. <details><summary>Touched package: @object-ui/app-shell</summary> - Dependency closure: `pnpm exec turbo run build --concurrency=2 --filter='@object-ui/app-shell^...'`, 63 tasks, VERDICT command-exit 0. Then `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit` and `tsc -p tsconfig.test.json`, so the tests too): VERDICT command-exit 0. - The four new or edited test files: Test Files 3 passed (3), Tests 4 passed (4); `StudioDesignSurface.sharedSelect-11865`: Tests 79 passed (79). - Narrowed app-shell run: every test file naming `ObjectFormDesigner`, `ObjectFieldInspector`, `StudioNavItemInspector`, `nav-link-object` or `InterfacesPillar` (89 files). First run at `a3764046`: 1 failed, 892 passed, the `sharedSelect-11865` pin of the unnamed object picker, updated in `67406db6` and green after. The rest of app-shell is left to CI's Test UI shards. - ESLint with `ui/eslint.config.js` on the seven touched files: 0 errors, and no warning on a changed line. </details> <details><summary>Derived gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 34 commands for the 8 changed paths (same list at both heads); all 34 exit 0, each code captured before any pipe, among them `check:nul-bytes`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:type-check-coverage`, `check:type-check-debt`, `check-sdui-manifest`, `check-empty-changeset` and `check-changeset-no-major`. `pnpm --dir ui exec vitest run scripts/`: Test Files 178 passed, 2 skipped (180), Tests 5065 passed, 2 skipped. - `--ran` reconciled: "34 derived famil(ies) accounted for — 34 run, 0 NOT-MEASURED". - PR-body gates on this body: `check-changeset-no-major`, `check-adr-0087-registration`, `check-closing-keyword-parity`, `check-partof-closing-keyword`, all exit 0. `pnpm --dir ui run check:new-line-citations`: 0 new citations. - Families CI runs over the whole tree (type-check lanes, lint, artifact rosters) are CI's; the PR's check-runs are the reading. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (lint is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jT6kwaZ4RiKsDR3s5f --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jT6kwaZ4RiKsDR3s5f)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8bbdb0b commit 7a2818c

8 files changed

Lines changed: 219 additions & 11 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): Studio's group-title, Description, Help text, nav "Link to object" and nav "Label" inputs have accessible names
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Five Studio inputs were anonymous to screen readers and automated accessibility checks. The form designer's inline group-title input is now named "Group name", the label the group inspector gives the same value. The field inspector's Description and Help text textareas, and the Interfaces nav-item inspector's "Link to object" picker and "Label" input, are now tied to the labels already drawn above them, so each is announced by its label and clicking the label focuses it. Nothing changes visually and nothing changes in what is saved.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26732 — the field inspector's Description and Help text
5+
* textareas are named by their visible labels.
6+
*
7+
* Before: the local `TextareaField` drew its `<Label>` beside the textarea
8+
* without closing the pair, so assistive tech read two anonymous edit fields
9+
* and clicking a label focused nothing. It now closes the pair the way
10+
* `_shared.tsx`'s form atoms do (`htmlFor` ⇄ a per-instance `useId()`).
11+
*/
12+
13+
import '@testing-library/jest-dom/vitest';
14+
import * as React from 'react';
15+
import { afterEach, describe, expect, it, vi } from 'vitest';
16+
import { cleanup, render, screen } from '@testing-library/react';
17+
18+
vi.mock('../useMetadata', () => ({
19+
useMetadataClient: () => ({
20+
withPreviewDrafts: () => ({ list: vi.fn().mockResolvedValue([]) }),
21+
}),
22+
}));
23+
24+
vi.mock('../previews/useObjectFields', () => ({
25+
useObjectFields: () => ({ fields: [], loading: false, error: null }),
26+
}));
27+
28+
import { ObjectFieldInspector } from './ObjectFieldInspector';
29+
import { t, type SupportedLocale } from '../i18n';
30+
// `designer.field.description` / `designer.field.helpText` live in
31+
// `@object-ui/i18n`'s `studio` namespace, whose `zh` catalogue is fetched on
32+
// demand. Resident before any case, so the zh names are read after it lands.
33+
import { loadStudioCatalogue } from '@object-ui/i18n/namespaces/studio';
34+
await loadStudioCatalogue('zh');
35+
36+
afterEach(cleanup);
37+
38+
function renderTextField(locale: SupportedLocale) {
39+
render(
40+
<ObjectFieldInspector
41+
type="object"
42+
name="account"
43+
draft={{
44+
name: 'account',
45+
fields: {
46+
notes: { type: 'text', label: 'Notes', description: 'What the note is for', inlineHelpText: 'Shown under the field' },
47+
},
48+
}}
49+
selection={{ kind: 'field', id: 'notes' }}
50+
onPatch={vi.fn()}
51+
onClearSelection={vi.fn()}
52+
onSelectionChange={vi.fn()}
53+
readOnly={false}
54+
locale={locale}
55+
/>,
56+
);
57+
}
58+
59+
describe('objectstack#26732 — the field inspector names its Description and Help text textareas', () => {
60+
it('en: each textarea is found by its label and holds its own key', () => {
61+
renderTextField('en-US');
62+
const description = screen.getByRole('textbox', { name: t('designer.field.description', 'en-US') });
63+
const helpText = screen.getByRole('textbox', { name: t('designer.field.helpText', 'en-US') });
64+
expect(description.tagName).toBe('TEXTAREA');
65+
expect(description).toHaveValue('What the note is for');
66+
expect(helpText.tagName).toBe('TEXTAREA');
67+
expect(helpText).toHaveValue('Shown under the field');
68+
expect(screen.getByLabelText(t('designer.field.description', 'en-US'))).toBe(description);
69+
});
70+
71+
it('zh: the same textareas carry the Chinese labels as their names', () => {
72+
renderTextField('zh-CN');
73+
const name = t('designer.field.description', 'zh-CN');
74+
// The zh catalogue is resident, so this is the Chinese label, not the en fallback.
75+
expect(name).toBe('描述');
76+
expect(screen.getByRole('textbox', { name })).toHaveValue('What the note is for');
77+
expect(screen.getByRole('textbox', { name: t('designer.field.helpText', 'zh-CN') })).toHaveValue(
78+
'Shown under the field',
79+
);
80+
});
81+
});

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1516,10 +1516,14 @@ function TextareaField({
15161516
mono?: boolean;
15171517
placeholder?: string;
15181518
}) {
1519+
// The pair is closed the way `_shared.tsx`'s form atoms close theirs: an id
1520+
// minted per instance, so the label names the textarea.
1521+
const id = React.useId();
15191522
return (
15201523
<div className="space-y-1">
1521-
<Label className="text-xs text-muted-foreground">{label}</Label>
1524+
<Label htmlFor={id} className="text-xs text-muted-foreground">{label}</Label>
15221525
<textarea
1526+
id={id}
15231527
value={value}
15241528
disabled={disabled}
15251529
rows={rows}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26732 — the form designer's inline group-title input has an
5+
* accessible name.
6+
*
7+
* Before: a declared group's header held a bare `<input>` with no label, no
8+
* `aria-label` and no `id`, so a screen reader announced an unnamed edit field.
9+
* There is no visible label beside it, so it names itself with the label the
10+
* group inspector gives the same value (`engine.studio.designer.group.nameLabel`).
11+
*/
12+
13+
import '@testing-library/jest-dom/vitest';
14+
import { afterEach, describe, expect, it, vi } from 'vitest';
15+
import { cleanup, render, screen } from '@testing-library/react';
16+
import { ObjectFormDesigner } from './ObjectFormDesigner';
17+
import { t } from '../metadata-admin/i18n';
18+
19+
afterEach(cleanup);
20+
21+
const draft = {
22+
name: 'crm_account',
23+
fields: [{ name: 'name', type: 'text', label: 'Account Name', group: 'basic' }],
24+
fieldGroups: [{ key: 'basic', label: 'Basic Information' }],
25+
};
26+
27+
describe('objectstack#26732 — the form designer names its group-title input', () => {
28+
it("a declared group's title input is found by its name and holds the group's label", () => {
29+
render(
30+
<ObjectFormDesigner
31+
draft={draft}
32+
systemFieldNames={new Set()}
33+
onChange={vi.fn()}
34+
onSelectField={() => {}}
35+
onSelectGroup={() => {}}
36+
/>,
37+
);
38+
const input = screen.getByRole('textbox', { name: t('engine.studio.designer.group.nameLabel', 'en-US') });
39+
expect(input).toHaveValue('Basic Information');
40+
});
41+
});

‎ui/packages/app-shell/src/views/studio-design/ObjectFormDesigner.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,9 @@ function Section({
481481
<div className="flex h-11 items-center gap-2 border-b px-4">
482482
{isDeclared && !readOnly ? (
483483
<input
484+
// No visible label sits beside the inline title: the input names
485+
// itself with the group inspector's own name-field label.
486+
aria-label={t('engine.studio.designer.group.nameLabel', locale)}
484487
defaultValue={title}
485488
onBlur={(e) => e.target.value.trim() && e.target.value !== title && onRename(e.target.value)}
486489
onKeyDown={(e) => {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectstack#26732 — the Interfaces nav-item inspector's *Link to object*
5+
* picker and *Label* input are named by their visible labels.
6+
*
7+
* Before: both labels were drawn above their controls without `htmlFor`, so
8+
* the picker's trigger and the text input were anonymous to assistive tech.
9+
* Each now closes the pair with a `useId()`, as the inspector's target, URL
10+
* and icon fields already did.
11+
*/
12+
import '@testing-library/jest-dom/vitest';
13+
import * as React from 'react';
14+
import { afterEach, describe, expect, it, vi } from 'vitest';
15+
import { render, screen, cleanup } from '@testing-library/react';
16+
import { MetadataCtx } from '@object-ui/react';
17+
18+
import { StudioNavItemInspector } from './StudioDesignSurface';
19+
import { t } from '../metadata-admin/i18n';
20+
21+
afterEach(cleanup);
22+
23+
const METADATA = {
24+
apps: [],
25+
objects: [{ name: 'account', label: 'Customers' }],
26+
dashboards: [],
27+
reports: [],
28+
pages: [],
29+
loading: false,
30+
error: null,
31+
refresh: async () => {},
32+
invalidate: () => {},
33+
ensureType: async () => [],
34+
getItem: async () => null,
35+
getItemsByType: () => [],
36+
getTypeStatus: () => 'ready' as const,
37+
};
38+
39+
describe('objectstack#26732 — the nav-item inspector names its object picker and label input', () => {
40+
it('an object entry: the picker and the Label field are found by their labels', () => {
41+
render(
42+
<MetadataCtx.Provider value={METADATA as never}>
43+
<StudioNavItemInspector
44+
navId="navigation[0]"
45+
appDraft={{ navigation: [{ id: 'nav_account', type: 'object', objectName: 'account', label: 'Accounts' }] }}
46+
objects={[{ name: 'account', label: 'Customers' }]}
47+
packageId="com.acme.app"
48+
onNavPatch={vi.fn()}
49+
onClear={vi.fn()}
50+
/>
51+
</MetadataCtx.Provider>,
52+
);
53+
expect(screen.getByRole('combobox', { name: t('engine.studio.nav.linkObject', 'en-US') })).toBe(
54+
screen.getByTestId('nav-link-object'),
55+
);
56+
expect(screen.getByRole('textbox', { name: t('engine.studio.nav.label', 'en-US') })).toHaveValue('Accounts');
57+
});
58+
});

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.sharedSelect-11865.test.tsx‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* What is pinned:
1515
* - each picker IS the primitive (a Radix combobox trigger), shows what the
1616
* entry or the dialog holds, and no native select is left;
17-
* - each picker keeps the accessible name the native control had (the
18-
* object picker had none, and has none);
17+
* - each picker keeps the accessible name the native control had, and the
18+
* object picker, which had none, is named by its *Link to object* label
19+
* (objectstack#26732);
1920
* - every option of every picker writes what the native control wrote:
2021
* the inspector's `onNavPatch` value as JSON text plus the keys it holds
2122
* as `undefined` (JSON erases those), the *New object* save and the
@@ -158,7 +159,7 @@ afterEach(() => {
158159
vi.unstubAllGlobals();
159160
});
160161

161-
/** The object picker's handle: it has no accessible name, as the native control had none. */
162+
/** The object picker's handle, kept from when it had no accessible name (the native control had none). */
162163
const OBJECT_PICKER = 'nav-link-object';
163164
const CHOOSE_OBJECT = en('engine.studio.nav.chooseObject');
164165
const CHOOSE = en('engine.inspector.appNav.choose');
@@ -337,21 +338,22 @@ describe('the nav-item inspector picks with the shared Select (objectui#11865)',
337338
});
338339

339340
// The names were read on the native controls; red there only because the object picker is found by its test id.
340-
it('each picker keeps the name its label gave the native control; the object picker had none', async () => {
341+
// The object picker had none there; objectstack#26732 names it by its *Link to object* label.
342+
it('each picker keeps the name its label gave the native control; the object picker is named by its label', async () => {
341343
const names: Record<string, string[]> = {};
342344
for (const key of ['objBorn', 'pageBorn', 'dashBound', 'reportBorn', 'actionBorn', 'compBorn', 'docBorn', 'urlDefault']) {
343345
renderEntry(key);
344346
names[key] = screen.getAllByRole('combobox').map((c) => {
345347
// objectui#12455 — the inspector's Icon picker is a combobox named by its label too.
346-
const named = ['Page', 'Dashboard', 'Report', 'Action', 'Component', 'Doc page', 'Book', 'Open in', 'Icon'].find(
348+
const named = ['Link to object', 'Page', 'Dashboard', 'Report', 'Action', 'Component', 'Doc page', 'Book', 'Open in', 'Icon'].find(
347349
(n) => screen.queryByRole('combobox', { name: n }) === c,
348350
);
349351
return named ?? '';
350352
});
351353
cleanup();
352354
}
353355
expect(names).toEqual({
354-
objBorn: ['', 'Icon'],
356+
objBorn: ['Link to object', 'Icon'],
355357
pageBorn: ['Page', 'Icon'],
356358
dashBound: ['Dashboard', 'Icon'],
357359
reportBorn: ['Report', 'Icon'],
@@ -361,7 +363,7 @@ describe('the nav-item inspector picks with the shared Select (objectui#11865)',
361363
urlDefault: ['Open in', 'Icon'],
362364
});
363365
renderEntry('objBorn');
364-
expect(screen.getByTestId(OBJECT_PICKER)).not.toHaveAccessibleName();
366+
expect(screen.getByTestId(OBJECT_PICKER)).toHaveAccessibleName('Link to object');
365367
});
366368

367369
it('each picker lists its options in the order the native control did', async () => {

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,8 @@ export function StudioNavItemInspector({
26312631
const targetLabel = useNavTargetLabel();
26322632
const typeGroupName = React.useId();
26332633
const iconFieldId = React.useId();
2634+
const objectFieldId = React.useId();
2635+
const labelFieldId = React.useId();
26342636
const idx = React.useMemo(() => {
26352637
const m = /^navigation\[(\d+)\]$/.exec(navId);
26362638
return m ? Number(m[1]) : -1;
@@ -2749,8 +2751,11 @@ export function StudioNavItemInspector({
27492751
: inheritedNavEntryText(node as NavEntryLike, targetLabel);
27502752
const labelField = (
27512753
<div>
2752-
<label className="mb-1 block text-[11px] font-medium text-muted-foreground">{t('engine.studio.nav.label', locale)}</label>
2754+
<label htmlFor={labelFieldId} className="mb-1 block text-[11px] font-medium text-muted-foreground">
2755+
{t('engine.studio.nav.label', locale)}
2756+
</label>
27532757
<input
2758+
id={labelFieldId}
27542759
value={navItemLabelText(label, locale)}
27552760
onChange={(e) => editLabel(e.target.value)}
27562761
placeholder={placeholderText}
@@ -2789,8 +2794,11 @@ export function StudioNavItemInspector({
27892794
</fieldset>
27902795
{(kind === 'object' || kind === null) && (
27912796
<div>
2792-
<label className="mb-1 block text-[11px] font-medium text-muted-foreground">{t('engine.studio.nav.linkObject', locale)}</label>
2797+
<label htmlFor={objectFieldId} className="mb-1 block text-[11px] font-medium text-muted-foreground">
2798+
{t('engine.studio.nav.linkObject', locale)}
2799+
</label>
27932800
<StudioPicker
2801+
id={objectFieldId}
27942802
value={boundObject}
27952803
options={[
27962804
{ value: '', label: t('engine.studio.nav.chooseObject', locale) },
@@ -3061,7 +3069,8 @@ const PICKER_XS = 'h-auto rounded px-2 py-1 text-xs';
30613069
* opens there.
30623070
* - Each caller keeps the name its native control had: a `<label htmlFor>`
30633071
* names the trigger by `id`, and a wrapping `<label>` names it as it named
3064-
* the native control. The object picker had none and still has none.
3072+
* the native control. The nav-item inspector's object picker is named by
3073+
* its *Link to object* label the same way.
30653074
*/
30663075
function StudioPicker({
30673076
id,

0 commit comments

Comments
 (0)