Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-avatar-activity-ring-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fluentui-react-native/components": patch
---

Prevent the Avatar activity ring from crashing React Native Windows Fabric by replacing native outlines with a persistently mounted decorative border. Preserve its token-derived gap, stroke, layout, and accessibility, and cover visibility changes across all avatar sizes.
5 changes: 5 additions & 0 deletions .changeset/fix-native-component-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fluentui-react-native/components": patch
---

Center intrinsic font icons, Avatar initials, and layout-stable labels inside their View frames, and move single-line Input spacing outside the native editor. Correct the compound text-row baseline examples and add alignment regression coverage and authoring guidance.
6 changes: 6 additions & 0 deletions .changeset/macos-loading-motion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@fluentui-react-native/framework-base': patch
'@fluentui-react-native/components': patch
---

Restore loading animations on macOS Fabric by selecting the supported JavaScript animation driver. Keep Skeleton silhouettes visible without motion and calculate Spinner's quarter-circle dash lengths from native geometry. Add native motion and macOS text-alignment regression coverage.
5 changes: 5 additions & 0 deletions .changeset/skeleton-diagonal-gradient.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/components': patch
---

Replace Skeleton's solid highlight strip with a soft, angled SVG linear-gradient sweep based on the repository's Shimmer pattern. Preserve shared timing, reduced motion, rounded clipping, and visible static placeholders, and mirror travel for RTL.
5 changes: 5 additions & 0 deletions .changeset/storybook-canvas-secondary-fill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@fluentui-react-native/storybook-desktop-runtime': patch
---

Align Storybook's actual canvas background with its preview surface so Secondary Button fills remain visible in Default Flex, light, and dark themes. Preserve explicit story background overrides and native-color fallbacks.
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,67 @@ Native rings default to Windows/macOS; Win32 uses the root-modality-aware custom
path requires RNW 0.81.35 or newer. Do not introduce `outline*` focus styles, local modality trackers, or
functional border changes to implement a focus ring.

For non-focus indicators such as Avatar's activity ring, reuse `FocusVisual` as a persistent decorative border rather
than native outline properties. This preserves stable border lifetime on renderers affected by late border creation.
The component owns the indicator's meaning, tokens, and visibility, independently of the focus policy. For an outward
ring with a gap, each absolute edge is `-(gap + strokeWidth)` so the border preserves the requested gap without changing
layout. Mount it with its border configured even when hidden; do not switch border width between zero and a nonzero value.

## Native motion

Use `useSharedAnimatedLoop` for phase-locked loading indicators. It treats `useNativeDriver` as a request and selects
the JavaScript driver on macOS Fabric, where native-driven transforms currently remain static. Other renderers keep
their requested driver. These continuous loops must use `isInteraction: false` so the JavaScript path does not starve
interaction-deferred work. Preserve reduced-motion gating independently of driver selection.

Verify animation with changing pixels on the actual native element, not merely an `Animated.loop().start()` assertion.
Keep a visible static silhouette when motion is disabled, and do not rely on web-only SVG `pathLength` normalization;
derive native dash lengths from the actual circumference.

## Native text and vertical alignment

Treat the component frame, the native text line box, and the glyph's visible ink as different measurements. `alignItems`
and `justifyContent` align Yoga boxes; they do not move a glyph inside a stretched Text or editor. A font's `fontSize`
is not its measured ascent plus descent. Platform font fallback can make the difference particularly visible on Win32.

