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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "feat: add Persona component",
"packageName": "@fluentui/react-headless-components-preview",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,8 +49,9 @@ console.log({
Input,
Link,
MessageBar,
ProgressBar,
Persona,
Popover,
ProgressBar,
Provider,
RadioGroup,
RatingDisplay,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PersonaProps>;

// @public
export type PersonaProps = ComponentProps<PersonaSlots> & Pick<PersonaProps_2, 'name' | 'textPosition'>;

// @public (undocumented)
export type PersonaSlots = Omit<PersonaSlots_2, 'avatar' | 'presence'> & {
avatar?: Slot<typeof Avatar>;
};

// @public
export type PersonaState = ComponentState<PersonaSlots> & Pick<PersonaState_2, 'textPosition' | 'numTextLines'>;

// @public (undocumented)
export const renderPersona: (state: PersonaState) => JSXElement;

// @public
export const usePersona: (props: PersonaProps, ref: React_2.Ref<HTMLDivElement>) => PersonaState;

// (No @packageDocumentation comment for this package)

```
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<Persona name="John Doe" primaryText="John Doe" secondaryText="Software Engineer" />,
);

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(
<Persona name="John Doe" primaryText="John Doe" secondaryText="Software Engineer" textPosition="before" />,
);

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);
});
});
Original file line number Diff line number Diff line change
@@ -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<PersonaProps> = React.forwardRef((props, ref) => {
const state = usePersona(props, ref);

return renderPersona(state);
});

Persona.displayName = 'Persona';
Original file line number Diff line number Diff line change
@@ -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<PersonaBaseSlots, 'avatar' | 'presence'> & {
avatar?: Slot<typeof Avatar>;
};

/**
* Persona Props
*/
export type PersonaProps = ComponentProps<PersonaSlots> & Pick<PersonaBaseProps, 'name' | 'textPosition'>;

/**
* State used in rendering Persona
*/
export type PersonaState = ComponentState<PersonaSlots> & Pick<PersonaBaseState, 'textPosition' | 'numTextLines'>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Persona } from './Persona';
export type { PersonaSlots, PersonaProps, PersonaState } from './Persona.types';
export { renderPersona } from './renderPersona';
export { usePersona } from './usePersona';
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>): 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,
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Persona, renderPersona, usePersona } from './components/Persona';
export type { PersonaSlots, PersonaProps, PersonaState } from './components/Persona';
Original file line number Diff line number Diff line change
Expand Up @@ -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<PositioningProps['position']>;
align: NonNullable<PositioningProps['align']>;
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 => (
<div className={classes.outer}>
<div className={classes.wrapper}>
{cells.map(cell => (
<div key={cell.label} className={cell.gridClass}>
<Popover positioning={{ position: cell.position, align: cell.align, coverTarget: true }}>
export const CoverTarget = (): React.ReactNode => {
const [selected, setSelected] = React.useState(options[1]);

return (
<div className={styles.outer}>
<div className={styles.layout}>
<label className={styles.controls}>
Position
<select
className={styles.select}
value={selected.label}
onChange={e => {
const next = options.find(o => o.label === e.target.value);
if (next) {
setSelected(next);
}
}}
>
{options.map(option => (
<option key={option.label} value={option.label}>
{option.label}
</option>
))}
</select>
</label>
<div className={styles.stage}>
<Popover open positioning={{ position: selected.position, align: selected.align, coverTarget: true }}>
<PopoverTrigger>
<button className={classes.trigger}>{cell.label}</button>
<button className={`${styles.trigger} ${styles.triggerFixed}`}>{selected.label}</button>
</PopoverTrigger>
<PopoverSurface className={classes.surface}>Container</PopoverSurface>
<PopoverSurface className={styles.surfaceCover}>Container</PopoverSurface>
</Popover>
</div>
))}
</div>
</div>
</div>
);
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 => (
<Popover positioning={props}>
<PopoverTrigger>
<button className={classes.trigger}>Click me</button>
<button className={styles.trigger}>Click me</button>
</PopoverTrigger>
<PopoverSurface className={classes.surface}>Container</PopoverSurface>
<PopoverSurface className={styles.surfaceCallout}>Container</PopoverSurface>
</Popover>
);

Expand Down
Loading