Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-tag): Added selected state for Tag and InteractionTag #33804

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
Expand Up @@ -110,6 +110,40 @@ export const SizeExtraSmallWithMedia = () => (
</InteractionTag>
);

// selected
export const Selected = () => (
<InteractionTag selected>
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
Primary Text
</InteractionTagPrimary>
<InteractionTagSecondary />
</InteractionTag>
);
export const SelectedHighContrast = getStoryVariant(Selected, HIGH_CONTRAST);
export const SelectedDarkMode = getStoryVariant(Selected, DARK_MODE);

export const OutlineSelected = () => (
<InteractionTag appearance="outline" selected>
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
Primary Text
</InteractionTagPrimary>
<InteractionTagSecondary />
</InteractionTag>
);
export const OutlineSelectedHighContrast = getStoryVariant(OutlineSelected, HIGH_CONTRAST);
export const OutlineSelectedDarkMode = getStoryVariant(OutlineSelected, DARK_MODE);

export const BrandSelected = () => (
<InteractionTag appearance="brand" selected>
<InteractionTagPrimary icon={<CalendarMonth />} hasSecondaryAction>
Primary Text
</InteractionTagPrimary>
<InteractionTagSecondary />
</InteractionTag>
);
export const BrandSelectedHighContrast = getStoryVariant(BrandSelected, HIGH_CONTRAST);
export const BrandSelectedDarkMode = getStoryVariant(BrandSelected, DARK_MODE);

