microsoft / microsoft/fluentui-react-native
Components: Align Button and ToggleButton contracts against upstream
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 179
- Avg merge
- 16h 17m
- Merged PRs (30d)
- 30
Description
Summary
The local button component carries a selected prop and full toggle-button semantics that upstream removed from button at v1.4.0 and placed in a dedicated toggle-button. Decide now whether the agentic button treats that as a permanent, documented deviation or migrates selected out into toggle-button. The same v1.4.0 upstream change also renamed the Radius axis to Shape with a two-value set (Rounded/Circular), and split the single icon + iconPosition slot into independent leading and trailing icon slots. Those three drifts affect both components in tandem, so they must be decided together rather than left to the toggle-button generation task. The toggle-button task is explicitly blocked here until an owner decision covers all three axes.
Inferred. If the decision is not made before generation starts, the package will simultaneously expose Button's externally driven selected and ToggleButton's self-driving selected/defaultSelected/onSelectedChange as two overlapping toggle patterns with no documented boundary, and any later cleanup becomes a breaking change.
Goal
Record a written owner decision for each of the three drifted axes, update the local button contract and SPEC.md to reflect that decision, and provide the exact scope statement that the toggle-button generation task needs to implement selected, icon slots, and shape consistently with button.
Stage
Stages 0 and 1 (scope decision through beta foundations). Stage 0 chooses the
Button and ToggleButton contract boundary; Stage 1 applies the Button-side API,
spec, test, story, and migration consequences. This decision is a prerequisite
for the Wave 1 toggle-button generation task and must be resolved before any
beta-readiness work on button claims spec fidelity.
Why it matters
Observed. The local button SPEC.md describes the Selected axis, the Regular-to-Filled icon swap, and the font-weight swap as first-class variant axes. The upstream button spec at d334acf removed all of that and placed it in toggle-button. Nothing in the local repository documents that difference or records it as a deliberate choice.
Observed. The local button test suite has 15 cases, 7 of which directly exercise the toggle axis: icon-swap selection, ghost-label reflow prevention, externally driven selection semantics, and accessibility state. Those tests will become misleading the moment a generated toggle-button next to them claims the same Selected axis.
Observed. toggle-button.md already flags this as an open risk: "This task and the button divergence are two halves of one decision. Landing toggle-button without deciding button's selected axis leaves two overlapping toggle APIs in one package."
Inferred. The spec-ingestion drift report (spec-ingestion-and-generation.md) will record button as drifted on all three axes. Without an explicit owner disposition, any automated drift check cannot distinguish "accepted deviation" from "unresolved divergence", and button blocks the drift gate permanently.
Observed current state
Local button: the three drifted axes
Observed. From
packages/agentic/components/src/components/button/button.types.ts:
| Axis | Local contract | Upstream contract at d334acf |
|---|---|---|
| Selected | selected?: boolean in ButtonStateProps; selectedIcon slot for icon swap |
Removed from button; moved to toggle-button as self-driving selected/defaultSelected/onSelectedChange |
| Shape | shape: 'rounded' | 'square' | 'circle' |
Shape: Rounded | Circular (no square) |
| Icon slots | Single icon: OptionalSlot<Icon> + iconPosition: 'before' | 'after' + selectedIcon: OptionalSlot<Icon> |
Independent leadingIcon and trailingIcon slots; icon swap is intrinsic to toggle-button |
Observed. From
packages/agentic/components/src/components/button/useButton.ts:
selection is externally driven. isToggleButton = selected !== undefined. The button
never changes selected on press; the caller owns the value and updates it from
onPress. When isToggleButton is true, accessibilityState.checked = selected is
set and a contentHidden ghost slot is added to prevent label reflow on weight swap.
Observed. From
packages/agentic/components/src/components/button/renderButton.tsx:
the active icon is resolved as state.selected ? (state.selectedIcon ?? state.icon) : state.icon,
and the ghost label via LayoutStableText is rendered only when isToggleButton && ContentHidden.
Observed. From
packages/agentic/components/src/components/button/button.test.tsx:
seven of the fifteen test cases exercise toggle behavior directly.
Upstream toggle-button: the target contract
Observed. From
toggle-button.md, citing upstream
x3-design/fluent-design @
d334acf,
plugins/components/skills/toggle-button/SKILL.md:
- Axis:
Selected(True/False). Upstream toggle-button is self-driving: it owns
state through theselected/defaultSelected/onSelectedChangetriple and
useToggleState, unlikebutton's externally drivenselected. - Icon swap: Regular-to-Filled icon swap on
selected=True. No separateselectedIcon
slot; the swap is intrinsic to the component. - Label: font-weight swap (Regular at rest, Semibold when selected), with the ghost
Semibold node reserving layout width. - Shape:
Rounded | Circularonly, matching the current upstreambutton. - Icon slots: independent
leadingIconandtrailingIcon, matching the current
upstreambutton.
FURN V1 ToggleButton: a third naming variant
Observed. From
packages/components/Button/src/ToggleButton/ToggleButton.types.ts:
the legacy component uses checked?: boolean and defaultChecked?: boolean (extending
ButtonProps). This differs from both the local agentic selected and the upstream
selected/defaultSelected pair. Migration guidance must account for the FURN V1
naming so consumers on the legacy package are not doubly confused.
Coverage that changes under each option
Observed. Button's toggle-related test cases in
button.test.tsx:
| Test case | Toggle-specific |
|---|---|
| uses the regular icon while a toggle button is not selected | Yes |
| uses selected semantics, selected icon, and a ghost label to prevent reflow | Yes |
| renders selection without changing it on press | Yes |
| reserves the selected label width in both selected states so toggling cannot reflow | Yes |
| omits selection semantics when no selection prop is supplied | Yes |
| places the icon after content and applies user styles last (touches iconPosition) | Indirect |
| renders a persistent dual-ring focus visual (touches selected path) | Indirect |
If selected migrates out (Decision A2), those cases move to toggle-button and
button loses the ghost label, selectedIcon slot, isToggleButton state, and all
contentHidden render logic.
Scope
This task is a decision task, not a generation task. Its artifact is a written
owner decision for each of the three axes, applied to button/SPEC.md and the
button.types.ts comment block, and a companion scope statement that the
toggle-button task will execute.
Decision A: The selected axis
Two options:
Option A1 -- Intentional deviation. Local button keeps selected as an
externally driven prop. Rationale: the caller owns state; a press is an action, not a
state change. The behavior is well-tested, documented, and consistent with the
button.types.ts comment ("Selection is externally driven: the button never changes
it on press, because a press is an action rather than a state change"). toggle-button
is generated as a self-driving wrapper whose onSelectedChange calls a user-supplied
handler and updates internal state. The two components coexist with a documented
boundary: button.selected for caller-owned toggle state, toggle-button for
component-owned toggle state. The deviation is recorded in button/SPEC.md with the
upstream reference and the rationale.
Option A2 -- Migrate. Remove selected, selectedIcon, isToggleButton,
contentHidden, and all ghost-label logic from button. The toggle-button
generation task inherits the full toggle contract from upstream. This is a breaking
change for any code using button.selected. The migration note must explain the
replacement pattern for both the local agentic button.selected and the FURN V1
ToggleButton.checked/defaultChecked APIs.
Decision B: The shape axis
Two options:
Option B1 -- Keep 'square'. Local button retains
shape: 'rounded' | 'square' | 'circle'. square is a local extension with no
upstream counterpart; the deviation is recorded in SPEC.md. toggle-button aligns
with upstream and exposes only 'rounded' | 'circular', with a note that button is
a superset.
Option B2 -- Align with upstream. Remove 'square' from ButtonShape and
rename 'circle' to 'circular'. useButton.ts currently defaults shape to 'circle'
for icon-only buttons; that default must be updated. This is a breaking change for
any code using shape="square" or shape="circle". toggle-button and button
then share the same shape vocabulary.
Decision C: The icon-slot layout
Two options:
Option C1 -- Keep icon + iconPosition + selectedIcon. The local single-icon
layout is retained. toggle-button inherits the same slots and adds the intrinsic
swap behavior on top. The deviation from upstream's independent leadingIcon /
trailingIcon is documented.
Option C2 -- Adopt independent leadingIcon and trailingIcon. Removes
iconPosition from ButtonStateProps and splits the icon slot into leadingIcon
and trailingIcon. The selectedIcon slot is also dropped; the icon swap for
toggle-button is achieved through the component's own state. toggle-button then
aligns fully with upstream's slot layout. This is a breaking change.
Inferred. Decision C is the least urgent of the three because the current
icon + iconPosition layout works correctly on both platforms and no blocking
downstream component depends on independent slots. If Decision A2 is chosen, the
selectedIcon removal is bundled and Decision C can be made in the same window.
Out of scope
- Implementing
toggle-button. This task only defines the scope statement for
toggle-button.md; execution stays in that
task. - Resolving the drift detection mechanism. Recording the decision as an accepted
deviation in the drift report is the responsibility of
spec-ingestion-and-generation.md. - Updating any FURN V1 (
packages/components/Button) implementation. Migration
guidance is documentation only; no changes to the legacy package. - Deciding whether the
buttontype test gap (see
component-test-strategy.md) is addressed here or in
the coverage task. Either task may addbutton.types.test.ts; this task governs
only the contract change.
Deliverables
- A written owner decision for each of the three axes (A, B, C), recorded in the
Risks and open decisions section of this file and inbutton/SPEC.mdfront matter
or aDeviationssection. - If Decision A1: an
Accepted deviationrecord inbutton/SPEC.mdciting the
upstream reference, the local rationale, and the upstream revision at which the
divergence was introduced. - If Decision A2: a PR removing
selected,selectedIcon,isToggleButton,
contentHidden, ghost-label render logic, and the toggle-specific tests from
button, with a migration comment in the changeset. Thetoggle-buttonscope
statement in this document is updated to reflect inherited behavior. - For Decision B and C: equivalent SPEC.md deviation records or API changes, scoped
analogously to A1 vs A2. - An updated scope statement for toggle-button.md
that specifies exactly which behaviorstoggle-buttonmust implement vs inherit or
extend frombutton, resolving the open risk items in that document. - A changeset if any source files in
packages/agentic/componentsare modified.
Acceptance criteria
- Each of the three axes has a recorded owner decision in this document and in
button/SPEC.md. - If A1:
button/SPEC.mdhas an explicit deviation record with the upstream
reference and rationale. - If A2:
button.types.ts,useButton.ts,renderButton.tsx, the test file,
and stories no longer referenceselected,selectedIcon,isToggleButton, or
contentHidden; the changeset includes a migration note. - toggle-button.md is updated: the
block imposed by this task is lifted, and the scope section carries a precise
statement for theselectedaxis,shapevalues, and icon-slot layout the
generated component must implement. -
yarn workspace @fluentui-react-native/components build,lint, andtest
pass after any source changes. - If Decision B2 or C2: no
shape="square",shape="circle", oriconPosition
references survive insrc/components/button/.
Dependencies and ordering
- Blocks toggle-button.md: the
toggle-buttongeneration task must not start until this alignment task is
resolved. toggle-button.md open risk 1
("This task and thebuttondivergence are two halves of one decision") and the
scope item ("Resolve the relationship with the existing agenticbutton...") are
directly answered here. - Informs spec-ingestion-and-generation.md:
once each axis has an owner decision, the drift report can classify thebutton
divergence as "accepted deviation" or "resolved" rather than "unresolved divergence". - Informs readiness-model.md: the provisional assessment
holdsbuttonatexperimentalbecause of the spec drift. A Decision A1 record
(accepted deviation, documented) may satisfy the spec-fidelity axis for readiness
purposes. - Informed by component-test-strategy.md: the
coverage contract governs how many testsbuttonmust retain after any removal.
Decision A2 removes toggle-specific cases; the remaining set must still meet the
minimum contract defined there. - Independent of focus-zone.md and the overlay wave.
Risks and open decisions
- Decision A (selected axis): OPEN. Choose between intentional deviation (A1)
and migration (A2). The downstream impact is asymmetric: A1 is non-breaking but
leaves two toggle patterns in one package; A2 aligns with upstream but breaks
current consumers ofbutton.selected. - Decision B (shape axis): OPEN. Retaining
'square'is the non-breaking path.
Removing it aligns fully with upstream but breaks any code usingshape="square".
Inferred. Renaming'circle'to'circular'is also required for full
alignment, affecting theuseButton.tsicon-only default. - Decision C (icon-slot layout): OPEN. Independent leading/trailing slots follow
upstream and match thetoggle-buttoncontract, but the migration cost is higher
than for the other two axes becauseiconPositionis a first-class prop with story
controls. Inferred. C2 is most natural to bundle with A2 if that path is chosen,
because removingselectedIconis part of both. - FURN V1 naming. The legacy
ToggleButtonuseschecked/defaultChecked.
Any migration documentation must name both the agenticbutton.selectedand the
FURN V1 pattern as the two APIs being replaced, to avoid confusing consumers on the
legacy package.
Evidence and references
| Claim | Kind | Source |
|---|---|---|
Local button carries selected, selectedIcon, iconPosition, shape: 'rounded' | 'square' | 'circle' |
Observed | button.types.ts |
Selection is externally driven; isToggleButton = selected !== undefined |
Observed | useButton.ts |
| Icon swap and ghost label in render | Observed | renderButton.tsx |
| Seven toggle-specific test cases | Observed | button.test.tsx |
Upstream button removed Selected, split icon slots, renamed Radius to Shape |
Observed | Components README; spec-ingestion-and-generation.md |
Upstream toggle-button SKILL.md at pinned revision |
Observed | plugins/components/skills/toggle-button/SKILL.md at x3-design/fluent-design @ d334acf |
Upstream button SKILL.md at pinned revision |
Observed | plugins/components/skills/button/SKILL.md at x3-design/fluent-design @ d334acf |
FURN V1 ToggleButton uses checked/defaultChecked |
Observed | ToggleButton/ToggleButton.types.ts |
| toggle-button task flags this as open risk 1 | Observed | toggle-button.md |
button held at experimental due to spec drift |
Inferred | readiness-model.md |
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 with packages/agentic/components/src/components/button/button.types.ts, useButton.ts, renderButton.tsx, button.test.tsx, and the button SPEC.md, then compare the cited upstream contracts and legacy ToggleButton types. Record an owner decision for selected, shape, and icon slots, apply it to the button contract and spec, and provide the companion scope statement for toggle-button.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- documentation, frontend, mobile-dev
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100