diff --git a/.changeset/sharp-teeth-happen.md b/.changeset/sharp-teeth-happen.md new file mode 100644 index 0000000000..d1e7907cd9 --- /dev/null +++ b/.changeset/sharp-teeth-happen.md @@ -0,0 +1,5 @@ +--- +"@fluentui-react-native/components": patch +--- + +Expose Button's plain content as its native name and declare Toggle when selected is supplied, preserving explicit naming, caller actions and externally owned checked state. diff --git a/packages/agentic/components/src/components/button/SPEC.md b/packages/agentic/components/src/components/button/SPEC.md index efac5fe7e3..54207440e6 100644 --- a/packages/agentic/components/src/components/button/SPEC.md +++ b/packages/agentic/components/src/components/button/SPEC.md @@ -62,11 +62,12 @@ layout and requires an action-oriented `accessibilityLabel`. with the selected-icon fallback and wrapping content behavior. - **BTN-003:** Resolve appearance, selection, disabled, pressed, and hovered visual state from Flex tokens; user root style wins last. -- **BTN-004:** Expose button semantics, merge caller accessibility state, warn +- **BTN-004:** Expose button semantics and a native name from plain content + unless the caller supplies naming, merge caller accessibility state, warn for an unnamed icon-only button, and hide decorative icons. - **BTN-005:** Treat `selected` as externally owned state, expose checked - accessibility state when that prop is present, and reserve semibold label - width to prevent toggle reflow. + accessibility state and the resolved native Toggle action when that prop is + present, and reserve semibold label width to prevent toggle reflow. - **BTN-006:** Follow the [shared focus visual policy](../../../references/higher-order-components.md#focus-visual-policy) on the root: default to native rings on Windows/macOS and root-modality-aware custom rings on Win32, retaining the same dual-ring geometry. @@ -92,6 +93,14 @@ Windows and macOS use React Native press, hover, and focus events. `Enter` and `Space` activation are supplied by the native `Pressable` button behavior. Disabled buttons are not focusable. +The checked React Native state and resolved Toggle declaration do not imply +identical native accessibility projection. Windows exposes toggle state through +UI Automation. With the pinned macOS renderer and desktop provider, the retained +AXButton role has no supported native `checked` read, and Toggle is a custom +action rather than an AXPress mapping. Native role and enabled-state checks +remain shared; checked projection is qualified separately on Windows. +Caller-owned pointer transitions on macOS require physical input. + Button adds no component-owned motion. ## Divergences from Flex @@ -106,11 +115,11 @@ Button adds no component-owned motion. ## Conformance -| Requirement | Evidence | -| ----------- | --------------------------------------------------------------------------- | -| BTN-001 | `button.types.ts`, `useButton.ts`, `button.test.tsx` | -| BTN-002 | `renderButton.tsx`, `button.test.tsx`, `button.stories.tsx` | -| BTN-003 | `button.styles.ts`, `useButtonStyles.ts`, `button.test.tsx` | -| BTN-004 | `useButton.ts`, `useButtonStyles.ts`, `button.test.tsx` | -| BTN-005 | `useButton.ts`, `renderButton.tsx`, `button.test.tsx` | -| BTN-006 | `useButton.ts`, `useButtonStyles.ts`, `renderButton.tsx`, `button.test.tsx` | +| Requirement | Evidence | +| ----------- | ----------------------------------------------------------------------------- | +| BTN-001 | `button.types.ts`, `useButton.ts`, `button.test.tsx` | +| BTN-002 | `renderButton.tsx`, `button.test.tsx`, `button.stories.tsx` | +| BTN-003 | `button.styles.ts`, `useButtonStyles.ts`, `button.test.tsx` | +| BTN-004 | `useButton.ts`, `useButtonStyles.ts`, `button.test.tsx`, `button.stories.tsx` | +| BTN-005 | `useButton.ts`, `renderButton.tsx`, `button.test.tsx`, `button.stories.tsx` | +| BTN-006 | `useButton.ts`, `useButtonStyles.ts`, `renderButton.tsx`, `button.test.tsx` | diff --git a/packages/agentic/components/src/components/button/button.stories.tsx b/packages/agentic/components/src/components/button/button.stories.tsx index eeae742bf3..a178b57d9a 100644 --- a/packages/agentic/components/src/components/button/button.stories.tsx +++ b/packages/agentic/components/src/components/button/button.stories.tsx @@ -1,10 +1,12 @@ /** @jsxImportSource @fluentui-react-native/framework-base */ import { useState } from 'react'; import type { ReactNode } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; +import { Platform, StyleSheet, Text, View } from 'react-native'; +import type { AccessibilityActionEvent } from 'react-native'; import type { Meta, StoryObj } from '@storybook/react-native'; import { ThemedRoot, useRootInputModality } from '@fluentui-react-native/design'; +import { resolveAccessibilityAction } from '@fluentui-react-native/framework-base'; import type { FocusKeyboardEvent, FocusRequest } from '@fluentui-react-native/framework-base'; import type { WdioStory } from '@fluentui-react-native/storybook-desktop/testing'; @@ -520,38 +522,63 @@ export const ExternallyDrivenSelection: Story = { 'starts unselected': async ({ browser, expect }) => { await expect(await browser.$('~agentic-storybook-button-selection-state')).toHaveText('Not selected'); }, - 'updates and resets caller-owned selection': async ({ browser, expect, skip }) => { + 'updates and resets caller-owned selection': async ({ browser, expect, platform, skip }) => { const features = browser.capabilities['furn:features']; if (!features) throw new Error('Desktop Driver did not provide feature capabilities.'); - if (!features.physicalClick) { - skip('This test requires physical pointer input.'); + if (platform === 'macos' && !features.physicalClick) { + skip('macOS caller-owned selection requires physical pointer input; the custom Toggle action is not an AXPress route.'); return; } + if (!features.physicalClick && !features.accessibilityClick) { + skip('This test requires native accessibility activation or physical pointer input.'); + return; + } + const { clickPhysical } = await import('../../common/desktopFocus.wdio.ts'); const favorite = await browser.$('~agentic-storybook-button-favorite'); const reset = await browser.$('~agentic-storybook-button-reset'); const status = await browser.$('~agentic-storybook-button-selection-state'); await expect(favorite).toBeEnabled(); await expect(status).toHaveText('Not selected'); - await favorite.click(); + if (platform === 'macos') { + await clickPhysical(browser, 'agentic-storybook-button-favorite'); + } else { + await favorite.click(); + } await expect(status).toHaveText('Selected'); - await reset.click(); + if (platform === 'macos') { + await clickPhysical(browser, 'agentic-storybook-button-reset'); + } else { + await reset.click(); + } await expect(status).toHaveText('Not selected'); }, }, render: () => { const ToggleGroup = () => { const [selected, setSelected] = useState(false); + const toggle = resolveAccessibilityAction('toggle', Platform.OS).name; return (