const useBoxSizingContainerStyles = makeStyles({
container: {
boxSizing: 'border-box',
Expand Down
25 changes: 25 additions & 0 deletions apps/vr-tests-react-components/src/stories/Tag/Tag.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,28 @@ export const SizeExtraSmallWithMedia = () => (
Primary Text
</Tag>
);

// selected
export const Selected = () => (
<Tag selected dismissible icon={<CalendarMonth />}>
Primary Text
</Tag>
);
export const SelectedHighContrast = getStoryVariant(Selected, HIGH_CONTRAST);
export const SelectedDarkMode = getStoryVariant(Selected, DARK_MODE);

export const OutlineSelected = () => (
<Tag appearance="outline" selected dismissible icon={<CalendarMonth />}>
Primary Text
</Tag>
);
export const OutlineSelectedHighContrast = getStoryVariant(OutlineSelected, HIGH_CONTRAST);
export const OutlineSelectedDarkMode = getStoryVariant(OutlineSelected, DARK_MODE);

export const BrandSelected = () => (
<Tag appearance="brand" selected dismissible icon={<CalendarMonth />}>
Primary Text
</Tag>
);
export const BrandSelectedHighContrast = getStoryVariant(BrandSelected, HIGH_CONTRAST);
export const BrandSelectedDarkMode = getStoryVariant(BrandSelected, DARK_MODE);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: Added selected state for Tag and InteractionTag",
"packageName": "@fluentui/react-tags",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ export type InteractionTagPrimarySlots = {
};

// @public
export type InteractionTagPrimaryState = ComponentState<InteractionTagPrimarySlots> & Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'shape' | 'size'> & Pick<InteractionTagPrimaryProps, 'hasSecondaryAction'>> & UseTagAvatarContextValuesOptions;
export type InteractionTagPrimaryState = ComponentState<InteractionTagPrimarySlots> & Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size'> & Pick<InteractionTagPrimaryProps, 'hasSecondaryAction'>> & UseTagAvatarContextValuesOptions;

// @public
export type InteractionTagProps<Value = TagValue> = ComponentProps<Partial<InteractionTagSlots>> & {
appearance?: TagAppearance;
disabled?: boolean;
selected?: boolean;
shape?: TagShape;
size?: TagSize;
value?: Value;
Expand All @@ -71,15 +72,15 @@ export type InteractionTagSecondarySlots = {
};

// @public
export type InteractionTagSecondaryState = ComponentState<InteractionTagSecondarySlots> & Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'shape' | 'size'>>;
export type InteractionTagSecondaryState = ComponentState<InteractionTagSecondarySlots> & Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size'>>;

// @public (undocumented)
export type InteractionTagSlots = {
root: NonNullable<Slot<'div'>>;
};

// @public
export type InteractionTagState<Value = TagValue> = ComponentState<InteractionTagSlots> & Required<Pick<InteractionTagProps, 'appearance' | 'disabled' | 'shape' | 'size' | 'value'>> & {
export type InteractionTagState<Value = TagValue> = ComponentState<InteractionTagSlots> & Required<Pick<InteractionTagProps, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size' | 'value'>> & {
handleTagDismiss: TagDismissHandler<Value>;
interactionTagPrimaryId: string;
};
Expand Down Expand Up @@ -155,6 +156,7 @@ export type TagProps<Value = string> = ComponentProps<Partial<TagSlots>> & {
appearance?: TagAppearance;
disabled?: boolean;
dismissible?: boolean;
selected?: boolean;
shape?: TagShape;
size?: TagSize;
value?: Value;
Expand All @@ -177,7 +179,7 @@ export type TagSlots = {
};

// @public
export type TagState = ComponentState<TagSlots> & Required<Pick<TagProps, 'appearance' | 'disabled' | 'dismissible' | 'shape' | 'size'>> & UseTagAvatarContextValuesOptions;
export type TagState = ComponentState<TagSlots> & Required<Pick<TagProps, 'appearance' | 'disabled' | 'dismissible' | 'selected' | 'shape' | 'size'>> & UseTagAvatarContextValuesOptions;

// @public (undocumented)
export type TagValue = string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export type InteractionTagProps<Value = TagValue> = ComponentProps<Partial<Inter
*/
disabled?: boolean;

/**
* An InteractionTag can be selected.
* Note: This prop only changes the appearance of the tag at the moment. A future PR will add the integration with TagGroup.
*
* @default false
*/
selected?: boolean;

/**
* An InteractionTag can have rounded or circular shape.
*
Expand All @@ -52,7 +60,7 @@ export type InteractionTagProps<Value = TagValue> = ComponentProps<Partial<Inter
* State used in rendering InteractionTag
*/
export type InteractionTagState<Value = TagValue> = ComponentState<InteractionTagSlots> &
Required<Pick<InteractionTagProps, 'appearance' | 'disabled' | 'shape' | 'size' | 'value'>> & {
Required<Pick<InteractionTagProps, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size' | 'value'>> & {
/**
* Event handler from TagGroup context that allows TagGroup to dismiss the tag
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const useInteractionTag_unstable = (
const {
appearance = contextAppearance ?? 'filled',
disabled = false,
selected = false,
shape = 'rounded',
size = contextSize,
value = id,
Expand All @@ -40,6 +41,7 @@ export const useInteractionTag_unstable = (
disabled: contextDisabled ? true : disabled,
handleTagDismiss,
interactionTagPrimaryId,
selected,
shape,
size,
value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import * as React from 'react';
import { InteractionTagState, InteractionTagContextValues } from './InteractionTag.types';

export function useInteractionTagContextValues_unstable(state: InteractionTagState): InteractionTagContextValues {
const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value } = state;
const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, selected, shape, size, value } = state;

return {
interactionTag: React.useMemo(
() => ({ appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value }),
[appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value],
() => ({ appearance, disabled, handleTagDismiss, interactionTagPrimaryId, selected, shape, size, value }),
[appearance, disabled, handleTagDismiss, interactionTagPrimaryId, selected, shape, size, value],
),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type InteractionTagPrimaryProps = ComponentProps<Partial<InteractionTagPr
*/
export type InteractionTagPrimaryState = ComponentState<InteractionTagPrimarySlots> &
Required<
Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'shape' | 'size'> &
Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size'> &
Pick<InteractionTagPrimaryProps, 'hasSecondaryAction'>
> &
UseTagAvatarContextValuesOptions;
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@ export const useInteractionTagPrimary_unstable = (
props: InteractionTagPrimaryProps,
ref: React.Ref<HTMLButtonElement>,
): InteractionTagPrimaryState => {
const { appearance, disabled, interactionTagPrimaryId, shape, size } = useInteractionTagContext_unstable();
const {
appearance,
disabled,
interactionTagPrimaryId,
selected: contextSelected,
shape,
size,
} = useInteractionTagContext_unstable();
const { hasSecondaryAction = false } = props;

return {
Expand All @@ -36,6 +43,7 @@ export const useInteractionTagPrimary_unstable = (
avatarSize: avatarSizeMap[size],
disabled,
hasSecondaryAction,
selected: contextSelected ?? false,
shape,
size,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,25 @@ const useRootStyles = makeStyles({
},
},
},

selected: {
background: tokens.colorBrandBackground,
color: tokens.colorNeutralForegroundOnBrand,
...shorthands.borderColor(tokens.colorBrandBackground),
':hover': {
backgroundColor: tokens.colorBrandBackgroundHover,
color: tokens.colorNeutralForegroundOnBrand,
[`& .${iconFilledClassName}`]: {
color: tokens.colorNeutralForegroundOnBrand,
},
},
':active': {
backgroundColor: tokens.colorBrandBackgroundPressed,
color: tokens.colorNeutralForegroundOnBrand,
[`& .${iconFilledClassName}`]: {
color: tokens.colorNeutralForegroundOnBrand,
},
},
},
medium: {
paddingRight: '7px',
},
Expand Down Expand Up @@ -279,7 +297,7 @@ export const useInteractionTagPrimaryStyles_unstable = (

const rootCircularContrastStyles = useRootCircularContrastStyles();

const { shape, size, appearance } = state;
const { shape, size, appearance, selected } = state;

state.root.className = mergeClasses(
interactionTagPrimaryClassNames.root,
Expand All @@ -289,6 +307,7 @@ export const useInteractionTagPrimaryStyles_unstable = (
shape === 'circular' && !state.hasSecondaryAction && rootCircularContrastStyles.withoutSecondaryAction,

state.disabled ? rootDisabledAppearances[appearance] : rootStyles[appearance],
selected && !state.disabled && rootStyles.selected,
rootStyles[size],

!state.media && !state.icon && rootWithoutMediaStyles[size],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ export type InteractionTagSecondaryProps = ComponentProps<InteractionTagSecondar
* State used in rendering InteractionTagSecondary
*/
export type InteractionTagSecondaryState = ComponentState<InteractionTagSecondarySlots> &
Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'shape' | 'size'>>;
Required<Pick<InteractionTagContextValue, 'appearance' | 'disabled' | 'selected' | 'shape' | 'size'>>;
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const useInteractionTagSecondary_unstable = (
props: InteractionTagSecondaryProps,
ref: React.Ref<HTMLButtonElement>,
): InteractionTagSecondaryState => {
const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, shape, size, value } =
const { appearance, disabled, handleTagDismiss, interactionTagPrimaryId, selected, shape, size, value } =
useInteractionTagContext_unstable();

const id = useId('fui-InteractionTagSecondary-', props.id);
Expand All @@ -40,6 +40,7 @@ export const useInteractionTagSecondary_unstable = (
return {
appearance,
disabled,
selected,
shape,
size,
components: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ const useRootStyles = makeStyles({
},
},
},
selected: {
background: tokens.colorBrandBackground,
color: tokens.colorNeutralForegroundOnBrand,
...shorthands.borderColor(tokens.colorBrandBackground),
':hover': {
backgroundColor: tokens.colorBrandBackgroundHover,
color: tokens.colorNeutralForegroundOnBrand,
},
':active': {
backgroundColor: tokens.colorBrandBackgroundPressed,
color: tokens.colorNeutralForegroundOnBrand,
},
// divider
borderLeftColor: tokens.colorNeutralStrokeOnBrand2,
},

rounded: {
borderTopRightRadius: tokens.borderRadiusMedium,
Expand Down Expand Up @@ -162,14 +177,15 @@ export const useInteractionTagSecondaryStyles_unstable = (
const rootStyles = useRootStyles();
const rootDisabledStyles = useRootDisabledStyles();

const { shape, size, appearance } = state;
const { selected, shape, size, appearance } = state;

state.root.className = mergeClasses(
interactionTagSecondaryClassNames.root,
rootBaseClassName,
state.disabled ? rootDisabledStyles[appearance] : rootStyles[appearance],
rootStyles[shape],
rootStyles[size],
selected && !state.disabled && rootStyles.selected,
state.root.className,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ export type TagProps<Value = string> = ComponentProps<Partial<TagSlots>> & {
*/
dismissible?: boolean;

/**
* An InteractionTag can be selected.
* Note: This prop only changes the appearance of the tag at the moment. A future PR will add the integration with TagGroup.
*
* @default false
*/
selected?: boolean;

/**
* A Tag can have rounded or circular shape.
*
Expand All @@ -82,5 +90,5 @@ export type TagProps<Value = string> = ComponentProps<Partial<TagSlots>> & {
* State used in rendering Tag
*/
export type TagState = ComponentState<TagSlots> &
Required<Pick<TagProps, 'appearance' | 'disabled' | 'dismissible' | 'shape' | 'size'>> &
Required<Pick<TagProps, 'appearance' | 'disabled' | 'dismissible' | 'selected' | 'shape' | 'size'>> &
UseTagAvatarContextValuesOptions;
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const useTag_unstable = (props: TagProps, ref: React.Ref<HTMLSpanElement
appearance = contextAppearance ?? 'filled',
disabled = false,
dismissible = contextDismissible ?? false,
selected = false,
shape = 'rounded',
size = contextSize,
value = id,
Expand Down Expand Up @@ -68,6 +69,7 @@ export const useTag_unstable = (props: TagProps, ref: React.Ref<HTMLSpanElement
avatarSize: tagAvatarSizeMap[size],
disabled: contextDisabled ? true : disabled,
dismissible,
selected,
shape,
size,

Expand Down
Loading