diff --git a/packages/react-components/react-tags/bundle-size/tag.fixture.js b/packages/react-components/react-tags/bundle-size/tag.fixture.js new file mode 100644 index 00000000000000..3e1a566ef404c8 --- /dev/null +++ b/packages/react-components/react-tags/bundle-size/tag.fixture.js @@ -0,0 +1,7 @@ +import { Tag } from '@fluentui/react-tags'; + +console.log(Tag); + +export default { + name: 'Tag', +}; diff --git a/packages/react-components/react-tags/etc/react-tags.api.md b/packages/react-components/react-tags/etc/react-tags.api.md index 659e2b81f72266..c8e561dc6958cd 100644 --- a/packages/react-components/react-tags/etc/react-tags.api.md +++ b/packages/react-components/react-tags/etc/react-tags.api.md @@ -6,7 +6,8 @@ /// -import { Avatar } from '@fluentui/react-avatar'; +import { AvatarShape } from '@fluentui/react-avatar'; +import { AvatarSize } 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'; @@ -15,10 +16,10 @@ import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; // @public -export const renderTag_unstable: (state: TagState) => JSX.Element; +export const renderTag_unstable: (state: TagState, contextValues: TagContextValues) => JSX.Element; // @public -export const renderTagButton_unstable: (state: TagButtonState) => JSX.Element; +export const renderTagButton_unstable: (state: TagButtonState, contextValues: TagButtonContextValues) => JSX.Element; // @public export const Tag: ForwardRefComponent; @@ -30,34 +31,19 @@ export const TagButton: ForwardRefComponent; export const tagButtonClassNames: SlotClassNames; // @public -export type TagButtonProps = ComponentProps & { - size?: 'extra-small' | 'small' | 'medium'; - shape?: 'rounded' | 'circular'; - appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline'; - disabled?: boolean; - checked?: boolean; - dismissable?: boolean; -}; +export type TagButtonProps = TagProps; // @public (undocumented) -export type TagButtonSlots = { - root: NonNullable>; - contentButton?: Slot<'button'>; - avatar?: Slot; - icon?: Slot<'span'>; - primaryText?: Slot<'span'>; - secondaryText?: Slot<'span'>; - dismissButton?: NonNullable>; -}; +export type TagButtonSlots = TagSlots; // @public -export type TagButtonState = ComponentState & Required>; +export type TagButtonState = TagState; // @public (undocumented) export const tagClassNames: SlotClassNames; // @public -export type TagProps = ComponentProps & { +export type TagProps = ComponentProps> & { size?: 'extra-small' | 'small' | 'medium'; shape?: 'rounded' | 'circular'; appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline'; @@ -69,16 +55,19 @@ export type TagProps = ComponentProps & { // @public (undocumented) export type TagSlots = { root: NonNullable>; - content?: Slot<'span'>; - avatar?: Slot; - icon?: Slot<'span'>; - primaryText?: Slot<'span'>; - secondaryText?: Slot<'span'>; - dismissButton?: NonNullable>; + media: Slot<'span'>; + content: Slot<'div'>; + icon: Slot<'span'>; + primaryText: Slot<'span'>; + secondaryText: Slot<'span'>; + dismissButton: Slot<'button'>; }; // @public -export type TagState = ComponentState & Required>; +export type TagState = ComponentState & Required & { + avatarSize: AvatarSize | undefined; + avatarShape: AvatarShape | undefined; +}>; // @public export const useTag_unstable: (props: TagProps, ref: React_2.Ref) => TagState; diff --git a/packages/react-components/react-tags/package.json b/packages/react-components/react-tags/package.json index 6df62172ac1f7f..0fa7345db834d9 100644 --- a/packages/react-components/react-tags/package.json +++ b/packages/react-components/react-tags/package.json @@ -14,6 +14,7 @@ "license": "MIT", "scripts": { "build": "just-scripts build", + "bundle-size": "bundle-size measure", "clean": "just-scripts clean", "code-style": "just-scripts code-style", "just": "just-scripts", @@ -32,9 +33,11 @@ "@fluentui/scripts-tasks": "*" }, "dependencies": { + "@fluentui/react-aria": "^9.3.18", "@fluentui/react-avatar": "^9.4.10", "@fluentui/react-icons": "^2.0.196", "@fluentui/react-jsx-runtime": "9.0.0-alpha.2", + "@fluentui/react-tabster": "^9.6.5", "@fluentui/react-theme": "^9.1.7", "@fluentui/react-utilities": "^9.8.0", "@griffel/react": "^1.5.2", diff --git a/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx b/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx index f883681c531f31..480342e78bece2 100644 --- a/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx +++ b/packages/react-components/react-tags/src/components/Tag/Tag.test.tsx @@ -1,12 +1,8 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; import { Tag } from './Tag'; import { isConformant } from '../../testing/isConformant'; const requiredProps = { - avatar: { - name: 'Katri Athokas', - }, + media: 'media', icon: 'i', primaryText: 'Primary text', secondaryText: 'Secondary text', @@ -19,11 +15,4 @@ describe('Tag', () => { displayName: 'Tag', requiredProps, }); - - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - - it('renders a default state', () => { - const result = render(Default Tag); - expect(result.container).toMatchSnapshot(); - }); }); diff --git a/packages/react-components/react-tags/src/components/Tag/Tag.tsx b/packages/react-components/react-tags/src/components/Tag/Tag.tsx index c1bb93011ef86a..30cded29f93faa 100644 --- a/packages/react-components/react-tags/src/components/Tag/Tag.tsx +++ b/packages/react-components/react-tags/src/components/Tag/Tag.tsx @@ -4,6 +4,7 @@ import { renderTag_unstable } from './renderTag'; import { useTagStyles_unstable } from './useTagStyles.styles'; import type { TagProps } from './Tag.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useTagContextValues_unstable } from '../../utils/useTagContextValues'; /** * Tag component - TODO: add more docs @@ -12,7 +13,7 @@ export const Tag: ForwardRefComponent = React.forwardRef((props, ref) const state = useTag_unstable(props, ref); useTagStyles_unstable(state); - return renderTag_unstable(state); + return renderTag_unstable(state, useTagContextValues_unstable(state)); }); Tag.displayName = 'Tag'; diff --git a/packages/react-components/react-tags/src/components/Tag/Tag.types.ts b/packages/react-components/react-tags/src/components/Tag/Tag.types.ts index 8cffeea46be0d1..9d130328b633c0 100644 --- a/packages/react-components/react-tags/src/components/Tag/Tag.types.ts +++ b/packages/react-components/react-tags/src/components/Tag/Tag.types.ts @@ -1,20 +1,45 @@ +import { AvatarSize, AvatarShape } from '@fluentui/react-avatar'; import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { Avatar } from '@fluentui/react-avatar'; + +export type TagContextValues = { + avatar: { + size?: AvatarSize; + shape?: AvatarShape; + }; +}; export type TagSlots = { root: NonNullable>; - content?: Slot<'span'>; - avatar?: Slot; - icon?: Slot<'span'>; - primaryText?: Slot<'span'>; - secondaryText?: Slot<'span'>; - dismissButton?: NonNullable>; + + /** + * Slot for an icon or other visual element + */ + media: Slot<'span'>; + + /** + * A layout wrapper for the icon slot, the primaryText and secondaryText slots + */ + content: Slot<'div'>; + + icon: Slot<'span'>; + + /** + * Main text for the Tag. Children of the root slot are automatically rendered here + */ + primaryText: Slot<'span'>; + + /** + * Secondary text that describes or complements the main text + */ + secondaryText: Slot<'span'>; + + dismissButton: Slot<'button'>; }; /** * Tag Props */ -export type TagProps = ComponentProps & { +export type TagProps = ComponentProps> & { size?: 'extra-small' | 'small' | 'medium'; shape?: 'rounded' | 'circular'; appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline'; @@ -27,4 +52,9 @@ export type TagProps = ComponentProps & { * State used in rendering Tag */ export type TagState = ComponentState & - Required>; + Required< + Pick & { + avatarSize: AvatarSize | undefined; + avatarShape: AvatarShape | undefined; + } + >; diff --git a/packages/react-components/react-tags/src/components/Tag/__snapshots__/Tag.test.tsx.snap b/packages/react-components/react-tags/src/components/Tag/__snapshots__/Tag.test.tsx.snap deleted file mode 100644 index f66f95a0e95775..00000000000000 --- a/packages/react-components/react-tags/src/components/Tag/__snapshots__/Tag.test.tsx.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Tag renders a default state 1`] = ` -
-
- - - - KA - - - - i - - - Primary text - - - Secondary text - - - -
-
-`; diff --git a/packages/react-components/react-tags/src/components/Tag/renderTag.tsx b/packages/react-components/react-tags/src/components/Tag/renderTag.tsx index ab3e1d6e61433b..6e382ca446cb48 100644 --- a/packages/react-components/react-tags/src/components/Tag/renderTag.tsx +++ b/packages/react-components/react-tags/src/components/Tag/renderTag.tsx @@ -4,22 +4,28 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { getSlotsNext } from '@fluentui/react-utilities'; -import type { TagState, TagSlots } from './Tag.types'; +import type { TagState, TagSlots, TagContextValues } from './Tag.types'; +import { AvatarContextProvider } from '@fluentui/react-avatar'; /** * Render the final JSX of Tag */ -export const renderTag_unstable = (state: TagState) => { +export const renderTag_unstable = (state: TagState, contextValues: TagContextValues) => { const { slots, slotProps } = getSlotsNext(state); - // TODO Add additional slots in the appropriate place return ( {slots.content && ( - {slots.avatar && } + {slots.media && ( + + + + )} {slots.icon && } - {slots.primaryText && } + {slots.primaryText && ( + {slotProps.root.children} + )} {slots.secondaryText && } )} diff --git a/packages/react-components/react-tags/src/components/Tag/useTag.tsx b/packages/react-components/react-tags/src/components/Tag/useTag.tsx index 0965e8455d9712..35f804d5c4c2a5 100644 --- a/packages/react-components/react-tags/src/components/Tag/useTag.tsx +++ b/packages/react-components/react-tags/src/components/Tag/useTag.tsx @@ -1,8 +1,21 @@ import * as React from 'react'; import { getNativeElementProps, resolveShorthand } from '@fluentui/react-utilities'; -import { Dismiss16Filled } from '@fluentui/react-icons'; -import { Avatar } from '@fluentui/react-avatar'; +import { DismissRegular, bundleIcon, DismissFilled } from '@fluentui/react-icons'; import type { TagProps, TagState } from './Tag.types'; +import { useARIAButtonShorthand } from '@fluentui/react-aria'; + +const tagAvatarSizeMap = { + medium: 28, + small: 24, + 'extra-small': 20, +} as const; + +const tagAvatarShapeMap = { + rounded: 'square', + circular: 'circular', +} as const; + +const DismissIcon = bundleIcon(DismissFilled, DismissRegular); /** * Create the state required to render Tag. @@ -26,8 +39,8 @@ export const useTag_unstable = (props: TagProps, ref: React.Ref): T return { components: { root: 'div', - content: 'span', - avatar: Avatar, + content: 'div', + media: 'span', icon: 'span', primaryText: 'span', secondaryText: 'span', @@ -43,17 +56,21 @@ export const useTag_unstable = (props: TagProps, ref: React.Ref): T ref, ...props, }), + avatarSize: tagAvatarSizeMap[size], + avatarShape: tagAvatarShapeMap[shape], + media: resolveShorthand(props.media), + content: resolveShorthand(props.content, { required: true }), - avatar: resolveShorthand(props.avatar), icon: resolveShorthand(props.icon), - primaryText: resolveShorthand(props.primaryText), + primaryText: resolveShorthand(props.primaryText, { required: true }), secondaryText: resolveShorthand(props.secondaryText), - dismissButton: resolveShorthand(props.dismissButton, { - required: true, + + dismissButton: useARIAButtonShorthand(props.dismissButton, { + required: props.dismissable, defaultProps: { disabled, type: 'button', - children: , + children: , }, }), }; diff --git a/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts b/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts index d388013ad1ebeb..1edf60c2352c94 100644 --- a/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts +++ b/packages/react-components/react-tags/src/components/Tag/useTagStyles.styles.ts @@ -1,11 +1,13 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { TagSlots, TagState } from './Tag.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { tokens, typographyStyles } from '@fluentui/react-theme'; +import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; export const tagClassNames: SlotClassNames = { root: 'fui-Tag', content: 'fui-Tag__content', - avatar: 'fui-Tag__avatar', + media: 'fui-Tag__media', icon: 'fui-Tag__icon', primaryText: 'fui-Tag__primaryText', secondaryText: 'fui-Tag__secondaryText', @@ -15,68 +17,161 @@ export const tagClassNames: SlotClassNames = { /** * Styles for the root slot */ -const useStyles = makeStyles({ +export const useTagBaseStyles = makeStyles({ root: { + // TODO use makeResetStyle when styles are settled display: 'inline-flex', + height: '32px', + width: 'fit-content', + ...shorthands.borderRadius(tokens.borderRadiusMedium), + + backgroundColor: tokens.colorNeutralBackground3, + color: tokens.colorNeutralForeground2, + ...shorthands.border(tokens.strokeWidthThin, 'solid', tokens.colorTransparentStroke), + }, + rootCircular: { + ...shorthands.borderRadius(tokens.borderRadiusCircular), + }, + + media: { + ...shorthands.gridArea('media'), + alignSelf: 'center', + paddingLeft: tokens.spacingHorizontalXXS, + paddingRight: tokens.spacingHorizontalS, }, + content: { display: 'inline-grid', - gridTemplateColumns: 'auto 8px auto auto 8px auto', gridTemplateRows: '1fr auto auto 1fr', gridTemplateAreas: ` - "avatar . icon . ." - "avatar . icon primary ." - "avatar . icon secondary ." - "avatar . icon . ." + "media . " + "media primary " + "media secondary" + "media . " `, + paddingRight: tokens.spacingHorizontalS, }, - avatar: { - alignSelf: 'center', - ...shorthands.gridArea('avatar'), + textOnlyContent: { + paddingLeft: tokens.spacingHorizontalS, }, + icon: { + display: 'flex', alignSelf: 'center', - ...shorthands.gridArea('icon'), + ...shorthands.gridArea('media'), + paddingLeft: '6px', + paddingRight: '2px', + }, + primaryText: { + gridColumnStart: 'primary', + gridRowStart: 'primary', + gridRowEnd: 'secondary', + ...typographyStyles.body1, + paddingLeft: tokens.spacingHorizontalXXS, + paddingRight: tokens.spacingHorizontalXXS, + }, + primaryTextWithSecondaryText: { + ...shorthands.gridArea('primary'), + ...typographyStyles.caption1, + }, + secondaryText: { + ...shorthands.gridArea('secondary'), + paddingLeft: tokens.spacingHorizontalXXS, + paddingRight: tokens.spacingHorizontalXXS, + ...typographyStyles.caption2, + }, + + resetButton: { + boxSizing: 'content-box', + color: 'inherit', + fontFamily: 'inherit', + lineHeight: 'normal', + ...shorthands.overflow('visible'), + ...shorthands.padding(0), + ...shorthands.borderStyle('none'), + appearance: 'button', + textAlign: 'unset', + }, + dismissButton: { + ...shorthands.padding('0px'), + backgroundColor: 'transparent', + width: '20px', + display: 'flex', + alignItems: 'center', + fontSize: '20px', + + ...createCustomFocusIndicatorStyle( + { + ...shorthands.borderRadius(tokens.borderRadiusMedium), + ...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2), + }, + { enableOutline: true }, + ), }, - primaryText: { ...shorthands.gridArea('primary') }, - secondaryText: { ...shorthands.gridArea('secondary') }, - dismissButton: {}, - // TODO add additional classes for different states and/or slots + // TODO add additional classes for fill/outline appearance, different sizes, and state +}); + +const useTagStyles = makeStyles({ + dismissableContent: { + paddingRight: '2px', + }, + dismissButton: { + marginRight: '6px', + }, }); /** * Apply styling to the Tag slots based on the state */ export const useTagStyles_unstable = (state: TagState): TagState => { - const styles = useStyles(); - state.root.className = mergeClasses(tagClassNames.root, styles.root, state.root.className); + const baseStyles = useTagBaseStyles(); + const styles = useTagStyles(); + + state.root.className = mergeClasses( + tagClassNames.root, + baseStyles.root, + state.shape === 'circular' && baseStyles.rootCircular, + state.root.className, + ); if (state.content) { - state.content.className = mergeClasses(tagClassNames.content, styles.content, state.content.className); + state.content.className = mergeClasses( + tagClassNames.content, + baseStyles.content, + !state.media && !state.icon && baseStyles.textOnlyContent, + + state.dismissButton && styles.dismissableContent, + state.content.className, + ); } - if (state.avatar) { - state.avatar.className = mergeClasses(tagClassNames.avatar, styles.avatar, state.avatar.className); + + if (state.media) { + state.media.className = mergeClasses(tagClassNames.media, baseStyles.media, state.media.className); } if (state.icon) { - state.icon.className = mergeClasses(tagClassNames.icon, styles.icon, state.icon.className); + state.icon.className = mergeClasses(tagClassNames.icon, baseStyles.icon, state.icon.className); } if (state.primaryText) { state.primaryText.className = mergeClasses( tagClassNames.primaryText, - styles.primaryText, + baseStyles.primaryText, + state.secondaryText && baseStyles.primaryTextWithSecondaryText, state.primaryText.className, ); } if (state.secondaryText) { state.secondaryText.className = mergeClasses( tagClassNames.secondaryText, - styles.secondaryText, + baseStyles.secondaryText, state.secondaryText.className, ); } if (state.dismissButton) { state.dismissButton.className = mergeClasses( tagClassNames.dismissButton, + baseStyles.resetButton, + baseStyles.dismissButton, + styles.dismissButton, state.dismissButton.className, ); diff --git a/packages/react-components/react-tags/src/components/TagButton/TagButton.test.tsx b/packages/react-components/react-tags/src/components/TagButton/TagButton.test.tsx index 00568aabcc30d1..74dd557f5f6efd 100644 --- a/packages/react-components/react-tags/src/components/TagButton/TagButton.test.tsx +++ b/packages/react-components/react-tags/src/components/TagButton/TagButton.test.tsx @@ -1,12 +1,8 @@ -import * as React from 'react'; -import { render } from '@testing-library/react'; import { TagButton } from './TagButton'; import { isConformant } from '../../testing/isConformant'; const requiredProps = { - avatar: { - name: 'Katri Athokas', - }, + media: 'media', icon: 'i', primaryText: 'Primary text', secondaryText: 'Secondary text', @@ -19,11 +15,4 @@ describe('TagButton', () => { displayName: 'TagButton', requiredProps, }); - - // TODO add more tests here, and create visual regression tests in /apps/vr-tests - - it('renders a default state', () => { - const result = render(Default TagButton); - expect(result.container).toMatchSnapshot(); - }); }); diff --git a/packages/react-components/react-tags/src/components/TagButton/TagButton.tsx b/packages/react-components/react-tags/src/components/TagButton/TagButton.tsx index 8b9fc592af5b28..aff06a81252e8d 100644 --- a/packages/react-components/react-tags/src/components/TagButton/TagButton.tsx +++ b/packages/react-components/react-tags/src/components/TagButton/TagButton.tsx @@ -4,6 +4,7 @@ import { renderTagButton_unstable } from './renderTagButton'; import { useTagButtonStyles_unstable } from './useTagButtonStyles.styles'; import type { TagButtonProps } from './TagButton.types'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useTagContextValues_unstable } from '../../utils/useTagContextValues'; /** * TagButton component - TODO: add more docs @@ -12,7 +13,7 @@ export const TagButton: ForwardRefComponent = React.forwardRef(( const state = useTagButton_unstable(props, ref); useTagButtonStyles_unstable(state); - return renderTagButton_unstable(state); + return renderTagButton_unstable(state, useTagContextValues_unstable(state)); }); TagButton.displayName = 'TagButton'; diff --git a/packages/react-components/react-tags/src/components/TagButton/TagButton.types.ts b/packages/react-components/react-tags/src/components/TagButton/TagButton.types.ts index 807800361a382e..ae5531c83167e0 100644 --- a/packages/react-components/react-tags/src/components/TagButton/TagButton.types.ts +++ b/packages/react-components/react-tags/src/components/TagButton/TagButton.types.ts @@ -1,30 +1,15 @@ -import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities'; -import { Avatar } from '@fluentui/react-avatar'; +import { TagContextValues, TagProps, TagSlots, TagState } from '../Tag/index'; -export type TagButtonSlots = { - root: NonNullable>; - contentButton?: Slot<'button'>; - avatar?: Slot; - icon?: Slot<'span'>; - primaryText?: Slot<'span'>; - secondaryText?: Slot<'span'>; - dismissButton?: NonNullable>; -}; +export type TagButtonContextValues = TagContextValues; + +export type TagButtonSlots = TagSlots; /** * TagButton Props */ -export type TagButtonProps = ComponentProps & { - size?: 'extra-small' | 'small' | 'medium'; - shape?: 'rounded' | 'circular'; - appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline'; - disabled?: boolean; - checked?: boolean; - dismissable?: boolean; -}; +export type TagButtonProps = TagProps; /** * State used in rendering TagButton */ -export type TagButtonState = ComponentState & - Required>; +export type TagButtonState = TagState; diff --git a/packages/react-components/react-tags/src/components/TagButton/__snapshots__/TagButton.test.tsx.snap b/packages/react-components/react-tags/src/components/TagButton/__snapshots__/TagButton.test.tsx.snap deleted file mode 100644 index ad849b8d3f3003..00000000000000 --- a/packages/react-components/react-tags/src/components/TagButton/__snapshots__/TagButton.test.tsx.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TagButton renders a default state 1`] = ` -
-
- - -
-
-`; diff --git a/packages/react-components/react-tags/src/components/TagButton/renderTagButton.tsx b/packages/react-components/react-tags/src/components/TagButton/renderTagButton.tsx index 17836bf23f9c6d..1d358dcaa60b5d 100644 --- a/packages/react-components/react-tags/src/components/TagButton/renderTagButton.tsx +++ b/packages/react-components/react-tags/src/components/TagButton/renderTagButton.tsx @@ -4,24 +4,30 @@ import { createElement } from '@fluentui/react-jsx-runtime'; import { getSlotsNext } from '@fluentui/react-utilities'; -import type { TagButtonState, TagButtonSlots } from './TagButton.types'; +import type { TagButtonState, TagButtonSlots, TagButtonContextValues } from './TagButton.types'; +import { AvatarContextProvider } from '@fluentui/react-avatar'; /** * Render the final JSX of TagButton */ -export const renderTagButton_unstable = (state: TagButtonState) => { +export const renderTagButton_unstable = (state: TagButtonState, contextValues: TagButtonContextValues) => { const { slots, slotProps } = getSlotsNext(state); - // TODO Add additional slots in the appropriate place return ( - {slots.contentButton && ( - - {slots.avatar && } + {slots.content && ( + + {slots.media && ( + + + + )} {slots.icon && } - {slots.primaryText && } + {slots.primaryText && ( + {slotProps.root.children} + )} {slots.secondaryText && } - + )} {slots.dismissButton && state.dismissable && } diff --git a/packages/react-components/react-tags/src/components/TagButton/useTagButton.tsx b/packages/react-components/react-tags/src/components/TagButton/useTagButton.tsx index 5a359ada2eec39..8d5bfe1d9d8dff 100644 --- a/packages/react-components/react-tags/src/components/TagButton/useTagButton.tsx +++ b/packages/react-components/react-tags/src/components/TagButton/useTagButton.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { getNativeElementProps, resolveShorthand } from '@fluentui/react-utilities'; +import { resolveShorthand } from '@fluentui/react-utilities'; import type { TagButtonProps, TagButtonState } from './TagButton.types'; -import { Dismiss16Filled } from '@fluentui/react-icons'; -import { Avatar } from '@fluentui/react-avatar'; +import { useTag_unstable } from '../Tag/index'; /** * Create the state required to render TagButton. @@ -14,47 +13,6 @@ import { Avatar } from '@fluentui/react-avatar'; * @param ref - reference to root HTMLElement of TagButton */ export const useTagButton_unstable = (props: TagButtonProps, ref: React.Ref): TagButtonState => { - const { - checked = false, - disabled = false, - dismissable = false, - shape = 'rounded', - size = 'medium', - appearance = 'filled-lighter', - } = props; - - return { - components: { - root: 'div', - contentButton: 'button', - avatar: Avatar, - icon: 'span', - primaryText: 'span', - secondaryText: 'span', - dismissButton: 'button', - }, - checked, - disabled, - dismissable, - shape, - size, - appearance, - root: getNativeElementProps('div', { - ref, - ...props, - }), - contentButton: resolveShorthand(props.contentButton, { required: true }), - avatar: resolveShorthand(props.avatar), - icon: resolveShorthand(props.icon), - primaryText: resolveShorthand(props.primaryText), - secondaryText: resolveShorthand(props.secondaryText), - dismissButton: resolveShorthand(props.dismissButton, { - required: true, - defaultProps: { - disabled, - type: 'button', - children: , - }, - }), - }; + const content = resolveShorthand(props.content, { required: true, defaultProps: { tabIndex: 0 } }); + return useTag_unstable({ ...props, content }, ref); }; diff --git a/packages/react-components/react-tags/src/components/TagButton/useTagButtonStyles.styles.ts b/packages/react-components/react-tags/src/components/TagButton/useTagButtonStyles.styles.ts index c03ad488ae53f5..fb28d8ef924084 100644 --- a/packages/react-components/react-tags/src/components/TagButton/useTagButtonStyles.styles.ts +++ b/packages/react-components/react-tags/src/components/TagButton/useTagButtonStyles.styles.ts @@ -1,11 +1,14 @@ import { makeStyles, mergeClasses, shorthands } from '@griffel/react'; import type { TagButtonSlots, TagButtonState } from './TagButton.types'; import type { SlotClassNames } from '@fluentui/react-utilities'; +import { createCustomFocusIndicatorStyle } from '@fluentui/react-tabster'; +import { tokens } from '@fluentui/react-theme'; +import { useTagBaseStyles } from '../Tag/index'; export const tagButtonClassNames: SlotClassNames = { root: 'fui-TagButton', - contentButton: 'fui-TagButton__contentButton', - avatar: 'fui-TagButton__avatar', + content: 'fui-TagButton__content', + media: 'fui-TagButton__media', icon: 'fui-TagButton__icon', primaryText: 'fui-TagButton__primaryText', secondaryText: 'fui-TagButton__secondaryText', @@ -16,72 +19,101 @@ export const tagButtonClassNames: SlotClassNames = { * Styles for the root slot */ const useStyles = makeStyles({ - root: { - display: 'inline-flex', + content: { + position: 'relative', + ...createCustomFocusIndicatorStyle( + { + ...shorthands.borderRadius(tokens.borderRadiusMedium), + ...shorthands.outline(tokens.strokeWidthThick, 'solid', tokens.colorStrokeFocus2), + }, + { enableOutline: true }, + ), }, - contentButton: { - display: 'inline-grid', - gridTemplateColumns: 'auto 8px auto auto 8px auto', - gridTemplateRows: '1fr auto auto 1fr', - gridTemplateAreas: ` - "avatar . icon . ." - "avatar . icon primary ." - "avatar . icon secondary ." - "avatar . icon . ." - `, - }, - avatar: { - alignSelf: 'center', - ...shorthands.gridArea('avatar'), + + circularContent: createCustomFocusIndicatorStyle(shorthands.borderRadius(tokens.borderRadiusCircular), { + enableOutline: true, + }), + dismissableContent: { + ...createCustomFocusIndicatorStyle({ + borderTopRightRadius: tokens.borderRadiusNone, + borderBottomRightRadius: tokens.borderRadiusNone, + }), }, - icon: { - alignSelf: 'center', - ...shorthands.gridArea('icon'), + + dismissButton: { + ...shorthands.padding('0px', '6px'), + ...shorthands.borderLeft(tokens.strokeWidthThin, 'solid', tokens.colorNeutralStroke1), + borderTopLeftRadius: tokens.borderRadiusNone, + borderBottomLeftRadius: tokens.borderRadiusNone, + ...createCustomFocusIndicatorStyle({ + borderTopLeftRadius: tokens.borderRadiusNone, + borderBottomLeftRadius: tokens.borderRadiusNone, + }), }, - primaryText: { ...shorthands.gridArea('primary') }, - secondaryText: { ...shorthands.gridArea('secondary') }, - dismissButton: {}, + dismissButtonCircular: createCustomFocusIndicatorStyle({ + borderTopRightRadius: tokens.borderRadiusCircular, + borderBottomRightRadius: tokens.borderRadiusCircular, + }), - // TODO add additional classes for different states and/or slots + // TODO add additional classes for fill/outline appearance, different sizes, and state }); /** * Apply styling to the TagButton slots based on the state */ export const useTagButtonStyles_unstable = (state: TagButtonState): TagButtonState => { + const baseStyles = useTagBaseStyles(); const styles = useStyles(); - state.root.className = mergeClasses(tagButtonClassNames.root, styles.root, state.root.className); - if (state.contentButton) { - state.contentButton.className = mergeClasses( - tagButtonClassNames.contentButton, - styles.contentButton, - state.contentButton.className, + + state.root.className = mergeClasses( + tagButtonClassNames.root, + baseStyles.root, + state.shape === 'circular' && baseStyles.rootCircular, + state.root.className, + ); + if (state.content) { + state.content.className = mergeClasses( + tagButtonClassNames.content, + baseStyles.content, + !state.media && !state.icon && baseStyles.textOnlyContent, + + styles.content, + state.shape === 'circular' && styles.circularContent, + state.dismissButton && styles.dismissableContent, + + state.content.className, ); } - if (state.avatar) { - state.avatar.className = mergeClasses(tagButtonClassNames.avatar, styles.avatar, state.avatar.className); + + if (state.media) { + state.media.className = mergeClasses(tagButtonClassNames.media, baseStyles.media, state.media.className); } if (state.icon) { - state.icon.className = mergeClasses(tagButtonClassNames.icon, styles.icon, state.icon.className); + state.icon.className = mergeClasses(tagButtonClassNames.icon, baseStyles.icon, state.icon.className); } if (state.primaryText) { state.primaryText.className = mergeClasses( tagButtonClassNames.primaryText, - styles.primaryText, + baseStyles.primaryText, + state.secondaryText && baseStyles.primaryTextWithSecondaryText, state.primaryText.className, ); } if (state.secondaryText) { state.secondaryText.className = mergeClasses( tagButtonClassNames.secondaryText, - styles.secondaryText, + baseStyles.secondaryText, state.secondaryText.className, ); } if (state.dismissButton) { state.dismissButton.className = mergeClasses( tagButtonClassNames.dismissButton, + baseStyles.resetButton, + baseStyles.dismissButton, + styles.dismissButton, + state.shape === 'circular' && styles.dismissButtonCircular, state.dismissButton.className, ); } diff --git a/packages/react-components/react-tags/src/utils/useTagContextValues.ts b/packages/react-components/react-tags/src/utils/useTagContextValues.ts new file mode 100644 index 00000000000000..d394a39d56c331 --- /dev/null +++ b/packages/react-components/react-tags/src/utils/useTagContextValues.ts @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { TagContextValues, TagState } from '../components/Tag/index'; + +export function useTagContextValues_unstable(state: TagState): TagContextValues { + const { avatarSize, avatarShape } = state; + + const avatar = React.useMemo( + () => ({ + size: avatarSize, + shape: avatarShape, + }), + [avatarShape, avatarSize], + ); + + return { + avatar, + }; +} diff --git a/packages/react-components/react-tags/stories/Tag/TagDefault.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagDefault.stories.tsx index c6639a6746769a..85917d78a12326 100644 --- a/packages/react-components/react-tags/stories/Tag/TagDefault.stories.tsx +++ b/packages/react-components/react-tags/stories/Tag/TagDefault.stories.tsx @@ -1,20 +1,148 @@ import * as React from 'react'; import { Tag, TagProps } from '@fluentui/react-tags'; -import { Calendar3Day28Regular } from '@fluentui/react-icons'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; +import { Avatar } from '@fluentui/react-components'; +// TODO I added many examples here for easier implementation. This story will be simplified to keep only the default example export const Default = (props: Partial) => ( - } - primaryText="Primary text" - secondaryText="Secondary text" - dismissable={true} - {...props} - /> +
+
+ + } + secondaryText="Secondary text" + dismissable={true} + {...props} + > + Primary text + + + } + dismissable={true} + {...props} + > + Primary text + + + } + {...props} + > + Primary text + + } secondaryText="Secondary text" dismissable={true} {...props} {...props}> + Primary text + + } dismissable={true} {...props}> + Primary text + + } {...props}> + Primary text + + Primary text +
+
+ + } + secondaryText="Secondary text" + dismissable={true} + {...props} + > + Primary text + + + } + dismissable={true} + {...props} + > + Primary text + + + } + {...props} + > + Primary text + + } + secondaryText="Secondary text" + dismissable={true} + {...props} + {...props} + > + Primary text + + } dismissable={true} {...props}> + Primary text + + } {...props}> + Primary text + + + Primary text + +
+
); diff --git a/packages/react-components/react-tags/stories/Tag/TagDismiss.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagDismiss.stories.tsx new file mode 100644 index 00000000000000..c5b805116d96d4 --- /dev/null +++ b/packages/react-components/react-tags/stories/Tag/TagDismiss.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Avatar, makeStyles } from '@fluentui/react-components'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; + +import { Tag } from '@fluentui/react-tags'; + +const useContainerStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + }, +}); + +export const Dismiss = () => { + const containerStyles = useContainerStyles(); + return ( +
+ Primary text + }> + Primary text + + } + secondaryText="Secondary text" + > + Primary text + +
+ ); +}; + +Dismiss.storyName = 'Dismiss'; +Dismiss.parameters = { + docs: { + description: { + story: 'A tag can have a button that dismisses it', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/Tag/TagIcon.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagIcon.stories.tsx new file mode 100644 index 00000000000000..28e79a1dc3e3f4 --- /dev/null +++ b/packages/react-components/react-tags/stories/Tag/TagIcon.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; +import { Tag } from '@fluentui/react-tags'; + +export const Icon = () => }>Primary text; + +Icon.storyName = 'Icon'; +Icon.parameters = { + docs: { + description: { + story: 'A Tag can render a custom icon if provided.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/Tag/TagMedia.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagMedia.stories.tsx new file mode 100644 index 00000000000000..941fbec7e0262a --- /dev/null +++ b/packages/react-components/react-tags/stories/Tag/TagMedia.stories.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { Avatar } from '@fluentui/react-components'; + +import { Tag } from '@fluentui/react-tags'; + +export const Media = () => }>Primary text; + +Media.storyName = 'Media'; +Media.parameters = { + docs: { + description: { + story: 'A tag can render a media, for example an Avatar.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/Tag/TagSecondaryText.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagSecondaryText.stories.tsx new file mode 100644 index 00000000000000..1f16e2b7b0815d --- /dev/null +++ b/packages/react-components/react-tags/stories/Tag/TagSecondaryText.stories.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Tag } from '@fluentui/react-tags'; + +export const SecondaryText = () => Primary text; + +SecondaryText.storyName = 'SecondaryText'; +SecondaryText.parameters = { + docs: { + description: { + story: 'A Tag can have a secondary text.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/Tag/TagShape.stories.tsx b/packages/react-components/react-tags/stories/Tag/TagShape.stories.tsx new file mode 100644 index 00000000000000..620b4001ce5e12 --- /dev/null +++ b/packages/react-components/react-tags/stories/Tag/TagShape.stories.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Avatar, makeStyles } from '@fluentui/react-components'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; + +import { Tag } from '@fluentui/react-tags'; + +const useContainerStyles = makeStyles({ + root: { + display: 'grid', + rowGap: '10px', + columnGap: '10px', + gridTemplateColumns: 'auto 1fr', + }, +}); + +export const Shape = () => { + const containerStyles = useContainerStyles(); + return ( +
+ }>Rounded + }> + Circular + + + } secondaryText="Secondary text"> + Rounded + + } secondaryText="Secondary text"> + Circular + +
+ ); +}; + +Shape.storyName = 'Shape'; +Shape.parameters = { + docs: { + description: { + story: 'A tag can be rounded or circular,', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/Tag/index.stories.tsx b/packages/react-components/react-tags/stories/Tag/index.stories.tsx index e0fb64458e077f..2aea59a32ff446 100644 --- a/packages/react-components/react-tags/stories/Tag/index.stories.tsx +++ b/packages/react-components/react-tags/stories/Tag/index.stories.tsx @@ -4,6 +4,11 @@ import descriptionMd from './TagDescription.md'; import bestPracticesMd from './TagBestPractices.md'; export { Default } from './TagDefault.stories'; +export { Icon } from './TagIcon.stories'; +export { Media } from './TagMedia.stories'; +export { SecondaryText } from './TagSecondaryText.stories'; +export { Dismiss } from './TagDismiss.stories'; +export { Shape } from './TagShape.stories'; export default { title: 'Preview Components/Tag', diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonDefault.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonDefault.stories.tsx index 08c2b41a875738..a9ef63153070a5 100644 --- a/packages/react-components/react-tags/stories/TagButton/TagButtonDefault.stories.tsx +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonDefault.stories.tsx @@ -1,20 +1,154 @@ import * as React from 'react'; import { TagButton, TagButtonProps } from '@fluentui/react-tags'; -import { Calendar3Day28Regular } from '@fluentui/react-icons'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; +import { Avatar } from '@fluentui/react-components'; +// TODO I added many examples here for easier implementation. This story will be simplified to keep only the default example export const Default = (props: Partial) => ( - } - primaryText="Primary text" - secondaryText="Secondary text" - dismissable={true} - {...props} - /> +
+
+ + } + secondaryText="Secondary text" + dismissable={true} + {...props} + > + Primary text + + + } + dismissable={true} + {...props} + > + Primary text + + + } + {...props} + > + Primary text + + } + secondaryText="Secondary text" + dismissable={true} + {...props} + {...props} + > + Primary text + + } dismissable={true} {...props}> + Primary text + + } {...props}> + Primary text + + Primary text +
+
+ + } + secondaryText="Secondary text" + dismissable={true} + {...props} + > + Primary text + + + } + dismissable={true} + {...props} + > + Primary text + + + } + {...props} + > + Primary text + + } + secondaryText="Secondary text" + dismissable={true} + {...props} + {...props} + > + Primary text + + } dismissable={true} {...props}> + Primary text + + } {...props}> + Primary text + + + Primary text + +
+
); diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonDismiss.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonDismiss.stories.tsx new file mode 100644 index 00000000000000..f10a12d9b45e1e --- /dev/null +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonDismiss.stories.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Avatar, makeStyles } from '@fluentui/react-components'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; + +import { TagButton } from '@fluentui/react-tags'; + +const useContainerStyles = makeStyles({ + root: { + display: 'flex', + flexDirection: 'column', + rowGap: '10px', + }, +}); + +export const Dismiss = () => { + const containerStyles = useContainerStyles(); + return ( +
+ Primary text + }> + Primary text + + } + secondaryText="Secondary text" + > + Primary text + +
+ ); +}; + +Dismiss.storyName = 'Dismiss'; +Dismiss.parameters = { + docs: { + description: { + story: 'A TagButton can have a button that dismisses it', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonIcon.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonIcon.stories.tsx new file mode 100644 index 00000000000000..621e32e4db1c0e --- /dev/null +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonIcon.stories.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; +import { TagButton } from '@fluentui/react-tags'; + +export const Icon = () => }>Primary text; + +Icon.storyName = 'Icon'; +Icon.parameters = { + docs: { + description: { + story: 'A TagButton can render a custom icon if provided.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonMedia.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonMedia.stories.tsx new file mode 100644 index 00000000000000..93db18c6b107ba --- /dev/null +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonMedia.stories.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { Avatar } from '@fluentui/react-components'; + +import { TagButton } from '@fluentui/react-tags'; + +export const Media = () => ( + }>Primary text +); + +Media.storyName = 'Media'; +Media.parameters = { + docs: { + description: { + story: 'A tag can render a media, for example an Avatar.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonSecondaryText.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonSecondaryText.stories.tsx new file mode 100644 index 00000000000000..26f4145b44d28c --- /dev/null +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonSecondaryText.stories.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { TagButton } from '@fluentui/react-tags'; + +export const SecondaryText = () => Primary text; + +SecondaryText.storyName = 'SecondaryText'; +SecondaryText.parameters = { + docs: { + description: { + story: 'A TagButton can have a secondary text.', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagButton/TagButtonShape.stories.tsx b/packages/react-components/react-tags/stories/TagButton/TagButtonShape.stories.tsx new file mode 100644 index 00000000000000..3f45196559c4b0 --- /dev/null +++ b/packages/react-components/react-tags/stories/TagButton/TagButtonShape.stories.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Avatar, makeStyles } from '@fluentui/react-components'; +import { Calendar3Day20Regular } from '@fluentui/react-icons'; + +import { TagButton } from '@fluentui/react-tags'; + +const useContainerStyles = makeStyles({ + root: { + display: 'grid', + rowGap: '10px', + columnGap: '10px', + gridTemplateColumns: 'auto 1fr', + }, +}); + +export const Shape = () => { + const containerStyles = useContainerStyles(); + return ( +
+ }>Rounded + }> + Circular + + + } secondaryText="Secondary text"> + Rounded + + } secondaryText="Secondary text"> + Circular + +
+ ); +}; + +Shape.storyName = 'Shape'; +Shape.parameters = { + docs: { + description: { + story: 'A TagButton can be rounded or circular,', + }, + }, +}; diff --git a/packages/react-components/react-tags/stories/TagButton/index.stories.tsx b/packages/react-components/react-tags/stories/TagButton/index.stories.tsx index 29de0d16523bf6..38b25718ef37c5 100644 --- a/packages/react-components/react-tags/stories/TagButton/index.stories.tsx +++ b/packages/react-components/react-tags/stories/TagButton/index.stories.tsx @@ -4,6 +4,11 @@ import descriptionMd from './TagButtonDescription.md'; import bestPracticesMd from './TagButtonBestPractices.md'; export { Default } from './TagButtonDefault.stories'; +export { Icon } from './TagButtonIcon.stories'; +export { Media } from './TagButtonMedia.stories'; +export { SecondaryText } from './TagButtonSecondaryText.stories'; +export { Dismiss } from './TagButtonDismiss.stories'; +export { Shape } from './TagButtonShape.stories'; export default { title: 'Preview Components/TagButton',