- Use `Icon` for font glyphs. Its sized View centers an intrinsic, non-shrinking Text child. With a fully specified frame,
the Text is absolute with no edge offsets so Yoga measures its natural height instead of clamping it to the frame.
Without a complete frame, normal flow retains intrinsic dimensions. Do not put the icon's
`height` or `lineHeight` on that Text, use a negative margin or transform to nudge one glyph, or depend on
`textAlignVertical` as a cross-platform centering mechanism. Icon dimensions own scaling, just as they do for images
and SVGs; dimensioned font icons do not independently follow text scaling.
- Retain typography line-height tokens for ordinary labels and paragraphs. Do not globally remove line height to fix
one renderer, and do not replace a typography line height with a control's height or the font's em size.
- Centered initials follow the same intrinsic-metrics rule. Avatar owns all typography and uses native Text to avoid
inheriting the themed Text component's body line height. It preserves token font sizes, native text scaling, and
explicit caller line metrics, and uses an absolute child without edge offsets to avoid clamping in the smallest
padded frame. Do not clip initials to an em square or assume `undefined` can clear merged defaults.
- For single-line `TextInput`, reset internal padding and let the native editor measure its font. Put spacing outside
the editor with Yoga margins or a containing View; keep the control's token-derived minimum height separate.
Input demonstrates this without adding another wrapper or changing the caller's final style precedence.
Preserve the pointer target when moving spacing: Input's inaccessible, non-focusable contents row forwards activation
to the editor through its shared focus target, whose ref is composed by the slot render path. Test disabled and
non-focusable behavior and caller ref forwarding too.
- Distinguish centered graphics from aligned text baselines. `CompoundItemLayout` centers arbitrary regions by default.
A text-only row with different label/shortcut font sizes can use `alignItems: 'baseline'` on the root and inline
content row. All participating regions must supply meaningful text baselines; do not impose that policy on images
or action buttons.
- On Android, disable extra font padding for tightly sized decorative glyphs. That setting is not a desktop fix.
- Exercise small and large sizes, font and image/SVG icons, different text metrics, selection, and constrained wrapping.
Check the running native output, not just flattened styles: a snapshot of the wrong Text height can pass indefinitely.

## Selected text without layout shift

When selected text changes weight:

- Render an inaccessible Semibold ghost that reserves width and height.
- Overlay the visible label.
- Overlay a View that vertically centers the visible label at its own measured height. Do not stretch the visible Text
itself with `absoluteFillObject`: smaller text will otherwise sit at the top of the reserved area on native renderers.
- Keep the ghost and container state-only.
- Apply the same wrapping constraints to both labels.

Button uses theme-independent visibility selectors for the hidden and overlaid styles and token-derived typography for
the actual font metrics.
Use `LayoutStableText`, as Button and Tab do, rather than duplicating the ghost/overlay structure. The overlay must
preserve visible-text accessibility and input handling. Typography remains owned by the consuming component.

## Review checklist

Expand All @@ -157,3 +207,5 @@ the actual font metrics.
- User styles are last.
- Slot props share resolved color and size values consistently.
- Constrained text can wrap unless truncation is an explicit public choice.
- Font frames and reserved labels center intrinsic text; editor spacing is external; text baselines are an explicit
choice distinct from centering arbitrary slots.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ contracts. Keep exploratory type probes outside package source or remove them be
Primitive runtime tests should cover every renderer or source branch and verify forwarding of size, color,
accessibility, and test props.

Native motion stories compare repeated element screenshots and honor the runtime reduced-motion preference.
Use `browser.isElementDisplayed(elementId)` and `browser.takeElementScreenshot(elementId)` for native protocol operations;
WebdriverIO's DOM-oriented `element.isDisplayed()` tries unsupported JavaScript execution. Skip pixel checks explicitly
when the driver does not provide element screenshots.

## Visual snapshots

Keep snapshots focused on resolved output rather than the full renderer tree. Button snapshots map each appearance to:
Expand Down Expand Up @@ -256,5 +261,14 @@ focus-test result; never bypass ownership or authenticated readiness to proceed.
A successful bundle proves story discovery and compilation only. For visual changes, inspect the running target-platform
story across hover, pressed, disabled, optional-slot, and constrained-content scenarios.

Alignment regressions must distinguish the outer frame from the inner text. LayoutStableText's Overview includes an
executable desktop check that the smaller visible Text has a smaller measured height than its reserve and that their
vertical centers agree within one layout pixel. Checking only their centers would let the old stretched-Text bug pass.
Its measurement fixture exposes an explicit group around the primitive and an accessible visible Text rather than
querying the primitive's intentionally inaccessible layout wrapper. macOS Fabric does not expose paragraph test IDs to
AX, so that endpoint reads the Text's actual `onLayout` measurements from a named `StoryStatus` View instead.
Pair geometry checks with native screenshots for glyph ink and caret placement; native element bounds alone do not
prove optical alignment. Never record an unavailable native check as passed.

Adding a new story file changes Metro's `require.context` catalog and may require restarting Metro and the native app
before the running Storybook index includes it.
5 changes: 5 additions & 0 deletions packages/agentic/components/src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ the change crosses component boundaries.

## Optimization principles

