From df3355d99ceb03774b9a93ddee5b2db40aba2b2a Mon Sep 17 00:00:00 2001 From: Dmytro Kirpa Date: Wed, 6 May 2026 12:54:22 +0200 Subject: [PATCH 1/2] feat(react-headless-components-preview): add Persona component (#36102) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Makoto Morimoto --- ...-f2171a9b-8a48-4ad5-84f2-0b897f173745.json | 7 ++ .../bundle-size/AllComponents.fixture.js | 4 +- .../library/etc/persona.api.md | 40 ++++++++++++ .../library/package.json | 6 ++ .../src/components/Persona/Persona.test.tsx | 34 ++++++++++ .../src/components/Persona/Persona.tsx | 18 +++++ .../src/components/Persona/Persona.types.ts | 21 ++++++ .../library/src/components/Persona/index.ts | 4 ++ .../src/components/Persona/renderPersona.tsx | 6 ++ .../src/components/Persona/usePersona.ts | 38 +++++++++++ .../library/src/persona.ts | 2 + .../src/Persona/PersonaDefault.stories.tsx | 21 ++++++ .../stories/src/Persona/PersonaDescription.md | 1 + .../stories/src/Persona/index.stories.tsx | 19 ++++++ .../stories/src/Persona/persona.module.css | 65 +++++++++++++++++++ 15 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 change/@fluentui-react-headless-components-preview-f2171a9b-8a48-4ad5-84f2-0b897f173745.json create mode 100644 packages/react-components/react-headless-components-preview/library/etc/persona.api.md create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.test.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/index.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/renderPersona.tsx create mode 100644 packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts create mode 100644 packages/react-components/react-headless-components-preview/library/src/persona.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDefault.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDescription.md create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Persona/index.stories.tsx create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Persona/persona.module.css diff --git a/change/@fluentui-react-headless-components-preview-f2171a9b-8a48-4ad5-84f2-0b897f173745.json b/change/@fluentui-react-headless-components-preview-f2171a9b-8a48-4ad5-84f2-0b897f173745.json new file mode 100644 index 00000000000000..8e3e54fe553ed8 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-f2171a9b-8a48-4ad5-84f2-0b897f173745.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: add Persona component", + "packageName": "@fluentui/react-headless-components-preview", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js index e6052ed976c98f..14b87aef5c243b 100644 --- a/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js +++ b/packages/react-components/react-headless-components-preview/library/bundle-size/AllComponents.fixture.js @@ -14,6 +14,7 @@ import * as Input from '@fluentui/react-headless-components-preview/input'; import * as Link from '@fluentui/react-headless-components-preview/link'; import * as MessageBar from '@fluentui/react-headless-components-preview/message-bar'; import * as ProgressBar from '@fluentui/react-headless-components-preview/progress-bar'; +import * as Persona from '@fluentui/react-headless-components-preview/persona'; import * as Popover from '@fluentui/react-headless-components-preview/popover'; import * as Provider from '@fluentui/react-headless-components-preview/provider'; import * as RadioGroup from '@fluentui/react-headless-components-preview/radio-group'; @@ -48,8 +49,9 @@ console.log({ Input, Link, MessageBar, - ProgressBar, + Persona, Popover, + ProgressBar, Provider, RadioGroup, RatingDisplay, diff --git a/packages/react-components/react-headless-components-preview/library/etc/persona.api.md b/packages/react-components/react-headless-components-preview/library/etc/persona.api.md new file mode 100644 index 00000000000000..d1b5428dd4e993 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/etc/persona.api.md @@ -0,0 +1,40 @@ +## API Report File for "@fluentui/react-headless-components-preview" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AvatarBaseProps } from '@fluentui/react-avatar'; +import type { ComponentProps } from '@fluentui/react-utilities'; +import type { ComponentState } from '@fluentui/react-utilities'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { JSXElement } from '@fluentui/react-utilities/'; +import type { PersonaProps as PersonaProps_2 } from '@fluentui/react-persona'; +import type { PersonaSlots as PersonaSlots_2 } from '@fluentui/react-persona'; +import type { PersonaState as PersonaState_2 } from '@fluentui/react-persona'; +import type * as React_2 from 'react'; +import type { Slot } from '@fluentui/react-utilities'; + +// @public +export const Persona: ForwardRefComponent; + +// @public +export type PersonaProps = ComponentProps & Pick; + +// @public (undocumented) +export type PersonaSlots = Omit & { + avatar?: Slot; +}; + +// @public +export type PersonaState = ComponentState & Pick; + +// @public (undocumented) +export const renderPersona: (state: PersonaState) => JSXElement; + +// @public +export const usePersona: (props: PersonaProps, ref: React_2.Ref) => PersonaState; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 3575cae4292a41..a14d253f4f5b30 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -159,6 +159,12 @@ "import": "./lib/message-bar.js", "require": "./lib-commonjs/message-bar.js" }, + "./persona": { + "types": "./dist/persona.d.ts", + "node": "./lib-commonjs/persona.js", + "import": "./lib/persona.js", + "require": "./lib-commonjs/persona.js" + }, "./popover": { "types": "./dist/popover.d.ts", "node": "./lib-commonjs/popover.js", diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.test.tsx new file mode 100644 index 00000000000000..1157334274ca08 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.test.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Persona } from './Persona'; + +describe('Persona', () => { + isConformant({ + Component: Persona, + displayName: 'Persona', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render( + , + ); + + expect(getByRole('img', { name: 'John Doe' })).toBeInTheDocument(); + expect(getByText('John Doe')).toBeInTheDocument(); + expect(getByText('Software Engineer')).toBeInTheDocument(); + }); + + it('renders the avatar after the text when textPosition is before', () => { + const { container, getByText, getByRole } = render( + , + ); + + const root = container.firstElementChild as HTMLElement; + const primaryText = getByText('John Doe'); + const avatar = getByRole('img', { name: 'John Doe' }); + + expect(root.firstElementChild).toBe(primaryText); + expect(root.lastElementChild).toBe(avatar); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.tsx new file mode 100644 index 00000000000000..d52df0507adeaa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { usePersona } from './usePersona'; +import { renderPersona } from './renderPersona'; +import type { PersonaProps } from './Persona.types'; + +/** + * Represents a person or with an avatar, primary text, and optional secondary text. + */ +export const Persona: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = usePersona(props, ref); + + return renderPersona(state); +}); + +Persona.displayName = 'Persona'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts new file mode 100644 index 00000000000000..0e2b786135191b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts @@ -0,0 +1,21 @@ +import type { + PersonaSlots as PersonaBaseSlots, + PersonaProps as PersonaBaseProps, + PersonaState as PersonaBaseState, +} from '@fluentui/react-persona'; +import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; +import type { Avatar } from '../Avatar'; + +export type PersonaSlots = Omit & { + avatar?: Slot; +}; + +/** + * Persona Props + */ +export type PersonaProps = ComponentProps & Pick; + +/** + * State used in rendering Persona + */ +export type PersonaState = ComponentState & Pick; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Persona/index.ts new file mode 100644 index 00000000000000..de25a0319397cc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/index.ts @@ -0,0 +1,4 @@ +export { Persona } from './Persona'; +export type { PersonaSlots, PersonaProps, PersonaState } from './Persona.types'; +export { renderPersona } from './renderPersona'; +export { usePersona } from './usePersona'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/renderPersona.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Persona/renderPersona.tsx new file mode 100644 index 00000000000000..29bcb44309215e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/renderPersona.tsx @@ -0,0 +1,6 @@ +import { renderPersona_unstable } from '@fluentui/react-persona'; + +import type { PersonaState } from './Persona.types'; +import type { JSXElement } from '@fluentui/react-utilities/'; + +export const renderPersona = renderPersona_unstable as (state: PersonaState) => JSXElement; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts b/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts new file mode 100644 index 00000000000000..af4f72ec262ef4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts @@ -0,0 +1,38 @@ +'use client'; + +import type * as React from 'react'; +import { usePersonaBase_unstable } from '@fluentui/react-persona'; +import { slot } from '@fluentui/react-utilities'; +import type { PersonaProps, PersonaState } from './Persona.types'; +import { Avatar } from '../Avatar'; + +/** + * Create the state required to render Persona. + * + * The returned state can be modified with hooks such as usePersonaStyles_unstable, + * before being passed to renderPersona_unstable. + * + * @param props - props from this instance of Persona + * @param ref - reference to root HTMLDivElement of Persona + */ +export const usePersona = (props: PersonaProps, ref: React.Ref): PersonaState => { + const { textPosition = 'after', ...baseProps } = props; + const baseState = usePersonaBase_unstable(baseProps, ref); + + return { + ...baseState, + textPosition, + components: { + // eslint-disable-next-line @typescript-eslint/no-deprecated + ...baseState.components, + avatar: Avatar, + }, + avatar: slot.optional(props.avatar, { + renderByDefault: true, + defaultProps: { + name: props.name, + }, + elementType: Avatar, + }), + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/persona.ts b/packages/react-components/react-headless-components-preview/library/src/persona.ts new file mode 100644 index 00000000000000..f592ab528aac63 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/persona.ts @@ -0,0 +1,2 @@ +export { Persona, renderPersona, usePersona } from './components/Persona'; +export type { PersonaSlots, PersonaProps, PersonaState } from './components/Persona'; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDefault.stories.tsx new file mode 100644 index 00000000000000..44ce832b0a7ffa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDefault.stories.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { Persona } from '@fluentui/react-headless-components-preview/persona'; + +import styles from './persona.module.css'; + +export const Default = (): React.ReactNode => ( + +); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDescription.md b/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDescription.md new file mode 100644 index 00000000000000..7750162b201c33 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Persona/PersonaDescription.md @@ -0,0 +1 @@ +A `Persona` is a visual representation of a person that showcases an avatar, primary text, and optional secondary text. diff --git a/packages/react-components/react-headless-components-preview/stories/src/Persona/index.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Persona/index.stories.tsx new file mode 100644 index 00000000000000..fcca2f6fc350b9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Persona/index.stories.tsx @@ -0,0 +1,19 @@ +import { Avatar } from '@fluentui/react-headless-components-preview/avatar'; +import { Persona } from '@fluentui/react-headless-components-preview/persona'; + +import descriptionMd from './PersonaDescription.md'; + +export { Default } from './PersonaDefault.stories'; + +export default { + title: 'Headless Components/Persona', + component: Persona, + subcomponent: { Avatar }, + parameters: { + docs: { + description: { + component: descriptionMd, + }, + }, + }, +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Persona/persona.module.css b/packages/react-components/react-headless-components-preview/stories/src/Persona/persona.module.css new file mode 100644 index 00000000000000..9a9c7550b50fd4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/stories/src/Persona/persona.module.css @@ -0,0 +1,65 @@ +.persona { + display: inline-grid; + grid-auto-rows: max-content; + grid-auto-flow: column; + justify-items: start; + grid-template-columns: max-content [middle] auto; + column-gap: var(--space-2); +} + +/* Avatar - grid column spanning and positioning */ +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--accent); + color: var(--accent-contrast); + font-weight: 600; + position: relative; + overflow: hidden; + flex-shrink: 0; + user-select: none; + grid-row: span 2; + align-self: center; +} + +/* Avatar initials */ +.avatarInitials { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; +} + +/* Avatar image */ +.avatarImage { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Primary text - body1 typography */ +.primaryText { + display: block; + font-size: 14px; + line-height: 20px; + color: var(--text); + font-weight: 400; +} + +/* Secondary text - caption1 typography */ +.secondaryText { + display: block; + font-size: 12px; + line-height: 16px; + color: var(--text-muted); + font-weight: 400; +} From 77aee0eba2a5e83f4fbb0da39aa5c085d939b829 Mon Sep 17 00:00:00 2001 From: Victor Genaev Date: Wed, 6 May 2026 13:03:53 +0200 Subject: [PATCH 2/2] docs(headless): adopt Popover stories to css modules (#36103) --- .../PositioningCoverTarget.stories.tsx | 79 ++-- .../PositioningDefault.stories.tsx | 10 +- .../PositioningFallbackPositions.stories.tsx | 74 ++-- .../PositioningFlippingBlock.stories.tsx | 36 +- .../PositioningFlippingCorner.stories.tsx | 40 +-- .../PositioningFlippingInline.stories.tsx | 36 +- .../PositioningMatchTargetSize.stories.tsx | 16 +- .../Positioning/PositioningOffset.stories.tsx | 40 +-- .../PositioningShorthandPositions.stories.tsx | 79 ++-- .../src/Concepts/Positioning/demoBox.ts | 21 -- .../Positioning/positioning.module.css | 340 ++++++++++++++++++ .../Concepts/Positioning/utils.stories.tsx | 20 +- .../stories/src/Popover/popover.module.css | 15 +- 13 files changed, 538 insertions(+), 268 deletions(-) delete mode 100644 packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/demoBox.ts create mode 100644 packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/positioning.module.css diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningCoverTarget.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningCoverTarget.stories.tsx index 6ecac5e95d9534..eb1d5b9bfb0af8 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningCoverTarget.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningCoverTarget.stories.tsx @@ -2,48 +2,61 @@ import * as React from 'react'; import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview/popover'; import type { PositioningProps } from '@fluentui/react-headless-components-preview/positioning'; -const classes = { - outer: 'w-full overflow-auto', - wrapper: 'grid grid-cols-[repeat(3,auto)] grid-rows-[repeat(5,auto)] gap-16 mx-32 my-16 w-max', - trigger: - 'h-12 w-32 flex items-center justify-center px-3 rounded-md bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 cursor-pointer border-none', - surface: - 'bg-white/95 rounded-lg shadow-lg border border-gray-200 px-4 py-3 text-sm w-56 h-28 flex items-center justify-center', -}; +import styles from './positioning.module.css'; -const cells: Array<{ +const options: Array<{ label: string; position: NonNullable; align: NonNullable; - gridClass: string; }> = [ - { label: 'above-start', position: 'above', align: 'start', gridClass: 'row-start-1 col-start-1' }, - { label: 'above', position: 'above', align: 'center', gridClass: 'row-start-1 col-start-2' }, - { label: 'above-end', position: 'above', align: 'end', gridClass: 'row-start-1 col-start-3' }, - { label: 'before-top', position: 'before', align: 'start', gridClass: 'row-start-2 col-start-1' }, - { label: 'before', position: 'before', align: 'center', gridClass: 'row-start-3 col-start-1' }, - { label: 'before-bottom', position: 'before', align: 'end', gridClass: 'row-start-4 col-start-1' }, - { label: 'after-top', position: 'after', align: 'start', gridClass: 'row-start-2 col-start-3' }, - { label: 'after', position: 'after', align: 'center', gridClass: 'row-start-3 col-start-3' }, - { label: 'after-bottom', position: 'after', align: 'end', gridClass: 'row-start-4 col-start-3' }, - { label: 'below-start', position: 'below', align: 'start', gridClass: 'row-start-5 col-start-1' }, - { label: 'below', position: 'below', align: 'center', gridClass: 'row-start-5 col-start-2' }, - { label: 'below-end', position: 'below', align: 'end', gridClass: 'row-start-5 col-start-3' }, + { label: 'above-start', position: 'above', align: 'start' }, + { label: 'above', position: 'above', align: 'center' }, + { label: 'above-end', position: 'above', align: 'end' }, + { label: 'before-top', position: 'before', align: 'start' }, + { label: 'before', position: 'before', align: 'center' }, + { label: 'before-bottom', position: 'before', align: 'end' }, + { label: 'after-top', position: 'after', align: 'start' }, + { label: 'after', position: 'after', align: 'center' }, + { label: 'after-bottom', position: 'after', align: 'end' }, + { label: 'below-start', position: 'below', align: 'start' }, + { label: 'below', position: 'below', align: 'center' }, + { label: 'below-end', position: 'below', align: 'end' }, ]; -export const CoverTarget = (): React.ReactNode => ( -
-
- {cells.map(cell => ( -
- +export const CoverTarget = (): React.ReactNode => { + const [selected, setSelected] = React.useState(options[1]); + + return ( +
+
+ +
+ - + - Container + Container
- ))} +
-
-); + ); +}; diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningDefault.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningDefault.stories.tsx index 39994731f56813..cf486690a55fd0 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningDefault.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningDefault.stories.tsx @@ -2,18 +2,14 @@ import * as React from 'react'; import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview/popover'; import type { PositioningProps } from '@fluentui/react-headless-components-preview/positioning'; -const classes = { - trigger: - 'px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 min-w-[160px]', -}; +import styles from './positioning.module.css'; export const Default = (props: PositioningProps): React.ReactNode => ( - + - Container + Container ); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx index be66959f6b0e8f..7e2f9d43437d62 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFallbackPositions.stories.tsx @@ -1,42 +1,26 @@ import * as React from 'react'; -import { demoBoxClass, demoBoxStyle, flipDemoSurfaceCss } from './demoBox'; import { InlineAnchored } from './InlineAnchored'; -const classes = { - page: 'flex flex-col gap-6 p-4', - sectionTitle: 'text-sm font-semibold text-gray-800 mb-1', - sectionNote: 'text-xs text-gray-500 max-w-xl mb-2', - grid: 'grid grid-cols-1 sm:grid-cols-2 gap-4', - trigger: - 'px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'flip-demo bg-white rounded-md shadow-md border border-gray-200 p-3 min-w-[200px] max-w-xs text-sm', -}; +import styles from './positioning.module.css'; export const FallbackPositions = (): React.ReactNode => ( -
- - -
-

Basic fallback

-

+

+
+

Basic fallback

+

Primary above overflows the box → first fallback below-start fits → surface renders there.

-
+
- trigger near top - + } > Requested: above · fallbacks: below-start, after @@ -44,25 +28,23 @@ export const FallbackPositions = (): React.ReactNode => (
-
-

Chain walking

-

+

+

Chain walking

+

Trigger pinned to top-left. Primary above overflows, first fallback before also overflows (no room to the left), so the browser falls through to below. The live{' '} Actual readout should read below.

-
+
- top-left trigger - + } > Requested: above · fallbacks: before, below @@ -70,43 +52,35 @@ export const FallbackPositions = (): React.ReactNode => (
-
-

Custom chain replaces default flip

-

+

+

Custom chain replaces default flip

+

Same overflow condition, different chains. Left popover has no fallbackPositions → default{' '} flip-block, flip-inline fires → surface ends up below. Right popover passes ['after']{' '} → custom chain replaces defaults → surface goes to the right instead of flipping.

-
-
+
+
- default (flip) - + } > Requested: above · no custom fallbacks
-
+
+ } diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingBlock.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingBlock.stories.tsx index dd09c15be9abb1..ff3b01731f2567 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingBlock.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingBlock.stories.tsx @@ -1,48 +1,30 @@ import * as React from 'react'; -import { demoBoxClass, demoBoxStyle, flipDemoSurfaceCss } from './demoBox'; import { InlineAnchored } from './InlineAnchored'; import descriptionMd from './PositioningFlippingBlockDescription.md'; - -const classes = { - page: 'flex flex-col gap-4 p-4', - grid: 'grid grid-cols-1 sm:grid-cols-2 gap-4', - trigger: - 'px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'flip-demo bg-white rounded-md shadow-md border border-gray-200 p-2 w-[160px] text-xs', -}; +import styles from './positioning.module.css'; export const FlippingBlock = (): React.ReactNode => ( -
- - -
-
+
+
+
- trigger near top - + } > Requested: above → flips below
-
+
+ } diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingCorner.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingCorner.stories.tsx index 4325ed1db1cc19..d5f3f1061aaf58 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingCorner.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingCorner.stories.tsx @@ -1,28 +1,18 @@ import * as React from 'react'; -import { demoBoxClass, demoBoxStyle, flipDemoSurfaceCss } from './demoBox'; import { InlineAnchored } from './InlineAnchored'; import descriptionMd from './PositioningFlippingCornerDescription.md'; - -const classes = { - page: 'flex flex-col gap-4 p-4', - grid: 'grid grid-cols-1 sm:grid-cols-2 gap-4', - trigger: - 'px-3 py-1.5 rounded-md bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'flip-demo bg-white rounded-md shadow-md border border-gray-200 p-3 w-[320px] max-h-[140px] text-sm', -}; +import styles from './positioning.module.css'; export const FlippingCorner = (): React.ReactNode => ( -
- - -
-
+
+
+
+ } @@ -31,12 +21,12 @@ export const FlippingCorner = (): React.ReactNode => (
-
+
+ } @@ -45,12 +35,12 @@ export const FlippingCorner = (): React.ReactNode => (
-
+
+ } @@ -59,12 +49,12 @@ export const FlippingCorner = (): React.ReactNode => (
-
+
+ } diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingInline.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingInline.stories.tsx index 97e24ff2b869be..9326fdf3f937c8 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingInline.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningFlippingInline.stories.tsx @@ -1,48 +1,30 @@ import * as React from 'react'; -import { demoBoxClass, demoBoxStyle, flipDemoSurfaceCss } from './demoBox'; import { InlineAnchored } from './InlineAnchored'; import descriptionMd from './PositioningFlippingInlineDescription.md'; - -const classes = { - page: 'flex flex-col gap-4 p-4', - grid: 'grid grid-cols-1 sm:grid-cols-2 gap-4', - trigger: - 'px-3 py-1.5 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'flip-demo bg-white rounded-md shadow-md border border-gray-200 p-2 w-[160px] text-xs', -}; +import styles from './positioning.module.css'; export const FlippingInline = (): React.ReactNode => ( -
- - -
-
+
+
+
- trigger on left - + } > Requested: before → flips after
-
+
+ } diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx index 337b98b8a2fb6e..dfd2e812a21755 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningMatchTargetSize.stories.tsx @@ -2,21 +2,17 @@ import * as React from 'react'; import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview/popover'; import descriptionMd from './PositioningMatchTargetSizeDescription.md'; - -const classes = { - wrapper: 'flex flex-col items-start gap-4 m-16', - trigger: - 'w-[350px] px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'bg-white rounded-lg shadow-lg border border-gray-200 p-4 box-border', -}; +import styles from './positioning.module.css'; export const MatchTargetSize = (): React.ReactNode => ( -
+
- + - This popover has the same width as its target anchor + + This popover has the same width as its target anchor +
); diff --git a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningOffset.stories.tsx b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningOffset.stories.tsx index 3d4c2afe951fe2..996a8a0c8d6721 100644 --- a/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningOffset.stories.tsx +++ b/packages/react-components/react-headless-components-preview/stories/src/Concepts/Positioning/PositioningOffset.stories.tsx @@ -1,61 +1,51 @@ import * as React from 'react'; import { Popover, PopoverTrigger, PopoverSurface } from '@fluentui/react-headless-components-preview/popover'; -const classes = { - outer: 'w-full overflow-auto', - wrapper: 'flex flex-col items-start gap-6 mx-32 my-16 w-max', - row: 'flex items-center gap-3 text-sm text-gray-700', - input: 'w-20 px-2 py-1 border border-gray-300 rounded', - trigger: - 'inline-flex w-fit px-4 py-2 rounded-md bg-blue-600 text-white font-medium hover:bg-blue-700 data-[open]:bg-blue-700 focus-visible:outline-2 focus-visible:outline-blue-500 focus-visible:outline-offset-2 cursor-pointer border-none', - surface: 'bg-white rounded-lg shadow-lg border border-gray-200 px-3 py-2 text-sm w-40', - group: 'flex flex-col items-start gap-2', - label: 'text-xs font-semibold text-gray-500 uppercase tracking-wide', -}; +import styles from './positioning.module.css'; export const Offset = (): React.ReactNode => { const [mainAxis, setMainAxis] = React.useState(10); const [crossAxis, setCrossAxis] = React.useState(0); return ( -
-
-