diff --git a/change/@fluentui-web-components-25e8f64c-845e-401f-a7f8-c9472a5720df.json b/change/@fluentui-web-components-25e8f64c-845e-401f-a7f8-c9472a5720df.json new file mode 100644 index 0000000000000..094c23a891496 --- /dev/null +++ b/change/@fluentui-web-components-25e8f64c-845e-401f-a7f8-c9472a5720df.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "add badge and counter badge as new components", + "packageName": "@fluentui/web-components", + "email": "chhol@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index c027d600e8202..2796247c3e572 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -8,8 +8,174 @@ import { ElementStyles } from '@microsoft/fast-element'; import { ElementViewTemplate } from '@microsoft/fast-element'; import { FASTElement } from '@microsoft/fast-element'; import { FASTElementDefinition } from '@microsoft/fast-element'; +import { StartEnd } from '@microsoft/fast-foundation'; +import { StartEndOptions } from '@microsoft/fast-foundation'; +import { StaticallyComposableHTML } from '@microsoft/fast-foundation'; import { ValuesOf } from '@microsoft/fast-foundation'; +// Warning: (ae-internal-mixed-release-tag) Mixed release tags are not allowed for "Badge" because one of its declarations is marked as @internal +// +// @public +export class Badge extends FASTElement { + appearance: BadgeAppearance; + color: BadgeColor; + shape: BadgeShape; + size: BadgeSize; +} + +// @internal +export interface Badge extends StartEnd { +} + +// @public +export const BadgeAppearance: { + readonly filled: "filled"; + readonly ghost: "ghost"; + readonly outline: "outline"; + readonly tint: "tint"; +}; + +// @public +export type BadgeAppearance = ValuesOf; + +// @public +export const BadgeColor: { + readonly brand: "brand"; + readonly danger: "danger"; + readonly important: "important"; + readonly informative: "informative"; + readonly severe: "severe"; + readonly subtle: "subtle"; + readonly success: "success"; + readonly warning: "warning"; +}; + +// @public +export type BadgeColor = ValuesOf; + +// @public +export const BadgeDefinition: FASTElementDefinition; + +// Warning: (ae-internal-missing-underscore) The name "BadgeOptions" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export type BadgeOptions = StartEndOptions & { + defaultContent?: StaticallyComposableHTML; +}; + +// @public +export const BadgeShape: { + readonly circular: "circular"; + readonly rounded: "rounded"; + readonly square: "square"; +}; + +// @public +export type BadgeShape = ValuesOf; + +// @public +export const BadgeSize: { + readonly tiny: "tiny"; + readonly extraSmall: "extra-small"; + readonly small: "small"; + readonly medium: "medium"; + readonly large: "large"; + readonly extraLarge: "extra-large"; +}; + +// @public +export type BadgeSize = ValuesOf; + +// @public +export const BadgeStyles: ElementStyles; + +// @public (undocumented) +export const BadgeTemplate: ElementViewTemplate; + +// Warning: (ae-internal-mixed-release-tag) Mixed release tags are not allowed for "CounterBadge" because one of its declarations is marked as @internal +// +// @public +export class CounterBadge extends FASTElement { + appearance: CounterBadgeAppearance; + color: CounterBadgeColor; + count: number; + // (undocumented) + protected countChanged(): void; + dot: boolean; + overflowCount: number; + // (undocumented) + protected overflowCountChanged(): void; + // @internal + setCount(): string | void; + shape: CounterBadgeShape; + showZero: boolean; + size: CounterBadgeSize; +} + +// @internal +export interface CounterBadge extends StartEnd { +} + +// @public +export const CounterBadgeAppearance: { + readonly filled: "filled"; + readonly ghost: "ghost"; +}; + +// @public +export type CounterBadgeAppearance = ValuesOf; + +// @public +export const CounterBadgeColor: { + readonly brand: "brand"; + readonly danger: "danger"; + readonly important: "important"; + readonly informative: "informative"; + readonly severe: "severe"; + readonly subtle: "subtle"; + readonly success: "success"; + readonly warning: "warning"; +}; + +// @public +export type CounterBadgeColor = ValuesOf; + +// @public +export const CounterBadgeDefinition: FASTElementDefinition; + +// Warning: (ae-incompatible-release-tags) The symbol "CounterBadgeOptions" is marked as @public, but its signature references "BadgeOptions" which is marked as @internal +// +// @public +export type CounterBadgeOptions = BadgeOptions; + +// @public +export const CounterBadgeShape: { + readonly circular: "circular"; + readonly rounded: "rounded"; +}; + +// @public +export type CounterBadgeShape = ValuesOf; + +// @public +export const CounterBadgeSize: { + readonly tiny: "tiny"; + readonly extraSmall: "extra-small"; + readonly small: "small"; + readonly medium: "medium"; + readonly large: "large"; + readonly extraLarge: "extra-large"; +}; + +// @public +export type CounterBadgeSize = ValuesOf; + +// @public +export const CounterBadgeStyles: ElementStyles; + +// @public +export const CounterBadgeTemplate: ElementViewTemplate; + // @public class Text_2 extends FASTElement { align: TextAlign; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index cc6f872c50bfd..9cd9deeb1d6f2 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -24,6 +24,14 @@ "types": "./dist/dts/index.d.ts", "default": "./dist/esm/index.js" }, + "./badge": { + "types": "./dist/esm/badge/define.d.ts", + "default": "./dist/esm/badge/define.js" + }, + "./counter-badge": { + "types": "./dist/esm/counter-badge/define.d.ts", + "default": "./dist/esm/counter-badge/define.js" + }, "./text": { "types": "./dist/esm/text/define.d.ts", "default": "./dist/esm/text/define.js" diff --git a/packages/web-components/src/badge/badge.definition.ts b/packages/web-components/src/badge/badge.definition.ts new file mode 100644 index 0000000000000..f15da6d05174f --- /dev/null +++ b/packages/web-components/src/badge/badge.definition.ts @@ -0,0 +1,19 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { Badge } from './badge.js'; +import { styles } from './badge.styles.js'; +import { template } from './badge.template.js'; + +/** + * The Fluent Badge Element. Implements {@link @microsoft/fast-foundation#Badge }, + * {@link @microsoft/fast-foundation#badgeTemplate} + * + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = Badge.compose({ + name: `${FluentDesignSystem.prefix}-badge`, + template, + styles, +}); diff --git a/packages/web-components/src/badge/badge.options.ts b/packages/web-components/src/badge/badge.options.ts new file mode 100644 index 0000000000000..9fd2a6613ca89 --- /dev/null +++ b/packages/web-components/src/badge/badge.options.ts @@ -0,0 +1,82 @@ +import { StartEndOptions, StaticallyComposableHTML, ValuesOf } from '@microsoft/fast-foundation'; +import type { Badge } from './badge.js'; + +/** + * @internal - marking as internal update when Badge PR for start/end is in + */ +export type BadgeOptions = StartEndOptions & { + defaultContent?: StaticallyComposableHTML; +}; + +/** + * BadgeAppearance constants + * @public + */ +export const BadgeAppearance = { + filled: 'filled', + ghost: 'ghost', + outline: 'outline', + tint: 'tint', +} as const; + +/** + * A Badge can be filled, outline, ghost, inverted + * @public + */ +export type BadgeAppearance = ValuesOf; + +/** + * BadgeColor constants + * @public + */ +export const BadgeColor = { + brand: 'brand', + danger: 'danger', + important: 'important', + informative: 'informative', + severe: 'severe', + subtle: 'subtle', + success: 'success', + warning: 'warning', +} as const; + +/** + * A Badge can be one of preset colors + * @public + */ +export type BadgeColor = ValuesOf; + +/** + * A Badge can be square, circular or rounded. + * @public + */ +export const BadgeShape = { + circular: 'circular', + rounded: 'rounded', + square: 'square', +} as const; + +/** + * A Badge can be one of preset colors + * @public + */ +export type BadgeShape = ValuesOf; + +/** + * A Badge can be square, circular or rounded. + * @public + */ +export const BadgeSize = { + tiny: 'tiny', + extraSmall: 'extra-small', + small: 'small', + medium: 'medium', + large: 'large', + extraLarge: 'extra-large', +} as const; + +/** + * A Badge can be on of several preset sizes. + * @public + */ +export type BadgeSize = ValuesOf; diff --git a/packages/web-components/src/badge/badge.stories.ts b/packages/web-components/src/badge/badge.stories.ts new file mode 100644 index 0000000000000..f931eadf524be --- /dev/null +++ b/packages/web-components/src/badge/badge.stories.ts @@ -0,0 +1,126 @@ +import { html, when } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../__test__/helpers.js'; +import type { Badge as FluentBadge } from './badge.js'; +import { BadgeAppearance, BadgeColor, BadgeShape, BadgeSize } from './badge.options.js'; +import './define.js'; + +type BadgeStoryArgs = Args & FluentBadge; +type BadgeStoryMeta = Meta; + +const storyTemplate = html` + + ${when( + x => x.iconPosition === 'start', + html``, + )} + ${x => x.content} + ${when( + x => x.iconPosition === 'end', + html``, + )} + +`; + +export default { + title: 'Components/Badge/Badge', + args: { + content: null, + }, + argTypes: { + appearance: { + options: Object.values(BadgeAppearance), + control: { + type: 'select', + }, + }, + color: { + options: Object.values(BadgeColor), + control: { + type: 'select', + }, + }, + shape: { + options: Object.values(BadgeShape), + control: { + type: 'select', + }, + }, + size: { + options: Object.values(BadgeSize), + control: { + type: 'select', + }, + }, + iconPosition: { + options: ['none', 'start', 'end'], + control: { + type: 'select', + }, + }, + content: { + control: 'text', + }, + }, +} as BadgeStoryMeta; + +export const Badge = renderComponent(storyTemplate).bind({}); + +export const Appearance = renderComponent(html` + filled + ghost + outline + tint +`); + +export const Color = renderComponent(html` + brand + danger + important + informative + severe + subtle + success + warning +`); + +export const Shape = renderComponent(html` + + + +`); + +export const Size = renderComponent(html` + + + + + + +`) as BadgeStoryMeta; diff --git a/packages/web-components/src/badge/badge.styles.ts b/packages/web-components/src/badge/badge.styles.ts new file mode 100644 index 0000000000000..64da6c5d16470 --- /dev/null +++ b/packages/web-components/src/badge/badge.styles.ts @@ -0,0 +1,36 @@ +import { css } from '@microsoft/fast-element'; +import { + badgeBaseStyles, + badgeFilledStyles, + badgeGhostStyles, + badgeOutlineStyles, + badgeSizeStyles, + badgeTintStyles, +} from '../styles/index.js'; +import { borderRadiusMedium, borderRadiusNone, borderRadiusSmall } from '../theme/design-tokens.js'; +// why is the border not showing up? +/** Badge styles + * @public + */ +export const styles = css` + :host([shape='square']) { + border-radius: ${borderRadiusNone}; + } + + :host([shape='rounded']) { + border-radius: ${borderRadiusMedium}; + } + + :host([shape='rounded'][size='tiny']), + :host([shape='rounded'][size='extra-small']), + :host([shape='rounded'][size='small']) { + border-radius: ${borderRadiusSmall}; + } + + ${badgeSizeStyles} + ${badgeFilledStyles} + ${badgeGhostStyles} + ${badgeOutlineStyles} + ${badgeTintStyles} + ${badgeBaseStyles} +`; diff --git a/packages/web-components/src/badge/badge.template.ts b/packages/web-components/src/badge/badge.template.ts new file mode 100644 index 0000000000000..1b4fab522ff87 --- /dev/null +++ b/packages/web-components/src/badge/badge.template.ts @@ -0,0 +1,18 @@ +import { ElementViewTemplate, html } from '@microsoft/fast-element'; +import { endSlotTemplate, startSlotTemplate, staticallyCompose } from '@microsoft/fast-foundation'; +import type { Badge } from './badge.js'; +import type { BadgeOptions } from './badge.options.js'; + +/** + * The template for the Badge component. + * @public + */ +export function badgeTemplate(options: BadgeOptions = {}): ElementViewTemplate { + return html` + ${startSlotTemplate(options)} + ${staticallyCompose(options.defaultContent)} + ${endSlotTemplate(options)} + `; +} + +export const template: ElementViewTemplate = badgeTemplate(); diff --git a/packages/web-components/src/badge/badge.ts b/packages/web-components/src/badge/badge.ts new file mode 100644 index 0000000000000..25481e08d57a5 --- /dev/null +++ b/packages/web-components/src/badge/badge.ts @@ -0,0 +1,58 @@ +import { attr, FASTElement } from '@microsoft/fast-element'; +import { applyMixins, StartEnd } from '@microsoft/fast-foundation'; +import { BadgeAppearance, BadgeColor, BadgeShape, BadgeSize } from './badge.options.js'; + +/** + * The base class used for constructing a fluent-badge custom element + * @public + */ +export class Badge extends FASTElement { + /** + * The appearance the badge should have. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: BadgeAppearance = BadgeAppearance.filled; + + /** + * The color the badge should have. + * + * @public + * @remarks + * HTML Attribute: color + */ + @attr + public color: BadgeColor = BadgeColor.brand; + /** + * The shape the badge should have. + * + * @public + * @remarks + * HTML Attribute: shape + */ + @attr + public shape: BadgeShape; + + /** + * The size the badge should have. + * + * @public + * @remarks + * HTML Attribute: size + */ + @attr + public size: BadgeSize; +} + +/** + * Mark internal because exporting class and interface of the same name + * confuses API extractor. + * TODO: Below will be unnecessary when Badge class gets updated + * @internal + */ +/* eslint-disable-next-line */ +export interface Badge extends StartEnd {} +applyMixins(Badge, StartEnd); diff --git a/packages/web-components/src/badge/define.ts b/packages/web-components/src/badge/define.ts new file mode 100644 index 0000000000000..d0e348b49c9c9 --- /dev/null +++ b/packages/web-components/src/badge/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './badge.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/badge/index.ts b/packages/web-components/src/badge/index.ts new file mode 100644 index 0000000000000..92e3fdcd87214 --- /dev/null +++ b/packages/web-components/src/badge/index.ts @@ -0,0 +1,5 @@ +export * from './badge.js'; +export * from './badge.options.js'; +export { template as BadgeTemplate } from './badge.template.js'; +export { styles as BadgeStyles } from './badge.styles.js'; +export { definition as BadgeDefinition } from './badge.definition.js'; diff --git a/packages/web-components/src/counter-badge/counter-badge.definition.ts b/packages/web-components/src/counter-badge/counter-badge.definition.ts new file mode 100644 index 0000000000000..2520d85972b6d --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.definition.ts @@ -0,0 +1,19 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { CounterBadge } from './counter-badge.js'; +import { styles } from './counter-badge.styles.js'; +import { template } from './counter-badge.template.js'; + +/** + * The Fluent CounterBadge Element. Implements {@link @microsoft/fast-foundation#Badge }, + * {@link @microsoft/fast-foundation#badgeTemplate} + * + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = CounterBadge.compose({ + name: `${FluentDesignSystem.prefix}-counter-badge`, + template, + styles, +}); diff --git a/packages/web-components/src/counter-badge/counter-badge.options.ts b/packages/web-components/src/counter-badge/counter-badge.options.ts new file mode 100644 index 0000000000000..02db6af0a483e --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.options.ts @@ -0,0 +1,78 @@ +import { ValuesOf } from '@microsoft/fast-foundation'; +import { BadgeOptions } from '../badge/badge.options.js'; + +/** + * CounterBadge options + * @public + */ +export type CounterBadgeOptions = BadgeOptions; + +/** + * CounterBadgeAppearance constants + * @public + */ +export const CounterBadgeAppearance = { + filled: 'filled', + ghost: 'ghost', +} as const; + +/** + * A CounterBadge can have an appearance of filled or ghost + * @public + */ +export type CounterBadgeAppearance = ValuesOf; + +/** + * CounterBadgeColor constants + * @public + */ +export const CounterBadgeColor = { + brand: 'brand', + danger: 'danger', + important: 'important', + informative: 'informative', + severe: 'severe', + subtle: 'subtle', + success: 'success', + warning: 'warning', +} as const; + +/** + * A CounterBadge can be one of preset colors + * @public + */ +export type CounterBadgeColor = ValuesOf; + +/** + * A CounterBadge shape can be circular or rounded. + * @public + */ +export const CounterBadgeShape = { + circular: 'circular', + rounded: 'rounded', +} as const; + +/** + * A CounterBadge can be one of preset colors + * @public + */ +export type CounterBadgeShape = ValuesOf; + +/** + * A CounterBadge can be square, circular or rounded. + * @public + */ +export const CounterBadgeSize = { + tiny: 'tiny', + extraSmall: 'extra-small', + small: 'small', + medium: 'medium', + large: 'large', + extraLarge: 'extra-large', +} as const; + +/** + * A CounterBadge can be on of several preset sizes. + * @public + */ +export type CounterBadgeSize = ValuesOf; diff --git a/packages/web-components/src/counter-badge/counter-badge.stories.ts b/packages/web-components/src/counter-badge/counter-badge.stories.ts new file mode 100644 index 0000000000000..aa3011ade4622 --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.stories.ts @@ -0,0 +1,123 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../__test__/helpers.js'; +import type { CounterBadge as FluentCounterBadge } from './counter-badge.js'; +import { + CounterBadgeAppearance, + CounterBadgeColor, + CounterBadgeShape, + CounterBadgeSize, +} from './counter-badge.options.js'; +import './define.js'; + +type CounterBadgeStoryArgs = Args & FluentCounterBadge; +type CounterBadgeStoryMeta = Meta; + +// TODO: Currently cannot show icon or content +// in the counter badge stories because it projects as slotted content +const storyTemplate = html` + +`; + +export default { + title: 'Components/Badge/Counter Badge', + args: { + dot: false, + showZero: false, + appearance: CounterBadgeAppearance.filled, + color: CounterBadgeColor.brand, + shape: CounterBadgeShape.circular, + count: 5, + }, + argTypes: { + appearance: { + options: Object.values(CounterBadgeAppearance), + control: { + type: 'select', + }, + }, + color: { + options: Object.values(CounterBadgeColor), + control: { + type: 'select', + }, + }, + shape: { + options: Object.values(CounterBadgeShape), + control: { + type: 'select', + }, + }, + size: { + options: Object.values(CounterBadgeSize), + control: { + type: 'select', + }, + }, + iconPosition: { + options: ['none', 'start', 'end'], + control: { + type: 'select', + }, + }, + dot: { + control: 'boolean', + }, + showZero: { + control: 'boolean', + }, + count: { + control: 'number', + }, + overflowCount: { + control: 'text', + }, + }, +} as CounterBadgeStoryMeta; + +export const CounterBadge = renderComponent(storyTemplate).bind({}); + +export const Appearance = renderComponent(html` + + +`); + +export const Color = renderComponent(html` + + + + + + + + +`); + +export const Shape = renderComponent(html` + + +`); + +export const Size = renderComponent(html` + + + + + + +`) as CounterBadgeStoryMeta; + +export const Dot = renderComponent(html``); + +export const ShowZero = renderComponent( + html``, +); diff --git a/packages/web-components/src/counter-badge/counter-badge.styles.ts b/packages/web-components/src/counter-badge/counter-badge.styles.ts new file mode 100644 index 0000000000000..7db498fccc673 --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.styles.ts @@ -0,0 +1,31 @@ +import { css } from '@microsoft/fast-element'; +import { badgeBaseStyles, badgeFilledStyles, badgeGhostStyles, badgeSizeStyles } from '../styles/index.js'; +import { borderRadiusMedium, borderRadiusSmall } from '../theme/design-tokens.js'; + +/** Badge styles + * @public + */ +export const styles = css` + :host([shape='rounded']) { + border-radius: ${borderRadiusMedium}; + } + + :host([shape='rounded'][size='tiny']), + :host([shape='rounded'][size='extra-small']), + :host([shape='rounded'][size='small']) { + border-radius: ${borderRadiusSmall}; + } + + ${badgeSizeStyles} + ${badgeFilledStyles} + ${badgeGhostStyles} + ${badgeBaseStyles} + + :host([dot]), + :host([dot][appearance][size]) { + min-width: auto; + width: 6px; + height: 6px; + padding: 0; + } +`; diff --git a/packages/web-components/src/counter-badge/counter-badge.template.ts b/packages/web-components/src/counter-badge/counter-badge.template.ts new file mode 100644 index 0000000000000..f0049a3abc528 --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.template.ts @@ -0,0 +1,16 @@ +import { ElementViewTemplate, html } from '@microsoft/fast-element'; +import { badgeTemplate } from '../badge/badge.template.js'; +import { CounterBadge } from './counter-badge.js'; +import { CounterBadgeOptions } from './counter-badge.options.js'; + +function composeTemplate(options: CounterBadgeOptions = {}): ElementViewTemplate { + return badgeTemplate({ + defaultContent: html`${x => x.setCount()}`, + }); +} + +/** + * The template for the Counter Badge component. + * @public + */ +export const template: ElementViewTemplate = composeTemplate(); diff --git a/packages/web-components/src/counter-badge/counter-badge.ts b/packages/web-components/src/counter-badge/counter-badge.ts new file mode 100644 index 0000000000000..38ca98713df4b --- /dev/null +++ b/packages/web-components/src/counter-badge/counter-badge.ts @@ -0,0 +1,125 @@ +import { attr, FASTElement, nullableNumberConverter } from '@microsoft/fast-element'; +import { applyMixins, StartEnd } from '@microsoft/fast-foundation'; +import { + CounterBadgeAppearance, + CounterBadgeColor, + CounterBadgeShape, + CounterBadgeSize, +} from './counter-badge.options.js'; + +/** + * The base class used for constructing a fluent-badge custom element + * @public + */ +export class CounterBadge extends FASTElement { + /** + * The appearance the badge should have. + * + * @public + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: CounterBadgeAppearance; + + /** + * The color the badge should have. + * + * @public + * @remarks + * HTML Attribute: color + */ + @attr + public color: CounterBadgeColor; + /** + * The shape the badge should have. + * + * @public + * @remarks + * HTML Attribute: shape + */ + @attr + public shape: CounterBadgeShape; + + /** + * The size the badge should have. + * + * @public + * @remarks + * HTML Attribute: size + */ + @attr + public size: CounterBadgeSize; + + /** + * The count the badge should have. + * + * @public + * @remarks + * HTML Attribute: count + */ + @attr({ converter: nullableNumberConverter }) + public count: number = 0; + protected countChanged() { + this.setCount(); + } + + /** + * Max number to be displayed + * + * @public + * @remarks + * HTML Attribute: overflow-count + */ + @attr({ attribute: 'overflow-count', converter: nullableNumberConverter }) + public overflowCount: number = 99; + protected overflowCountChanged() { + this.setCount(); + } + + /** + * If the badge should be shown when count is 0 + * + * @public + * @remarks + * HTML Attribute: show-zero + */ + @attr({ attribute: 'show-zero', mode: 'boolean' }) + public showZero: boolean = false; + + /** + * If a dot should be displayed without the count + * + * @public + * @remarks + * HTML Attribute: dot + */ + @attr({ mode: 'boolean' }) + public dot: boolean = false; + + /** + * @internal + * Function to set the count + * This is the default slotted content for the counter badge + * If children are slotted, that will override the value returned + */ + public setCount(): string | void { + const count: number | null = this.count ?? 0; + + if ((count !== 0 || this.showZero) && !this.dot) { + return count > this.overflowCount ? `${this.overflowCount}+` : `${count}`; + } + + return; + } +} + +/** + * Mark internal because exporting class and interface of the same name + * confuses API extractor. + * TODO: Below will be unnecessary when Badge class gets updated + * @internal + */ +/* eslint-disable-next-line */ +export interface CounterBadge extends StartEnd {} +applyMixins(CounterBadge, StartEnd); diff --git a/packages/web-components/src/counter-badge/define.ts b/packages/web-components/src/counter-badge/define.ts new file mode 100644 index 0000000000000..e08fef18aeccb --- /dev/null +++ b/packages/web-components/src/counter-badge/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './counter-badge.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/counter-badge/index.ts b/packages/web-components/src/counter-badge/index.ts new file mode 100644 index 0000000000000..159c31d2e2f94 --- /dev/null +++ b/packages/web-components/src/counter-badge/index.ts @@ -0,0 +1,5 @@ +export * from './counter-badge.js'; +export * from './counter-badge.options.js'; +export { template as CounterBadgeTemplate } from './counter-badge.template.js'; +export { styles as CounterBadgeStyles } from './counter-badge.styles.js'; +export { definition as CounterBadgeDefinition } from './counter-badge.definition.js'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index dd622fd56e566..bd9a8b7c5b0b6 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -1 +1,3 @@ +export * from './badge/index.js'; +export * from './counter-badge/index.js'; export * from './text/index.js'; diff --git a/packages/web-components/src/styles/index.ts b/packages/web-components/src/styles/index.ts new file mode 100644 index 0000000000000..8613859ad3e89 --- /dev/null +++ b/packages/web-components/src/styles/index.ts @@ -0,0 +1 @@ +export * from './partials/index.js'; diff --git a/packages/web-components/src/styles/partials/badge.partials.ts b/packages/web-components/src/styles/partials/badge.partials.ts new file mode 100644 index 0000000000000..cb1263e59d458 --- /dev/null +++ b/packages/web-components/src/styles/partials/badge.partials.ts @@ -0,0 +1,327 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '@microsoft/fast-foundation'; +import { + borderRadiusCircular, + colorBrandBackground, + colorBrandBackground2, + colorBrandForeground1, + colorBrandForeground2, + colorBrandStroke2, + colorNeutralBackground1, + colorNeutralBackground4, + colorNeutralBackground5, + colorNeutralForeground1, + colorNeutralForeground3, + colorNeutralForegroundInverted, + colorNeutralForegroundOnBrand, + colorNeutralForegroundStaticInverted, + colorNeutralStroke2, + colorNeutralStrokeAccessible, + colorPaletteDarkOrangeBackground1, + colorPaletteDarkOrangeBackground3, + colorPaletteDarkOrangeBorder1, + colorPaletteDarkOrangeForeground1, + colorPaletteDarkOrangeForeground3, + colorPaletteGreenBackground1, + colorPaletteGreenBackground3, + colorPaletteGreenBorder2, + colorPaletteGreenForeground1, + colorPaletteGreenForeground2, + colorPaletteGreenForeground3, + colorPaletteRedBackground1, + colorPaletteRedBackground3, + colorPaletteRedBorder1, + colorPaletteRedForeground1, + colorPaletteRedForeground3, + colorPaletteYellowBackground1, + colorPaletteYellowBackground3, + colorPaletteYellowBorder1, + colorPaletteYellowForeground2, + colorTransparentStroke, + fontFamilyBase, + fontSizeBase100, + fontSizeBase200, + fontWeightSemibold, + lineHeightBase100, + lineHeightBase200, + spacingHorizontalSNudge, + spacingHorizontalXS, + spacingHorizontalXXS, + strokeWidthThin, +} from '../../theme/design-tokens.js'; + +const textPadding = spacingHorizontalXXS; + +export const badgeBaseStyles = css.partial` + ${display('inline-flex')} :host { + position: relative; + box-sizing: border-box; + align-items: center; + justify-content: center; + font-family: ${fontFamilyBase}; + font-weight: ${fontWeightSemibold}; + font-size: ${fontSizeBase200}; + line-height: ${lineHeightBase200}; + min-width: 20px; + height: 20px; + padding-inline: calc(${spacingHorizontalXS} + ${textPadding}); + border-radius: ${borderRadiusCircular}; + border-color: ${colorTransparentStroke}; + background-color: ${colorBrandBackground}; + color: ${colorNeutralForegroundOnBrand}; + } + + ::slotted(svg) { + font-size: 12px; + } + + :host(:not([appearance='ghost']))::after { + position: absolute; + content: ''; + top: 0; + left: 0; + bottom: 0; + right: 0; + border-style: solid; + border-width: ${strokeWidthThin}; + border-color: inherit; + border-radius: inherit; + } +`; + +/** + * @public + * The badge's size styles + */ +export const badgeSizeStyles = css.partial` + :host([size='tiny']) { + width: 6px; + height: 6px; + font-size: 4px; + line-height: 4px; + padding-inline: 0; + min-width: unset; + } + :host([size='tiny']) ::slotted(svg) { + font-size: 6px; + } + :host([size='extra-small']) { + width: 10px; + height: 10px; + font-size: 6px; + line-height: 6px; + padding-inline: 0; + min-width: unset; + } + :host([size='extra-small']) ::slotted(svg) { + font-size: 10px; + } + :host([size='small']) { + min-width: 16px; + height: 16px; + font-size: ${fontSizeBase100}; + line-height: ${lineHeightBase100}; + padding-inline: calc(${spacingHorizontalXXS} + ${textPadding}); + } + :host([size='small']) ::slotted(svg) { + font-size: 12px; + } + :host([size='large']) { + min-width: 24px; + height: 24px; + font-size: ${fontSizeBase200}; + line-height: ${lineHeightBase200}; + padding-inline: calc(${spacingHorizontalXS} + ${textPadding}); + } + :host([size='large']) ::slotted(svg) { + font-size: 16px; + } + :host([size='extra-large']) { + min-width: 32px; + height: 32px; + font-size: ${fontSizeBase200}; + line-height: ${lineHeightBase200}; + padding-inline: calc(${spacingHorizontalSNudge} + ${textPadding}); + } + :host([size='extra-large']) ::slotted(svg) { + font-size: 20px; + } +`; + +/** + * The badge's `filled` appearance styles + * Filled appearance is default so do not + * Include that attribute as it's not present by default + * @public + */ +export const badgeFilledStyles = css.partial` + :host([color='danger']) { + background-color: ${colorPaletteRedBackground3}; + color: ${colorNeutralForegroundOnBrand}; + } + + :host([color='important']) { + background-color: ${colorNeutralForeground1}; + color: ${colorNeutralBackground1}; + } + + :host([color='informative']) { + background-color: ${colorNeutralBackground5}; + color: ${colorNeutralForeground3}; + } + + :host([color='severe']) { + background-color: ${colorPaletteDarkOrangeBackground3}; + color: ${colorNeutralForegroundOnBrand}; + } + + :host([color='subtle']) { + background-color: ${colorNeutralBackground1}; + color: ${colorNeutralForeground1}; + } + + :host([color='success']) { + background-color: ${colorPaletteGreenBackground3}; + color: ${colorNeutralForegroundOnBrand}; + } + + :host([color='warning']) { + background-color: ${colorPaletteYellowBackground3}; + color: ${colorNeutralForeground1}; + } +`; + +/** + * The badge's `ghost` appearance styles + * @public + */ +export const badgeGhostStyles = css.partial` + :host([appearance='ghost']) { + color: ${colorBrandBackground}; + background-color: initial; + } + + :host([appearance='ghost'][color='danger']) { + color: ${colorPaletteRedForeground3}; + } + + :host([appearance='ghost'][color='important']) { + color: ${colorNeutralForeground1}; + } + + :host([appearance='ghost'][color='informative']) { + color: ${colorNeutralForeground3}; + } + + :host([appearance='ghost'][color='severe']) { + color: ${colorPaletteDarkOrangeForeground3}; + } + + :host([appearance='ghost'][color='subtle']) { + color: ${colorNeutralForegroundInverted}; + } + + :host([appearance='ghost'][color='success']) { + color: ${colorPaletteGreenForeground3}; + } + + :host([appearance='ghost'][color='warning']) { + color: ${colorPaletteYellowForeground2}; + } +`; + +/** + * The badge's `outline` appearance styles + * @public + */ +export const badgeOutlineStyles = css.partial` + :host([appearance='outline']) { + border-color: currentColor; + color: ${colorBrandForeground1}; + background-color: initial; + } + + :host([appearance='outline'][color='danger']) { + color: ${colorPaletteRedForeground3}; + } + + :host([appearance='outline'][color='important']) { + color: ${colorNeutralForeground3}; + border-color: ${colorNeutralStrokeAccessible}; + } + + :host([appearance='outline'][color='informative']) { + color: ${colorNeutralForeground3}; + border-color: ${colorNeutralStroke2}; + } + + :host([appearance='outline'][color='severe']) { + color: ${colorPaletteDarkOrangeForeground3}; + } + + :host([appearance='outline'][color='subtle']) { + color: ${colorNeutralForegroundStaticInverted}; + } + + :host([appearance='outline'][color='success']) { + color: ${colorPaletteGreenForeground2}; + } + + :host([appearance='outline'][color='warning']) { + color: ${colorPaletteYellowForeground2}; + } +`; + +/** + * The badge's `tint` appearance styles + * @public + */ +export const badgeTintStyles = css.partial` + :host([appearance='tint']) { + background-color: ${colorBrandBackground2}; + color: ${colorBrandForeground2}; + border-color: ${colorBrandStroke2}; + } + + :host([appearance='tint'][color='danger']) { + background-color: ${colorPaletteRedBackground1}; + color: ${colorPaletteRedForeground1}; + border-color: ${colorPaletteRedBorder1}; + } + + :host([appearance='tint'][color='important']) { + background-color: ${colorNeutralForeground3}; + color: ${colorNeutralBackground1}; + border-color: ${colorTransparentStroke}; + } + + :host([appearance='tint'][color='informative']) { + background-color: ${colorNeutralBackground4}; + color: ${colorNeutralForeground3}; + border-color: ${colorNeutralStroke2}; + } + + :host([appearance='tint'][color='severe']) { + background-color: ${colorPaletteDarkOrangeBackground1}; + color: ${colorPaletteDarkOrangeForeground1}; + border-color: ${colorPaletteDarkOrangeBorder1}; + } + + :host([appearance='tint'][color='subtle']) { + background-color: ${colorNeutralBackground1}; + color: ${colorNeutralForeground3}; + border-color: ${colorNeutralStroke2}; + } + + :host([appearance='tint'][color='success']) { + background-color: ${colorPaletteGreenBackground1}; + color: ${colorPaletteGreenForeground1}; + border-color: ${colorPaletteGreenBorder2}; + } + + :host([appearance='tint'][color='warning']) { + background-color: ${colorPaletteYellowBackground1}; + color: ${colorPaletteYellowForeground2}; + border-color: ${colorPaletteYellowBorder1}; + } +`; diff --git a/packages/web-components/src/styles/partials/index.ts b/packages/web-components/src/styles/partials/index.ts new file mode 100644 index 0000000000000..c7515fc1e06b2 --- /dev/null +++ b/packages/web-components/src/styles/partials/index.ts @@ -0,0 +1 @@ +export * from './badge.partials.js';