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
30 changes: 29 additions & 1 deletion packages/react-components/react-tags/etc/react-tags.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export const renderTag_unstable: (state: TagState, contextValues: TagContextValu
// @public
export const renderTagButton_unstable: (state: TagButtonState, contextValues: TagButtonContextValues) => JSX.Element;

// @public
export const renderTagGroup_unstable: (state: TagGroupState, contextValue: TagGroupContextValues) => JSX.Element;

// @public
export const Tag: ForwardRefComponent<TagProps>;

Expand All @@ -47,13 +50,32 @@ export type TagButtonState = ComponentState<TagButtonSlots> & Omit<TagState, 'co
// @public (undocumented)
export const tagClassNames: SlotClassNames<TagSlots>;

// @public
export const TagGroup: ForwardRefComponent<TagGroupProps>;

// @public (undocumented)
export const tagGroupClassNames: SlotClassNames<TagGroupSlots>;

// @public
export type TagGroupProps = ComponentProps<TagGroupSlots> & {
size?: TagSize;
};

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

// @public
export type TagGroupState = ComponentState<TagGroupSlots> & Required<Pick<TagGroupProps, 'size'>>;

// @public
export type TagProps = ComponentProps<Partial<TagSlots>> & {
appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline';
disabled?: boolean;
dismissible?: boolean;
shape?: 'rounded' | 'circular';
size?: 'extra-small' | 'small' | 'medium';
size?: TagSize;
};

// @public (undocumented)
Expand Down Expand Up @@ -81,6 +103,12 @@ export const useTagButton_unstable: (props: TagButtonProps, ref: React_2.Ref<HTM
// @public
export const useTagButtonStyles_unstable: (state: TagButtonState) => TagButtonState;

// @public
export const useTagGroup_unstable: (props: TagGroupProps, ref: React_2.Ref<HTMLElement>) => TagGroupState;

// @public
export const useTagGroupStyles_unstable: (state: TagGroupState) => TagGroupState;

// @public
export const useTagStyles_unstable: (state: TagState) => TagState;

Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-tags/src/TagGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './components/TagGroup/index';
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as React from 'react';
import { Tag } from './Tag';
import { isConformant } from '../../testing/isConformant';
import { TagProps } from './Tag.types';
import { render } from '@testing-library/react';
import { tagClassNames } from './useTagStyles.styles';

const requiredProps: TagProps = {
dismissible: true,
Expand All @@ -16,4 +19,9 @@ describe('Tag', () => {
displayName: 'Tag',
requiredProps,
});

it('should render root as a button', () => {
const { getByRole } = render(<Tag>Tag</Tag>);
expect(getByRole('button').className.includes(tagClassNames.root)).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { AvatarSize, AvatarShape } from '@fluentui/react-avatar';
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';

export type TagSize = 'extra-small' | 'small' | 'medium';

export type TagContextValues = {
avatar: {
size?: AvatarSize;
Expand Down Expand Up @@ -36,12 +38,10 @@ export type TagSlots = {
*/
export type TagProps = ComponentProps<Partial<TagSlots>> & {
appearance?: 'filled-darker' | 'filled-lighter' | 'tint' | 'outline';
// TODO implement tag checked state
// checked?: boolean;
disabled?: boolean;
dismissible?: boolean;
shape?: 'rounded' | 'circular';
size?: 'extra-small' | 'small' | 'medium';
size?: TagSize;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { getNativeElementProps, resolveShorthand } from '@fluentui/react-utilities';
import { DismissRegular, bundleIcon, DismissFilled } from '@fluentui/react-icons';
import type { TagProps, TagState } from './Tag.types';
import { useTagGroupContext_unstable } from '../../contexts/TagGroupContext';

const tagAvatarSizeMap = {
medium: 28,
Expand All @@ -26,12 +27,14 @@ const DismissIcon = bundleIcon(DismissFilled, DismissRegular);
* @param ref - reference to root HTMLElement of Tag
*/
export const useTag_unstable = (props: TagProps, ref: React.Ref<HTMLElement>): TagState => {
const { size: contextSize } = useTagGroupContext_unstable();

const {
appearance = 'filled-lighter',
disabled = false,
dismissible = false,
shape = 'rounded',
size = 'medium',
size = contextSize,
} = props;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ const useTagStyles = makeStyles({
},
{ enableOutline: true },
),

':hover': {
cursor: 'pointer',
},
},
rootCircular: {
...shorthands.borderRadius(tokens.borderRadiusCircular),
Expand All @@ -118,13 +122,22 @@ const useTagStyles = makeStyles({

// TODO add additional classes for fill/outline appearance, different sizes, and state
});

const useSmallTagStyles = makeStyles({
root: {
height: '24px',
},
// TODO add additional styles for sizes
});

/**
* Apply styling to the Tag slots based on the state
*/
export const useTagStyles_unstable = (state: TagState): TagState => {
const baseStyles = useTagBaseStyles();
const resetButtonStyles = useResetButtonStyles();
const styles = useTagStyles();
const smallStyles = useSmallTagStyles();

state.root.className = mergeClasses(
tagClassNames.root,
Expand All @@ -135,6 +148,8 @@ export const useTagStyles_unstable = (state: TagState): TagState => {
!state.media && !state.icon && styles.rootWithoutMedia,
!state.dismissIcon && styles.rootWithoutDismiss,

state.size === 'small' && smallStyles.root,

state.root.className,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getNativeElementProps, resolveShorthand } from '@fluentui/react-utiliti
import { DismissRegular, bundleIcon, DismissFilled } from '@fluentui/react-icons';
import type { TagButtonProps, TagButtonState } from './TagButton.types';
import { useARIAButtonShorthand } from '@fluentui/react-aria';
import { useTagGroupContext_unstable } from '../../contexts/TagGroupContext';

const tagButtonAvatarSizeMap = {
medium: 28,
Expand All @@ -27,12 +28,14 @@ const DismissIcon = bundleIcon(DismissFilled, DismissRegular);
* @param ref - reference to root HTMLElement of TagButton
*/
export const useTagButton_unstable = (props: TagButtonProps, ref: React.Ref<HTMLElement>): TagButtonState => {
const { size: contextSize } = useTagGroupContext_unstable();

const {
appearance = 'filled-lighter',
disabled = false,
dismissible = false,
shape = 'rounded',
size = 'medium',
size = contextSize,
} = props;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ const useStyles = makeStyles({
},
{ enableOutline: true },
),

':hover': {
cursor: 'pointer',
},
},
circularContent: createCustomFocusIndicatorStyle(shorthands.borderRadius(tokens.borderRadiusCircular)),
contentWithoutMedia: {
Expand Down Expand Up @@ -78,6 +82,10 @@ const useStyles = makeStyles({
borderTopRightRadius: tokens.borderRadiusMedium,
borderBottomRightRadius: tokens.borderRadiusMedium,
}),

':hover': {
cursor: 'pointer',
},
},
dismissButtonCircular: createCustomFocusIndicatorStyle({
borderTopRightRadius: tokens.borderRadiusCircular,
Expand All @@ -87,18 +95,29 @@ const useStyles = makeStyles({
// TODO add additional classes for fill/outline appearance, different sizes, and state
});

const useSmallTagButtonStyles = makeStyles({
root: {
height: '24px',
},
// TODO add additional styles for sizes
});

/**
* Apply styling to the TagButton slots based on the state
*/
export const useTagButtonStyles_unstable = (state: TagButtonState): TagButtonState => {
const baseStyles = useTagBaseStyles();
const resetButtonStyles = useResetButtonStyles();
const styles = useStyles();
const smallStyles = useSmallTagButtonStyles();

state.root.className = mergeClasses(
tagButtonClassNames.root,
styles.root,
state.shape === 'circular' && styles.rootCircular,

state.size === 'small' && smallStyles.root,

state.root.className,
);
if (state.content) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TagGroup } from './TagGroup';
import { isConformant } from '../../testing/isConformant';

describe('TagGroup', () => {
isConformant({
Component: TagGroup,
displayName: 'TagGroup',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { useTagGroup_unstable } from './useTagGroup';
import { renderTagGroup_unstable } from './renderTagGroup';
import { useTagGroupStyles_unstable } from './useTagGroupStyles.styles';
import type { TagGroupProps } from './TagGroup.types';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { useTagGroupContextValues_unstable } from './useTagGroupContextValues';

/**
* TagGroup component - TODO: add more docs
*/
export const TagGroup: ForwardRefComponent<TagGroupProps> = React.forwardRef((props, ref) => {
const state = useTagGroup_unstable(props, ref);

useTagGroupStyles_unstable(state);
return renderTagGroup_unstable(state, useTagGroupContextValues_unstable(state));
});

TagGroup.displayName = 'TagGroup';
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { TagSize } from '../Tag/Tag.types';
import { TagGroupContextValue } from '../../contexts/TagGroupContext';

export type TagGroupContextValues = {
tagGroup: TagGroupContextValue;
};

export type TagGroupSlots = {
root: Slot<'div'>;
};

/**
* TagGroup Props
*/
export type TagGroupProps = ComponentProps<TagGroupSlots> & {
size?: TagSize;
};

/**
* State used in rendering TagGroup
*/
export type TagGroupState = ComponentState<TagGroupSlots> & Required<Pick<TagGroupProps, 'size'>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './TagGroup';
export * from './TagGroup.types';
export * from './renderTagGroup';
export * from './useTagGroup';
export * from './useTagGroupStyles.styles';
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/** @jsxRuntime classic */
/** @jsx createElement */
import { createElement } from '@fluentui/react-jsx-runtime';
import { getSlotsNext } from '@fluentui/react-utilities';
import type { TagGroupState, TagGroupSlots, TagGroupContextValues } from './TagGroup.types';
import { TagGroupContextProvider } from '../../contexts/TagGroupContext';

/**
* Render the final JSX of TagGroup
*/
export const renderTagGroup_unstable = (state: TagGroupState, contextValue: TagGroupContextValues) => {
const { slots, slotProps } = getSlotsNext<TagGroupSlots>(state);

return (
<TagGroupContextProvider value={contextValue.tagGroup}>
<slots.root {...slotProps.root} />
</TagGroupContextProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from 'react';
import { getNativeElementProps } from '@fluentui/react-utilities';
import type { TagGroupProps, TagGroupState } from './TagGroup.types';

/**
* Create the state required to render TagGroup.
*
* The returned state can be modified with hooks such as useTagGroupStyles_unstable,
* before being passed to renderTagGroup_unstable.
*
* @param props - props from this instance of TagGroup
* @param ref - reference to root HTMLElement of TagGroup
*/
export const useTagGroup_unstable = (props: TagGroupProps, ref: React.Ref<HTMLElement>): TagGroupState => {
const { size = 'medium' } = props;

return {
size,
components: {
root: 'div',
},
root: getNativeElementProps('div', {
ref,
...props,
// TODO aria attributes
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';
import type { TagGroupContextValues, TagGroupState } from './TagGroup.types';

export function useTagGroupContextValues_unstable(state: TagGroupState): TagGroupContextValues {
const { size } = state;
return { tagGroup: React.useMemo(() => ({ size }), [size]) };
}
Loading