diff --git a/change/@fluentui-web-components-b6b8fdad-d432-4c23-871e-ff051099582f.json b/change/@fluentui-web-components-b6b8fdad-d432-4c23-871e-ff051099582f.json new file mode 100644 index 00000000000000..3293939cda5954 --- /dev/null +++ b/change/@fluentui-web-components-b6b8fdad-d432-4c23-871e-ff051099582f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Adds Spinner as a web component", + "packageName": "@fluentui/web-components", + "email": "ryan@ryanmerrill.net", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/api-report.md b/packages/web-components/docs/api-report.md index 482ea6f2b5a68c..aa4ae9ca2ea41a 100644 --- a/packages/web-components/docs/api-report.md +++ b/packages/web-components/docs/api-report.md @@ -10,6 +10,7 @@ import { ElementViewTemplate } from '@microsoft/fast-element'; import { FASTElement } from '@microsoft/fast-element'; import { FASTElementDefinition } from '@microsoft/fast-element'; import { FASTProgress } from '@microsoft/fast-foundation'; +import { FASTProgressRing } from '@microsoft/fast-foundation'; import { StartEnd } from '@microsoft/fast-foundation'; import { StartEndOptions } from '@microsoft/fast-foundation'; import { StaticallyComposableHTML } from '@microsoft/fast-foundation'; @@ -1351,6 +1352,44 @@ export const spacingVerticalXXS: CSSDesignToken; // @public (undocumented) export const spacingVerticalXXXL: CSSDesignToken; +// @public +export class Spinner extends FASTProgressRing { + appearance: SpinnerAppearance; + size: SpinnerSize; +} + +// @public +export const SpinnerAppearance: { + readonly primary: "primary"; + readonly inverted: "inverted"; +}; + +// @public +export type SpinnerAppearance = ValuesOf; + +// @public +export const SpinnerDefinition: FASTElementDefinition; + +// @public +export const SpinnerSize: { + readonly tiny: "tiny"; + readonly extraSmall: "extra-small"; + readonly small: "small"; + readonly medium: "medium"; + readonly large: "large"; + readonly extraLarge: "extra-large"; + readonly huge: "huge"; +}; + +// @public +export type SpinnerSize = ValuesOf; + +// @public (undocumented) +export const SpinnerStyles: ElementStyles; + +// @public (undocumented) +export const SpinnerTemplate: ElementViewTemplate; + // @public (undocumented) export const strokeWidthThick: CSSDesignToken; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 7d602c3f90c628..8c93c239671325 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -39,6 +39,10 @@ "./progress-bar": { "types": "./dist/esm/progress-bar/define.d.ts", "default": "./dist/esm/progress-bar/define.js" + }, + "./spinner": { + "types": "./dist/esm/spinner/define.d.ts", + "default": "./dist/esm/spinner/define.js" } }, "scripts": { diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index e0440b0f08d5b1..78cf3190790a69 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -1,6 +1,7 @@ export * from './badge/index.js'; export * from './counter-badge/index.js'; export * from './progress-bar/index.js'; +export * from './spinner/index.js'; export * from './text/index.js'; export * from './theme/index.js'; diff --git a/packages/web-components/src/spinner/define.ts b/packages/web-components/src/spinner/define.ts new file mode 100644 index 00000000000000..8bca7912b206c8 --- /dev/null +++ b/packages/web-components/src/spinner/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './spinner.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/spinner/index.ts b/packages/web-components/src/spinner/index.ts new file mode 100644 index 00000000000000..2a6c2db892f33c --- /dev/null +++ b/packages/web-components/src/spinner/index.ts @@ -0,0 +1,5 @@ +export * from './spinner.js'; +export * from './spinner.options.js'; +export { template as SpinnerTemplate } from './spinner.template.js'; +export { styles as SpinnerStyles } from './spinner.styles.js'; +export { definition as SpinnerDefinition } from './spinner.definition.js'; diff --git a/packages/web-components/src/spinner/spinner.definition.ts b/packages/web-components/src/spinner/spinner.definition.ts new file mode 100644 index 00000000000000..edb459e3528552 --- /dev/null +++ b/packages/web-components/src/spinner/spinner.definition.ts @@ -0,0 +1,19 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { Spinner } from './spinner.js'; +import { styles } from './spinner.styles.js'; +import { template } from './spinner.template.js'; + +/** + * The Fluent Spinner Element. Implements {@link @microsoft/fast-foundation#ProgressRing }, + * {@link @microsoft/fast-foundation#progress-ringTemplate} + * + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = Spinner.compose({ + name: `${FluentDesignSystem.prefix}-spinner`, + template, + styles, +}); diff --git a/packages/web-components/src/spinner/spinner.options.ts b/packages/web-components/src/spinner/spinner.options.ts new file mode 100644 index 00000000000000..5d1c888833552c --- /dev/null +++ b/packages/web-components/src/spinner/spinner.options.ts @@ -0,0 +1,36 @@ +import { ValuesOf } from '@microsoft/fast-foundation'; + +/** + * SpinnerAppearance constants + * @public + */ +export const SpinnerAppearance = { + primary: 'primary', + inverted: 'inverted', +} as const; + +/** + * A Spinner's appearance can be either primary or inverted + * @public + */ +export type SpinnerAppearance = ValuesOf; + +/** + * SpinnerSize constants + * @public + */ +export const SpinnerSize = { + tiny: 'tiny', + extraSmall: 'extra-small', + small: 'small', + medium: 'medium', + large: 'large', + extraLarge: 'extra-large', + huge: 'huge', +} as const; + +/** + * A Spinner's size can be either small, tiny, extra-small, medium, large, extra-large, or huge + * @public + */ +export type SpinnerSize = ValuesOf; diff --git a/packages/web-components/src/spinner/spinner.stories.ts b/packages/web-components/src/spinner/spinner.stories.ts new file mode 100644 index 00000000000000..e160b592207ab9 --- /dev/null +++ b/packages/web-components/src/spinner/spinner.stories.ts @@ -0,0 +1,53 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../__test__/helpers.js'; +import { SpinnerAppearance, SpinnerSize } from './spinner.options.js'; +import './define.js'; + +type SpinnerStoryArgs = Args; +type SpinnerStoryMeta = Meta; + +const storyTemplate = html` + x.appearance} size=${x => x.size}> +`; + +export default { + title: 'Components/Spinner', + argTypes: { + appearance: { + description: 'The appearance of the spinner', + table: { + defaultValue: { summary: 'primary' }, + }, + control: { + type: 'select', + options: Object.values(SpinnerAppearance), + }, + defaultValue: 'primary', + }, + size: { + description: 'The size of the spinner', + table: { + defaultValue: { summary: 'medium' }, + }, + control: { + type: 'select', + options: Object.values(SpinnerSize), + }, + defaultValue: 'medium', + }, + }, + parameters: { + status: { + type: 'experimental', + }, + }, +} as SpinnerStoryMeta; + +export const Spinner = renderComponent(storyTemplate).bind({}); + +export const SpinnerInverted = renderComponent(html` +
+ +
+`); diff --git a/packages/web-components/src/spinner/spinner.styles.ts b/packages/web-components/src/spinner/spinner.styles.ts new file mode 100644 index 00000000000000..c4061520382f11 --- /dev/null +++ b/packages/web-components/src/spinner/spinner.styles.ts @@ -0,0 +1,96 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '@microsoft/fast-foundation'; +import { colorBrandStroke1, colorBrandStroke2, colorNeutralStrokeOnBrand2 } from '../theme/design-tokens.js'; + +export const styles = css` + ${display('flex')} + + :host { + display: flex; + align-items: center; + height: 32px; + width: 32px; + } + :host([size='tiny']) { + height: 20px; + width: 20px; + } + :host([size='extra-small']) { + height: 24px; + width: 24px; + } + :host([size='small']) { + height: 28px; + width: 28px; + } + :host([size='large']) { + height: 36px; + width: 36px; + } + :host([size='extra-large']) { + height: 40px; + width: 40px; + } + :host([size='huge']) { + height: 44px; + width: 44px; + } + .progress { + height: 100%; + width: 100%; + } + + .background { + fill: none; + stroke: ${colorBrandStroke2}; + stroke-width: 1.5px; + } + + :host([appearance='inverted']) .background { + stroke: rgba(255, 255, 255, 0.2); + } + + .determinate { + stroke: ${colorBrandStroke1}; + fill: none; + stroke-width: 1.5px; + stroke-linecap: round; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: all 0.2s ease-in-out; + } + + :host([appearance='inverted']) .determinite { + stroke: ${colorNeutralStrokeOnBrand2}; + } + + .indeterminate-indicator-1 { + stroke: ${colorBrandStroke1}; + fill: none; + stroke-width: 1.5px; + stroke-linecap: round; + transform-origin: 50% 50%; + transform: rotate(-90deg); + transition: all 0.2s ease-in-out; + animation: spin-infinite 3s cubic-bezier(0.53, 0.21, 0.29, 0.67) infinite; + } + + :host([appearance='inverted']) .indeterminate-indicator-1 { + stroke: ${colorNeutralStrokeOnBrand2}; + } + + @keyframes spin-infinite { + 0% { + stroke-dasharray: 0.01px 43.97px; + transform: rotate(0deg); + } + 50% { + stroke-dasharray: 21.99px 21.99px; + transform: rotate(450deg); + } + 100% { + stroke-dasharray: 0.01px 43.97px; + transform: rotate(1080deg); + } + } +`; diff --git a/packages/web-components/src/spinner/spinner.template.ts b/packages/web-components/src/spinner/spinner.template.ts new file mode 100644 index 00000000000000..473efe699a8283 --- /dev/null +++ b/packages/web-components/src/spinner/spinner.template.ts @@ -0,0 +1,24 @@ +import type { ElementViewTemplate } from '@microsoft/fast-element'; +import { progressRingTemplate } from '@microsoft/fast-foundation'; +import { Spinner } from './spinner.js'; + +export const template: ElementViewTemplate = progressRingTemplate({ + indeterminateIndicator: ` + + + + + `, +}); diff --git a/packages/web-components/src/spinner/spinner.ts b/packages/web-components/src/spinner/spinner.ts new file mode 100644 index 00000000000000..75a70af5f8949b --- /dev/null +++ b/packages/web-components/src/spinner/spinner.ts @@ -0,0 +1,30 @@ +import { attr } from '@microsoft/fast-element'; +import { FASTProgressRing } from '@microsoft/fast-foundation'; +import type { SpinnerAppearance, SpinnerSize } from './spinner.options.js'; + +/** + * The base class used for constructing a fluent-spinner custom element + * @public + */ +export class Spinner extends FASTProgressRing { + /** + * The size of the spinner + * + * @public + * @default 'medium' + * @remarks + * HTML Attribute: size + */ + @attr + public size: SpinnerSize; + + /** + * The appearance of the spinner + * @public + * @default 'primary' + * @remarks + * HTML Attribute: appearance + */ + @attr + public appearance: SpinnerAppearance; +}