- Align native text with View layout, not synthetic text heights or baseline offsets. A font's em size, line box, and
visible glyph are different measurements. Use `Icon` for font icons and `LayoutStableText` for reserved labels; follow
the [native alignment rules](../../../../.github/skills/agentic-component-authoring/references/styles-and-tokens.md#native-text-and-vertical-alignment)
for editors, mixed-size text rows, and regression coverage.

- Audit dependency direction before introducing shared code.
- Look for repeated types, constants, routines, and subtrees across components.
- Validate extraction payoff before creating another layer of indirection.
Expand Down
2 changes: 2 additions & 0 deletions packages/agentic/components/src/components/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ audit.
- Follow the [focus visual policy](#focus-visual-policy): create the optional `FocusRing` in the state hook,
apply native settings only to the actual focus target, and style the custom ring in the styling phase.
Do not introduce component-specific modality trackers or `outline*` focus styling.
- Keep non-focus indicators such as Avatar's activity ring independent of the focus visual policy.
Reuse `FocusVisual` as a persistent decorative border and toggle opacity rather than native outline properties.
- Keep render functions free of hooks, token reads, style creation, and slot mutation.
- Export the resolved state type and the state, style-application, and render stages from the package root under
component-qualified unstable names so another component can reuse the pipeline.
Expand Down
37 changes: 28 additions & 9 deletions packages/agentic/components/src/components/avatar/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ Avatar is a non-interactive identity marker. It renders exactly one image, icon,

`size` defaults to `40` and accepts `16`, `20`, `24`, `28`, `32`, `40`, `56`, or `120`. `activityRing` defaults to `false`. `root` is required; `image`, `icon`, and `initials` are optional slots. The resolved content mode uses image first, then initials, then icon. Icon mode renders a person icon by default. Initials mode defaults to `AB`, uppercases string or numeric content, trims whitespace, and limits output to one character at size `16` or two characters otherwise.

The root contains only the active content slot. Images are absolute-fill and use cover resizing. Icon and initials slots are centered; all content slots are decorative because the root owns any accessible identity. Supplying more than one content slot is accepted for compatibility, resolves through the documented priority, and issues a development warning.
The root contains a persistent decorative activity-ring visual and only the active content slot. Images are absolute-fill and use cover resizing. Icon and initials slots are centered; all content slots are decorative because the root owns any accessible identity. Supplying more than one content slot is accepted for compatibility, resolves through the documented priority, and issues a development warning.

The circular View centers the initials' intrinsic native line box. Avatar owns
all initials typography and uses native Text without an inherited body line
height or an em-sized line-height clamp, which can clip or displace glyphs.
Font-size tokens, native text scaling, and explicit caller line metrics are preserved.
The initials are an absolute child without edge offsets, so the smallest
avatar's padded frame cannot clamp that intrinsic line box during measurement.

The resolved state retains size, activity-ring value, content mode, theme state, and user root style. User style is applied last. Avatar has no interaction-state ownership.

Expand All @@ -32,12 +39,23 @@ The resolved state retains size, activity-ring value, content mode, theme state,
- **AVT-005:** Add no interaction or focus behavior of its own and delegate
those semantics to a containing control, while forwarding the broad root
`ViewProps` surface.
- **AVT-006:** Keep the activity ring mounted with its border configured from the
first render, hide it through opacity when inactive, and avoid native outline
properties. Its token-derived gap and stroke extend outside the avatar without
changing layout, hit testing, or accessibility.

## Platform behavior

An Avatar with `accessibilityLabel` is accessible with React Native image role; callers can also explicitly control `accessible`. Without an informative label, the default root is hidden from accessibility descendants. Its image, icon, and initials children are always hidden so identity is not announced twice.

Windows exposes an informative root as a UI Automation image; macOS exposes it as an AX image. Avatar adds no tab stop or `FocusVisual`, although a caller can opt the forwarded root into focus with `focusable`. The active ring uses React Native root outline properties and does not introduce a separate rendered child or change the requested width and height.
Windows exposes an informative root as a UI Automation image; macOS exposes it as an AX image. Avatar adds no tab stop or focus feedback, although a caller can opt the forwarded root into focus with `focusable`.

The activity ring reuses `FocusVisual` as a persistent decorative border, not as
a focus indicator. Its absolute edges are inset by the negative sum of the
activity-ring gap and stroke width. The same border remains configured when
inactive; only opacity changes. This avoids the native outline path that can
fail-fast in React Native Windows 0.81 Fabric on a background-filled avatar,
including when the initial render already requests an active ring.

## Divergences from Flex

Expand All @@ -48,10 +66,11 @@ Windows exposes an informative root as a UI Automation image; macOS exposes it a

## Conformance

| Requirement | Evidence |
| ----------- | ---------------------------------------------------------------------------- |
| AVT-001 | `avatar.types.ts`, `useAvatar.ts`, `avatar.test.tsx`, `avatar.types.test.ts` |
| AVT-002 | `useAvatarStyles.ts`, `renderAvatar.tsx`, `avatar.test.tsx` |
| AVT-003 | `avatar.styles.ts`, `useAvatarStyles.ts`, `avatar.test.tsx` |
| AVT-004 | `useAvatar.ts`, `useAvatarStyles.ts`, `avatar.test.tsx` |
| AVT-005 | `avatar.types.ts`, `useAvatar.ts`, `avatar.stories.tsx` |
| Requirement | Evidence |
| ----------- | ----------------------------------------------------------------------------------------------------- |
| AVT-001 | `avatar.types.ts`, `useAvatar.ts`, `avatar.test.tsx`, `avatar.types.test.ts` |
| AVT-002 | `useAvatarStyles.ts`, `renderAvatar.tsx`, `avatar.test.tsx` |
| AVT-003 | `avatar.styles.ts`, `useAvatarStyles.ts`, `avatar.test.tsx` |
| AVT-004 | `useAvatar.ts`, `useAvatarStyles.ts`, `avatar.test.tsx` |
| AVT-005 | `avatar.types.ts`, `useAvatar.ts`, `avatar.stories.tsx` |
| AVT-006 | `avatar.styles.ts`, `useAvatarStyles.ts`, `renderAvatar.tsx`, `avatar.test.tsx`, `avatar.stories.tsx` |
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { ReactNode } from 'react';
import { StyleSheet, Text, View } from 'react-native';

import type { Meta, StoryObj } from '@storybook/react-native';
import type { WdioStory } from '@fluentui-react-native/storybook-desktop/testing';

import { Avatar } from './avatar';
import type { AvatarSize } from './avatar.types';
import { StoryStatus } from '../../common/StoryStatus.story-helpers';
import type { AvatarProps, AvatarSize } from './avatar.types';

type StoryGroupProps = {
children: ReactNode;
Expand Down Expand Up @@ -44,7 +46,7 @@ const meta: Meta<typeof Avatar> = {

export default meta;

type Story = StoryObj<typeof Avatar>;
type Story = WdioStory<StoryObj<typeof Avatar>>;

export const Default: Story = {};

Expand Down Expand Up @@ -93,13 +95,39 @@ export const ActivityRing: Story = {
activityRing: true,
initials: 'LM',
},
render: (args: AvatarProps) => (
<View style={styles.story}>
<Avatar {...args} testID="agentic-storybook-avatar-activity-ring" />
<StoryStatus testID="agentic-storybook-avatar-ring-state">{`Activity ring ${args.activityRing ? 'on' : 'off'}, size ${args.size}`}</StoryStatus>
</View>
),
parameters: {
docs: {
description: {
story: 'The optional activity ring renders as an outline offset and width that scale with the avatar size.',
story: 'The activity ring uses a persistent decorative border with a gap and stroke that scale with the avatar size.',
},
},
},
wdio: {
'toggles the activity ring at every size without crashing or changing avatar bounds': async ({ browser, desktop, expect }) => {
const assert: typeof import('node:assert') = (await import('node:assert')).default;
for (const size of [16, 20, 24, 28, 32, 40, 56, 120]) {
for (const activityRing of [false, true]) {
await desktop.session.updateStoryArgs('components-avatar--activity-ring', { activityRing, size });
await expect(await browser.$('~agentic-storybook-avatar-ring-state')).toHaveText(
`Activity ring ${activityRing ? 'on' : 'off'}, size ${size}`,
);
const avatar = await browser.$('~agentic-storybook-avatar-activity-ring');
await expect(avatar).toExist();
const bounds = await browser.getElementRect(await avatar.elementId);
assert(
Math.abs(bounds.width - size) <= 1 && Math.abs(bounds.height - size) <= 1,
'The activity ring must not resize the avatar.',
);
}
}
},
},
};

const styles = StyleSheet.create({
Expand Down
Loading
Loading