diff --git a/packages/fluentui/CHANGELOG.md b/packages/fluentui/CHANGELOG.md index 4a945f2feabb02..de786dde943242 100644 --- a/packages/fluentui/CHANGELOG.md +++ b/packages/fluentui/CHANGELOG.md @@ -20,6 +20,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Features - Add `borderActive4` color slot @notandrew ([#17391](https://github.com/microsoft/fluentui/pull/17391)) +- Add `Pill` base componet @assuncaocharles ([#17500](https://github.com/microsoft/fluentui/pull/17500)) ## Documentation - Update left nav in UI Builder to separate add components from navigator @codepretty ([#17002](https://github.com/microsoft/fluentui/pull/17002)) diff --git a/packages/fluentui/docs/src/examples/components/Pill/State/PillExampleDisabled.tsx b/packages/fluentui/docs/src/examples/components/Pill/State/PillExampleDisabled.tsx new file mode 100644 index 00000000000000..c98c8319a3f4c9 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/State/PillExampleDisabled.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { Pill } from '@fluentui/react-northstar'; + +const PillExampleDisabled = () => This is a disabled Pill; + +export default PillExampleDisabled; diff --git a/packages/fluentui/docs/src/examples/components/Pill/State/index.tsx b/packages/fluentui/docs/src/examples/components/Pill/State/index.tsx new file mode 100644 index 00000000000000..ef3674b06fa490 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/State/index.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +import ComponentExample from '../../../../components/ComponentDoc/ComponentExample'; +import ExampleSection from '../../../../components/ComponentDoc/ExampleSection'; + +const State = () => ( + + + +); + +export default State; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Types/PillExample.tsx b/packages/fluentui/docs/src/examples/components/Pill/Types/PillExample.tsx new file mode 100644 index 00000000000000..f8eda43032ea0f --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Types/PillExample.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { Pill } from '@fluentui/react-northstar'; + +const PillExample = () => This is a default Pill; + +export default PillExample; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Types/index.tsx b/packages/fluentui/docs/src/examples/components/Pill/Types/index.tsx new file mode 100644 index 00000000000000..2fb95251c229c3 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Types/index.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; + +import ComponentExample from '../../../../components/ComponentDoc/ComponentExample'; +import ExampleSection from '../../../../components/ComponentDoc/ExampleSection'; + +const Types = () => ( + + + +); + +export default Types; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleAppearance.tsx b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleAppearance.tsx new file mode 100644 index 00000000000000..b9cf4bbea6a0e7 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleAppearance.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { Pill, Flex } from '@fluentui/react-northstar'; + +const PillAppearanceExample = () => ( + + Filled Pill (Default) + Inverted Pill + Outlined Pill + +); + +export default PillAppearanceExample; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleRectangular.tsx b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleRectangular.tsx new file mode 100644 index 00000000000000..171a34b5e9d86c --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleRectangular.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { Pill } from '@fluentui/react-northstar'; + +const PillRectangularExample = () => Pill Content; + +export default PillRectangularExample; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleSizes.tsx b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleSizes.tsx new file mode 100644 index 00000000000000..1a204691936c0a --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Variations/PillExampleSizes.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Pill, Flex, PillProps } from '@fluentui/react-northstar'; + +const PillSizesExample = () => ( + + {['smaller', 'small', 'medium'].map((size: PillProps['size']) => ( + + {size} pill + + ))} + +); + +export default PillSizesExample; diff --git a/packages/fluentui/docs/src/examples/components/Pill/Variations/index.tsx b/packages/fluentui/docs/src/examples/components/Pill/Variations/index.tsx new file mode 100644 index 00000000000000..7eb61ed7d6010a --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/Variations/index.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +import ComponentExample from '../../../../components/ComponentDoc/ComponentExample'; +import ExampleSection from '../../../../components/ComponentDoc/ExampleSection'; + +const Variations = () => ( + + + + + +); + +export default Variations; diff --git a/packages/fluentui/docs/src/examples/components/Pill/index.tsx b/packages/fluentui/docs/src/examples/components/Pill/index.tsx new file mode 100644 index 00000000000000..6694394cc809e7 --- /dev/null +++ b/packages/fluentui/docs/src/examples/components/Pill/index.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import Types from './Types'; +import Variations from './Variations'; +import State from './State'; + +const PillExamples = () => ( + <> + + + + +); + +export default PillExamples; diff --git a/packages/fluentui/react-northstar/src/components/Pill/Pill.tsx b/packages/fluentui/react-northstar/src/components/Pill/Pill.tsx new file mode 100644 index 00000000000000..f4da514038c0a1 --- /dev/null +++ b/packages/fluentui/react-northstar/src/components/Pill/Pill.tsx @@ -0,0 +1,118 @@ +import * as PropTypes from 'prop-types'; +import * as React from 'react'; +import * as customPropTypes from '@fluentui/react-proptypes'; +import { UIComponentProps, ContentComponentProps, commonPropTypes, SizeValue, createShorthand } from '../../utils'; +import { ShorthandValue, FluentComponentStaticProps } from '../../types'; +import { BoxProps } from '../Box/Box'; + +import { + ComponentWithAs, + useAccessibility, + getElementType, + useStyles, + useTelemetry, + useFluentContext, + useUnhandledProps, +} from '@fluentui/react-bindings'; +import { PillContent } from './PillContent'; + +export interface PillProps extends UIComponentProps, ContentComponentProps> { + /** + * A Pill can be sized. + */ + size?: Extract; + + /** + * A Pill can be rectangular + */ + rectangular?: boolean; + + /** + * A Pill can be filled, inverted or outline + */ + appearance?: 'filled' | 'inverted' | 'outline'; + + /** + * A Pill can be disbled + */ + disabled?: boolean; +} + +export type PillStylesProps = Required>; + +export const pillClassName = 'ui-pill'; + +/** + * THIS COMPONENT IS STILL IN DEVELOPMENT AND IS NOT READY FOR PRODUCTION + * Pills should be used when representing an input, as a way to filter content, or to represent an attribute. + */ +export const Pill: ComponentWithAs<'span', PillProps> & FluentComponentStaticProps = props => { + const context = useFluentContext(); + const { setStart, setEnd } = useTelemetry(Pill.displayName, context.telemetry); + setStart(); + + const { className, design, styles, variables, appearance, size, rectangular, children, content, disabled } = props; + + const ElementType = getElementType(props); + const unhandledProps = useUnhandledProps(Pill.handledProps, props); + + const getA11yProps = useAccessibility(props.accessibility, { + debugName: Pill.displayName, + mapPropsToBehavior: () => ({}), + rtl: context.rtl, + }); + + const { classes } = useStyles(Pill.displayName, { + className: pillClassName, + mapPropsToStyles: () => ({ + appearance, + size, + rectangular, + disabled, + }), + mapPropsToInlineStyles: () => ({ + className, + design, + styles, + variables, + }), + rtl: context.rtl, + }); + + const element = getA11yProps.unstable_wrapWithFocusZone( + + {createShorthand(PillContent, content || {}, { + defaultProps: () => ({ + children, + size, + }), + })} + , + ); + + setEnd(); + + return element; +}; + +Pill.defaultProps = { + as: 'span', +}; + +Pill.propTypes = { + ...commonPropTypes.createCommon(), + content: customPropTypes.shorthandAllowingChildren, + size: PropTypes.oneOf(['small', 'smaller', 'medium']), + rectangular: PropTypes.bool, + disabled: PropTypes.bool, + appearance: PropTypes.oneOf(['filled', 'inverted', 'outline']), +}; + +Pill.displayName = 'Pill'; + +Pill.handledProps = Object.keys(Pill.propTypes) as any; diff --git a/packages/fluentui/react-northstar/src/components/Pill/PillContent.tsx b/packages/fluentui/react-northstar/src/components/Pill/PillContent.tsx new file mode 100644 index 00000000000000..fb17bbc0f05118 --- /dev/null +++ b/packages/fluentui/react-northstar/src/components/Pill/PillContent.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { Accessibility } from '@fluentui/accessibility'; +import { + ComponentWithAs, + getElementType, + useUnhandledProps, + useAccessibility, + useFluentContext, + useStyles, + useTelemetry, +} from '@fluentui/react-bindings'; +import { + childrenExist, + UIComponentProps, + ChildrenComponentProps, + ContentComponentProps, + commonPropTypes, + rtlTextContainer, + SizeValue, +} from '../../utils'; + +import { FluentComponentStaticProps } from '../../types'; + +export interface PillContentProps extends UIComponentProps, ChildrenComponentProps, ContentComponentProps { + /** + * Accessibility behavior if overridden by the user. + */ + accessibility?: Accessibility; + + /** + * A Pill can be sized. + */ + size?: Extract; +} + +export type PillContentStylesProps = Required>; +export const pillContentClassName = 'ui-pillcontent'; + +/** + * A PillContent allows user to classify content. + */ +export const PillContent: ComponentWithAs<'span', PillContentProps> & + FluentComponentStaticProps = props => { + const context = useFluentContext(); + const { setStart, setEnd } = useTelemetry(PillContent.displayName, context.telemetry); + setStart(); + + const { accessibility, children, className, content, design, styles, variables, size } = props; + + const getA11Props = useAccessibility(accessibility, { + debugName: PillContent.displayName, + rtl: context.rtl, + }); + + const { classes } = useStyles(PillContent.displayName, { + className: pillContentClassName, + mapPropsToStyles: () => ({ size }), + mapPropsToInlineStyles: () => ({ className, design, styles, variables }), + rtl: context.rtl, + }); + + const ElementType = getElementType(props); + const unhandledProps = useUnhandledProps(PillContent.handledProps, props); + + const element = ( + + {childrenExist(children) ? children : content} + + ); + + setEnd(); + + return element; +}; + +PillContent.displayName = 'PillContent'; + +PillContent.propTypes = { + ...commonPropTypes.createCommon(), +}; + +PillContent.handledProps = Object.keys(PillContent.propTypes) as any; + +PillContent.defaultProps = { + as: 'span', +}; + +PillContent.shorthandConfig = { + mappedProp: 'content', +}; diff --git a/packages/fluentui/react-northstar/src/index.ts b/packages/fluentui/react-northstar/src/index.ts index 04a0e51cdb8d98..e11721488301e2 100644 --- a/packages/fluentui/react-northstar/src/index.ts +++ b/packages/fluentui/react-northstar/src/index.ts @@ -72,6 +72,9 @@ export * from './components/Design/Design'; export * from './components/MenuButton/MenuButton'; +export * from './components/Pill/Pill'; +export * from './components/Pill/PillContent'; + export * from './components/Divider/Divider'; export * from './components/Divider/DividerContent'; diff --git a/packages/fluentui/react-northstar/src/themes/teams/componentStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/componentStyles.ts index 239384c2e09641..5cd29e953e0c4a 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/componentStyles.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/componentStyles.ts @@ -97,6 +97,9 @@ export { menuDividerStyles as MenuDivider } from './components/Menu/menuDividerS export { menuButtonStyles as MenuButton } from './components/MenuButton/menuButtonStyles'; +export { pillStyles as Pill } from './components/Pill/pillStyles'; +export { pillContentStyles as PillContent } from './components/Pill/pillContentStyles'; + export { popupContentStyles as PopupContent } from './components/Popup/popupContentStyles'; export { providerStyles as Provider } from './components/Provider/providerStyles'; diff --git a/packages/fluentui/react-northstar/src/themes/teams/componentVariables.ts b/packages/fluentui/react-northstar/src/themes/teams/componentVariables.ts index a1030b78c0b5cd..3d43639ce03374 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/componentVariables.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/componentVariables.ts @@ -90,6 +90,9 @@ export { menuItemContentVariables as MenuItemIndicator } from './components/Menu export { menuItemWrapperVariables as MenuItemWrapper } from './components/Menu/menuItemWrapperVariables'; export { menuDividerVariables as MenuDivider } from './components/Menu/menuDividerVariables'; +export { pillVariables as Pill } from './components/Pill/pillVariables'; +export { pillVariables as PillContent } from './components/Pill/pillVariables'; + export { popupContentVariables as PopupContent } from './components/Popup/popupContentVariables'; export { providerVariables as Provider } from './components/Provider/providerVariables'; diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillContentStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillContentStyles.ts new file mode 100644 index 00000000000000..6ba101da950e8f --- /dev/null +++ b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillContentStyles.ts @@ -0,0 +1,19 @@ +import { ComponentSlotStylesPrepared, ICSSInJSStyle } from '@fluentui/styles'; +import { PillContentStylesProps } from '../../../../components/Pill/PillContent'; +import { PillVariables } from './pillVariables'; + +export const pillContentStyles: ComponentSlotStylesPrepared = { + root: ({ props: p, variables: v }): ICSSInJSStyle => ({ + fontSize: v.contentFontSize, + padding: v.contentPadding, + + ...(p.size === 'small' && { + fontSize: v.contentFontSizeSmall, + padding: v.contentPaddingSmall, + }), + ...(p.size === 'smaller' && { + fontSize: v.contentFontSizeSmaller, + padding: v.contentPaddingSmaller, + }), + }), +}; diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillStyles.ts b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillStyles.ts new file mode 100644 index 00000000000000..b0f84b9b4284ed --- /dev/null +++ b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillStyles.ts @@ -0,0 +1,79 @@ +import { ComponentSlotStylesPrepared, ICSSInJSStyle } from '@fluentui/styles'; +import { PillStylesProps } from '../../../../components/Pill/Pill'; +import { PillVariables } from './pillVariables'; +import { getBorderFocusStyles } from '../../getBorderFocusStyles'; + +export const pillStyles: ComponentSlotStylesPrepared = { + root: ({ props: p, variables: v, theme: { siteVariables } }): ICSSInJSStyle => { + return { + display: 'inline-flex', + width: 'fit-content', + height: v.height, + maxHeight: v.height, + borderRadius: v.borderRadius, + background: v.background, + margin: v.margin, + minWidth: v.minWidth, + + ':hover': { + background: v.backgroundHover, + }, + + ...(p.rectangular && { + borderRadius: v.roundedBorderRadius, + ...((p.size === 'small' || p.size === 'smaller') && { + borderRadius: v.smallerRoundedBorderRadius, + }), + }), + + ...(p.size === 'smaller' && { + minWidth: v.smallerMinWidth, + margin: v.smallerMargin, + height: v.smallerHeight, + maxHeight: v.smallerHeight, + }), + + ...(p.size === 'small' && { + minWidth: v.smallMinWidth, + margin: v.smallMargin, + height: v.smallHeight, + maxHeight: v.smallHeight, + }), + + ...(p.disabled && { + pointerEvents: 'none', + cursor: 'not-allowed', + background: v.disabledBackground, + color: v.disabledColor, + ':hover': { + background: v.disabledBackground, + }, + }), + + ...(p.appearance === 'outline' && { + borderWidth: '1px', + borderStyle: 'solid', + background: v.outlineBackground, + borderColor: v.outlineBorderColor, + ':hover': { + background: v.outlineBackground, + }, + ...(p.disabled && { + borderColor: v.outlineDisabledborder, + }), + }), + + ...(p.appearance === 'inverted' && { + background: v.invertedBackground, + ':hover': { + background: v.invertedBackground, + }, + ...(p.disabled && { + background: v.disabledBackground, + }), + }), + + ...getBorderFocusStyles({ variables: siteVariables }), + }; + }, +}; diff --git a/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillVariables.ts b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillVariables.ts new file mode 100644 index 00000000000000..33da8f4396d1ce --- /dev/null +++ b/packages/fluentui/react-northstar/src/themes/teams/components/Pill/pillVariables.ts @@ -0,0 +1,98 @@ +import { pxToRem } from '../../../../utils'; +import { SiteVariablesPrepared } from '@fluentui/styles'; + +export interface PillVariables { + background: string; + backgroundHover: string; + borderRadius: string; + roundedBorderRadius: string; + + // Outline + outlineBackground: string; + outlineBorderColor: string; + outlineDisabledborder: string; + + // Inverted + invertedBackground: string; + + // Disabled + disabledBackground: string; + disabledColor: string; + + // Smaller + smallerHeight: string; + smallerMinWidth: string; + smallerMargin: string; + + // Small + smallHeight: string; + smallMinWidth: string; + smallMargin: string; + + // medium + height: string; + minWidth: string; + margin: string; + smallerRoundedBorderRadius: string; + + // Content + contentPadding: string; + contentFontSize: string; + + // Content Smaller + contentPaddingSmaller: string; + contentFontSizeSmaller: string; + + // Content Small + contentPaddingSmall: string; + contentFontSizeSmall: string; +} + +export const pillVariables = (siteVars: SiteVariablesPrepared): PillVariables => ({ + background: siteVars.colorScheme.default.background3, + backgroundHover: siteVars.colorScheme.default.background1, + borderRadius: '9999px', + smallerRoundedBorderRadius: pxToRem(2), + + // Disabled + disabledBackground: siteVars.colorScheme.default.backgroundDisabled, + disabledColor: siteVars.colorScheme.default.foregroundDisabled, + + // Inverted + invertedBackground: siteVars.colorScheme.default.background, + + // Outline + outlineBackground: 'transparent', + // TODO: The design spec maps to Neutral Stroke 1 that is equivalent to gre[440] + // but we don't have this token + outlineBorderColor: siteVars.colorScheme.default.borderActive4, + outlineDisabledborder: siteVars.colorScheme.default.borderDisabled, + + // Smaller + smallerHeight: pxToRem(20), + smallerMinWidth: pxToRem(80), + smallerMargin: `${pxToRem(6)} ${pxToRem(2)}`, + + // Small + smallHeight: pxToRem(24), + smallMinWidth: pxToRem(80), + smallMargin: pxToRem(4), + + // Medium (default) + height: pxToRem(32), + minWidth: pxToRem(90), + margin: `${pxToRem(6)} ${pxToRem(4)}`, + roundedBorderRadius: pxToRem(4), + + // Content Smaller + contentPaddingSmaller: `${pxToRem(2)} ${pxToRem(8)}`, + contentFontSizeSmaller: pxToRem(12), + + // Content Small + contentFontSizeSmall: pxToRem(12), + contentPaddingSmall: `${pxToRem(4)} ${pxToRem(8)}`, + + // Content Medium + contentFontSize: pxToRem(14), + contentPadding: `${pxToRem(6)} ${pxToRem(8)}`, +}); diff --git a/packages/fluentui/react-northstar/src/themes/teams/types.ts b/packages/fluentui/react-northstar/src/themes/teams/types.ts index 5f319cdcc74176..13252725c9b5bd 100644 --- a/packages/fluentui/react-northstar/src/themes/teams/types.ts +++ b/packages/fluentui/react-northstar/src/themes/teams/types.ts @@ -105,6 +105,8 @@ import { SkeletonAvatarStylesProps } from '../../components/Skeleton/SkeletonAva import { SkeletonInputStylesProps } from '../../components/Skeleton/SkeletonInput'; import { SplitButtonToggleStylesProps } from '../../components/SplitButton/SplitButtonToggle'; import { AttachmentBodyStylesProps } from '../../components/Attachment/AttachmentBody'; +import { PillStylesProps } from '../../components/Pill/Pill'; +import { PillContentStylesProps } from '../../components/Pill/PillContent'; export type TeamsThemeStylesProps = { Accordion: AccordionStylesProps; @@ -162,6 +164,8 @@ export type TeamsThemeStylesProps = { MenuItemContent: MenuItemContentStylesProps; MenuItemWrapper: MenuItemWrapperStylesProps; MenuDivider: MenuDividerStylesProps; + Pill: PillStylesProps; + PillContent: PillContentStylesProps; Portal: PortalProps; PopupContent: PopupContentStylesProps; RadioGroup: RadioGroupProps;