microsoft / microsoft/fluentui-react-native
Components: Accessibility conformance suite
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 179
- Avg merge
- 16h 17m
- Merged PRs (30d)
- 30
Description
Summary
Define and enforce a data-driven, package-level accessibility conformance suite for
packages/agentic/components that asserts static native accessibility output - role,
accessible name, state, disabled semantics, and any required-prop validation - uniformly
across every exported higher-order component. The suite runs inside the package's existing
Jest pass (macOS preset) with @testing-library/react-native, produces a machine-readable
per-component conformance record, and is wired into the package's declared test script so
it gates the coverage contract from
component-test-strategy.md.
Observed. Accessibility assertions already exist in most per-component test files, but
their depth, shape, and completeness vary. Seven of eighteen components do not assert the
static role in their default case. Three components assert partial state (for example,
checked without disabled). Two components warn on missing required props but the
warning assertion is isolated in a single test case with no cross-component pattern. No file
today asserts that every exported component satisfies a shared minimum accessibility
contract; the coverage is individual, not systemic.
This task defines a shared conformance contract, populates a registry of per-component
applicability and exceptions, closes the gaps in the existing per-component tests, and
adds a structural gate that prevents a newly generated component from shipping without
declaring its conformance entry. It does not build the real-platform screen-reader pipeline
(that is
research/infrastructure/tasks/storybook-e2e.md)
and does not add manual screen-reader validation steps.
Goal
Deliver a shared, data-driven conformance suite that asserts static native accessibility
output over every exported higher-order component in packages/agentic/components, backed
by an applicability registry that makes conformance decisions explicit and machine-checkable,
and gates new components through an onboarding requirement before they join the export
surface.
Stage
Stage 1 (beta foundations). The readiness model
(readiness-model.md) requires asserted accessibility semantics
as evidence for the preview tier. Without a uniform contract those assertions are
self-reported and unverifiable at scale.
Why it matters
Observed. The preview level in
readiness-model.md requires "asserted accessibility
semantics" as one of its four evidence axes, alongside runtime coverage, type test, and
spec provenance. That requirement today has no uniform definition: "asserted" means
whatever the component author happened to write.
Observed. Existing test coverage across the eighteen components shows three distinct
accessibility patterns:
- Full contract:
checkbox,radio,switch,tabassert role, label, and complete
accessibilityStatein one default case, then drive state change assertions from there. - Partial contract:
button,list-item,listbox-item,tagassertaccessibilityState
but not role in the default case; role is either absent or implicit in agetByRolequery. - Thin contract:
avatar,badge,divider,skeletonassert an accessible name but no
state because their role carries no stateful axis.progress-barandspinnerassert role
and state but omit the base disabled assertion.accordionandcardassert role and
expanded or selected state in isolation without a unified default case. - Primitives:
iconassertsaccessibilityRoleand role-conditional behavior.
checkbox-indicatorandfocus-visualassertaccessible: falsevia
hiddenFromAccessibilityProps.compound-item-layoutandlayout-stable-textcarry no
direct accessibility assertions.
Inferred. Because the gaps are structural rather than per-component, adding more
per-component assertions without a shared contract would leave the next generated component
in the same position: the author decides what to assert, and the result is only as good as
the author's awareness of the contract.
Inferred. A registry-driven suite closes this loop permanently: each new component
declares its conformance entry before its exports are wired, the suite runs that entry
against the live output, and the gate fails if the entry is missing or if the live output
does not match the declared contract.
Observed current state
Existing accessibility assertions by component
Observed. All counts from packages/agentic/components/src on
main as retrieved. Test files are the per-component .test.tsx files;
no shared conformance runner exists today.
| Component | Role asserted | Name asserted | Base state asserted | Disabled asserted | Required-prop warning |
|---|---|---|---|---|---|
accordion |
button (header) |
accessibilityLabel |
{ expanded: false } |
no | no |
avatar |
image via getByRole |
accessibilityLabel |
none (decorative) | n/a | no |
badge |
image via getByRole |
accessibilityLabel |
none (decorative) | n/a | yes (icon-only) |
button |
implicit in getByRole |
accessibilityLabel |
{ disabled: false } |
yes | yes (icon-only) |
card |
button or group |
accessibilityLabel |
{ disabled: false, selected: false } |
no | no |
checkbox |
checkbox explicit |
accessibilityLabel |
{ checked: false, disabled: false } |
yes | no |
divider |
separator via getByRole |
accessibilityLabel |
none (structural) | n/a | no |
input |
textbox via getByRole |
none | { disabled: false, invalid: undefined, readOnly: false } |
yes | no |
list-item |
implicit in getByRole |
none | { disabled: false, selected: false } |
yes | no |
listbox-item |
implicit in getByRole |
none | { disabled: false } |
no | no |
menu-item |
menuitem explicit |
none | none in default case | no | no |
progress-bar |
progressbar explicit |
none | {} |
n/a | no |
radio |
radio via getByRole |
accessibilityLabel |
{ checked: false, disabled: false } |
yes | no |
skeleton |
none (decorative) | none | accessible: false |
n/a | no |
spinner |
progressbar via getByRole |
accessibilityLabel |
{ busy: true } |
n/a | yes (label required) |
switch |
switch via getByRole |
accessibilityLabel |
{ checked: false, disabled: false } |
yes | no |
tab |
tab explicit |
accessibilityLabel |
{ disabled: false, selected: false } |
yes | no |
tag |
implicit in getByRole |
accessibilityLabel |
{ disabled: false } |
yes | yes (icon-only) |
Observed. The shared accessibility helper at
src/common/accessibility.ts
exports hiddenFromAccessibilityProps (the three-property "remove from accessibility
tree" object) and hideSlotProps. These are consumed by decorative and structural
primitives but are not used as part of a conformance test.
Observed. No file in packages/agentic/components today imports from a shared
conformance runner or references a per-component conformance declaration. The
src/index.test.ts
export guard asserts that pipeline-stage symbols exist but does not touch accessibility
output.
Observed. Per-component spec/accessibility.md files exist for all 18 components.
They record the authoritative role, required attributes, disabled state contract, and
WCAG citation for each component. They are currently read by agents during authoring
but are not parsed or enforced by any automated check.
Testing infrastructure
Observed.
jest.config.cjs
extends @fluentui-react-native/scripts/jest-config with testTimeout: 10000 and
furn.jestPlatform: "macos". All tests in the package run under the macOS React Native
preset. There is no Windows or Win32 unit-test pass.
Observed. Runtime tests use @testing-library/react-native throughout. The pattern
of getByRole(<role>) followed by props.accessibilityState and props.accessibilityRole
assertions is already established in checkbox, radio, switch, tab, input,
progress-bar, spinner, badge, avatar, card, and accordion.
Observed. The on-device pipeline in
apps/storybook/windows-tests/storybook-smoke.test.cjs
asserts HasKeyboardFocus on eleven components but does not assert native accessibility
properties (role, state, label) through UI Automation. That pipeline is Windows-only,
uninvoked by any workflow, and owned by
storybook-e2e.md.
Scope
-
Define the shared accessibility conformance contract as a small set of assertion
classes, each applicable to some subset of components, with explicit conditions:- Role class: the root element exposes the correct
accessibilityRolevalue in
its default render, queryable bygetByRole. - Name class: a required accessible name is present when the component's
spec/accessibility.mddeclaresaria-labelas required. - State class:
accessibilityStatecontains exactly the keys declared in
spec/accessibility.mdfor the default props, with none missing and no extra
undeclared keys. - Disabled class: when
disabled={true},accessibilityState.disabledis
trueand the state keys match the disabled contract. Applies to every component
whoseuse<Component>.tsaccepts adisabledprop. - Decorative class: the root element sets
accessible: false(or equivalent
hiddenFromAccessibilityProps) when the component'sspec/accessibility.md
declares it decorative. Applies toskeletonand the non-interactive rendering
ofavatarandbadgewhenaccessibilityLabelis absent and the intent is
decorative. - Required-prop warning class: a development-mode
console.warnfires when a
required accessible name prop is omitted, and the message matches the declared
pattern. Applies tobutton(icon-only),badge(icon-only),tag(icon-only),
andspinner(label required).
- Role class: the root element exposes the correct
-
Create a conformance registry as a TypeScript or JSON file at
src/conformance/accessibility-registry.ts(or.json) inside the package. Each
entry records:- the component name and its root
accessibilityRole; - the applicable assertion classes;
- any explicit exceptions with a documented reason;
- the default props required to produce a valid accessible render (e.g.,
accessibilityLabel,label,content); - the expected
accessibilityStatekeys and their default values.
- the component name and its root
-
Write a conformance runner at
src/conformance/accessibility-conformance.test.ts
that iterates the registry, renders each component with its declared default
accessible props, and asserts every applicable class. Each assertion should be
individually named so failures identify the component and the class. -
Close the gaps in the per-component test files that the conformance runner does not
cover because they require component-specific setup (state transitions, required-prop
warnings, controlled props):- Add role assertions to the default case of
button,list-item,listbox-item,
andtag. - Add complete base
accessibilityStateassertions toaccordion,card,
menu-item, andlistbox-itemdefault cases. - Add
disabledassertions tocard,accordion,listbox-item, andmenu-item. - Add
accessibilityValueassertions toprogress-barfor the indeterminate path
(accessibilityValuemust beundefinedwhenvalueis absent). - Add the
accessibilityLabelledByassertion forprogress-barto a case that
verifies the ref-based link is established rather than merely defined.
- Add role assertions to the default case of
-
Add an onboarding gate: a structural check (usable in the package's
lintortest
script) that fails when a symbol exported fromsrc/index.tshas no corresponding
entry in the conformance registry. This prevents a new component from joining the
export surface without a declared conformance entry. -
Update
packages/agentic/components/src/components/AGENTS.md
to require that a new component's authoring checklist includes adding a conformance
registry entry before wiring exports.
Out of scope
- Real-platform screen-reader validation. VoiceOver on macOS and Narrator on Windows
announce role, name, and state at the OS level. That validation is the responsibility
of research/infrastructure/tasks/storybook-e2e.md
and cannot be automated inside a Jest-only pass. - Keyboard and focus behavior. Focus assertions (
HasKeyboardFocus, tab order,
arrow-key navigation) are on-device concerns. The conformance suite asserts static
native accessibility props only. - WCAG contrast ratios. Token color validation is a theming concern tracked in
research/theming/README.md. - Coverage for
packages/components,packages/experimental, andpackages/native.
Those packages have separate test infrastructure and are not in scope. - Primitives without a role contract.
compound-item-layoutandlayout-stable-text
are structural and have no native accessibility role. They are covered by the decorative
class only if the owningspec/declares them as such. Primitives without aSPEC.md
get a recorded exemption entry in the registry, not a generated assertion. - Adding new component behavior to make existing components easier to assert. If an
assertion requires a behavior change, that behavior change is its own item.
Applicability and exception registry
Inferred. The following table is the initial applicability proposal, derived from the
observed test patterns and per-component spec/accessibility.md files. Each entry should
be ratified by the owner and encoded in the machine-readable registry file.
| Component | Role class | Name class | State class | Disabled class | Decorative class | Warn class |
|---|---|---|---|---|---|---|
accordion |
yes | conditional | yes | no | no | no |
avatar |
yes | yes | no | n/a | conditional (no label) | no |
badge |
yes | yes | no | n/a | no | yes (icon-only) |
button |
yes | conditional | yes | yes | no | yes (icon-only) |
card |
yes | yes | yes | yes | no | no |
checkbox |
yes | yes | yes | yes | no | no |
divider |
yes | conditional | no | n/a | no | no |
input |
yes | no | yes | yes | no | no |
list-item |
yes | no | yes | yes | no | no |
listbox-item |
yes | no | yes | yes | no | no |
menu-item |
yes | no | yes | yes | no | no |
progress-bar |
yes | no | yes | n/a | no | no |
radio |
yes | yes | yes | yes | no | no |
skeleton |
no | no | no | n/a | yes | no |
spinner |
yes | yes | yes | n/a | no | yes (required) |
switch |
yes | yes | yes | yes | no | no |
tab |
yes | conditional | yes | yes | no | no |
tag |
yes | conditional | yes | yes | no | yes (icon-only) |
Exception notes:
accordiondisabled: Observed.accordion.test.tsxhas nodisabledprop test
andaccordion.types.tsdoes not declare adisabledprop. Exception: "component does
not exposedisabled"; if adisabledprop is added in future, the disabled class
becomes required.avatarname: Observed.accessibilityLabelis required onAvatarper
spec/accessibility.mdonly when the avatar is meaningfully identified for a screen
reader. When decorative (no label supplied),accessible: falseapplies. The registry
entry should declare both paths.inputname: Observed.input.test.tsxdoes not assert an accessible name.
Theinputspec defers name sourcing to thefieldcontainer (aria-labelledbyfrom
aLabel). A standaloneInputhas no requiredaccessibilityLabel; the name class
does not apply.skeletonrole: Observed.skeleton.test.tsxassertsaccessible: false. A
skeletonis decorative by design; no role class applies, and the decorative class is
the operative assertion.progress-bardisabled:ProgressBarhas nodisabledprop in its type signature.
Exception: "component does not exposedisabled".spinnerdisabled:Spinnerhas nodisabledprop. Exception: "component does not
exposedisabled".
Deliverables
- A conformance contract document at
packages/agentic/components/src/conformance/CONFORMANCE.mddefining the six assertion
classes, the applicability conditions, and the exception format. - A machine-readable conformance registry at
packages/agentic/components/src/conformance/accessibility-registry.ts(or.json)
covering all 18 components and 5 primitives, with each exception documented. - A conformance runner at
packages/agentic/components/src/conformance/accessibility-conformance.test.ts
that iterates the registry and asserts every applicable class, named per component and
class so failures are actionable. - Closed per-component gaps listed in Scope item 4: role assertions added to
button,
list-item,listbox-item, andtag; complete state assertions added toaccordion,
card,menu-item, andlistbox-item; disabled assertions added tocard,
accordion,listbox-item, andmenu-item. - An onboarding gate (structural check) that fails when a higher-order component export
exists insrc/index.tswith no conformance registry entry, wired into the package's
declaredtestorlintscript. - Updated
packages/agentic/components/src/components/AGENTS.md
requiring a conformance entry before export wiring as part of the component authoring
checklist.
Acceptance criteria
- A conformance contract document exists at the stated path and defines the six
assertion classes with applicability conditions. - The conformance registry covers all 18 higher-order components, with every
exception recorded and a reason given. - The conformance runner iterates the registry, renders each component, and asserts
every applicable class. Every assertion is individually named. -
yarn workspace @fluentui-react-native/components testruns the conformance
runner as part of the declared test pass and fails if any assertion fails. - The conformance runner produces a structured per-component summary (pass/fail per
class) that can be consumed by the readiness model to satisfy the "asserted
accessibility semantics" evidence axis. - Every higher-order component in
src/index.tshas a registry entry; the onboarding
gate fails when one is missing. - Per-component gap closures in Scope item 4 are applied and the affected test files
passyarn workspace @fluentui-react-native/components test. -
packages/agentic/components/src/components/AGENTS.mdincludes a conformance
entry requirement in the authoring checklist. - The suite does not replace the per-component test files; it adds a shared layer
above them. Per-component files retain their interaction, state-transition, and
component-specific assertions. - The conformance runner is not the authoritative accessibility gate for on-device
real-platform behavior; the task document and the conformance contract record that
explicitly.
Dependencies and ordering
- Depends on component-test-strategy.md: the
coverage contract must exist before the conformance suite can declare itself an
extension of it. The conformance runner should be adopted as an artifact in the
coverage contract. - Feeds readiness-model.md: the per-component conformance
summary is the machine-readable evidence the readiness model requires for the
"asserted accessibility semantics" axis at thepreviewtier. Completing this task
removes the self-reported character of that evidence axis. - Independent of focus-zone.md,
spec-ingestion-and-generation.md, and the
missing-component tasks. The registry simply adds an entry for each new component. - Independent of the Infrastructure workstream's on-device pipeline
(storybook-e2e.md). The conformance
suite is a static Jest check; it does not consume or replace real-platform automation.
Risks and open decisions
-
Registry format. A
.tsregistry imports directly from the component modules,
giving type safety and IDE discoverability but adding a build-time dependency cycle
risk if the registry is imported by the components themselves. A.jsonregistry
avoids cycles but requires a separate schema file for validation. Open decision:
whether to use TypeScript with a structural import guard or a standalone JSON manifest. -
Conformance runner location. A dedicated
src/conformance/directory isolates
the suite from per-component tests. An alternative is a single file atsrc/root
alongsideindex.test.ts. Open decision: whether dedicated directory or root file
is preferred. -
Handling components with multiple root roles.
cardcan render asrole="button"
(interactive) orrole="group"(display).menu-itemcan render asmenuitem,
menuitemcheckbox, ormenuitemradio. The registry entry must declare the variant
map. Open decision: whether the conformance runner renders one variant per
applicable class or multiple registry entries per role variant. -
Primitives without a role contract.
compound-item-layoutand
layout-stable-textare implementation helpers with no upstream catalog entry and no
SPEC.md. Their registry entries can be stubs with only decorative or no-op entries.
Open decision: whether primitives get minimal registry stubs or are excluded from
the runner scope entirely, with the decision recorded in the registry. -
Interaction with the coverage contract. The conformance runner is an additional
artifact alongside per-component tests, not a replacement. The coverage contract in
component-test-strategy.md must be updated to include
the conformance runner as a required artifact. If that task is completed first, the
contract update should be a deliverable here; if this task lands first, the conformance
runner should be listed as a pending contract artifact.
Evidence and references
packages/agentic/components/src/common/accessibility.ts- the shared
hiddenFromAccessibilityPropsandhideSlotPropshelpers. The conformance
registry should reference this module for the decorative class.
- the shared
packages/agentic/components/src/components/button/button.test.tsx- canonical accessibility pattern:
getByRole,accessibilityState,accessibilityLabel,
andconsole.warnassertions.
- canonical accessibility pattern:
packages/agentic/components/src/components/checkbox/checkbox.test.tsx
andradio/radio.test.tsx- deepest existing contracts: role, label, full state tuple, disabled, and consumer
passthrough in one file.
- deepest existing contracts: role, label, full state tuple, disabled, and consumer
packages/agentic/components/src/components/*/spec/accessibility.md- authoritative per-component ARIA role, required attributes, disabled contract, and
WCAG citations. The registry entries should be derived from these files.
- authoritative per-component ARIA role, required attributes, disabled contract, and
packages/agentic/components/src/index.ts- the 175-line explicit export surface. The onboarding gate uses this file as its
source of truth.
- the 175-line explicit export surface. The onboarding gate uses this file as its
packages/agentic/components/src/index.test.ts- the existing export guard; the onboarding check extends the same pattern.
packages/agentic/components/jest.config.cjs- test execution environment; the conformance runner runs in the same pass.
- component-test-strategy.md - the coverage contract
this suite extends. - readiness-model.md - the consumer of the conformance summary.
- research/infrastructure/tasks/storybook-e2e.md
- the real-platform accessibility pipeline this suite explicitly does not replace.
Sources retrieved 2026-08-21.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the per-component spec/accessibility.md files, the existing component tests, src/common/accessibility.ts, jest.config.cjs, and src/index.test.ts. Run the existing package Jest pass to understand the macOS setup and current accessibility assertions. Done means every exported component has a declared registry entry, the shared suite verifies its applicable contract, and the test script gates missing declarations or mismatched output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- accessibility, frontend, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100