diff --git a/change/@fluentui-web-components-6f5897dc-d10c-4bbc-941b-54a948630c4c.json b/change/@fluentui-web-components-6f5897dc-d10c-4bbc-941b-54a948630c4c.json new file mode 100644 index 0000000000000..23047291676d1 --- /dev/null +++ b/change/@fluentui-web-components-6f5897dc-d10c-4bbc-941b-54a948630c4c.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat(compound-button): add compound button as new web component", + "packageName": "@fluentui/web-components", + "email": "chhol@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 9690f5342ea6a..cc90f8b8fed2a 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -44,6 +44,10 @@ "types": "./dist/esm/button/define.d.ts", "default": "./dist/esm/button/define.js" }, + "./compound-button": { + "types": "./dist/esm/compound-button/define.d.ts", + "default": "./dist/esm/compound-button/define.js" + }, "./counter-badge": { "types": "./dist/esm/counter-badge/define.d.ts", "default": "./dist/esm/counter-badge/define.js" diff --git a/packages/web-components/src/button/button.styles.ts b/packages/web-components/src/button/button.styles.ts index 449ce868bb47e..cee6f0935ad93 100644 --- a/packages/web-components/src/button/button.styles.ts +++ b/packages/web-components/src/button/button.styles.ts @@ -64,6 +64,7 @@ export const styles = css` :host { --icon-spacing: ${spacingHorizontalSNudge}; contain: layout style; + vertical-align: middle; } :host .control { diff --git a/packages/web-components/src/compound-button/compound-button.definition.ts b/packages/web-components/src/compound-button/compound-button.definition.ts new file mode 100644 index 0000000000000..cec288120c4da --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.definition.ts @@ -0,0 +1,21 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { CompoundButton } from './compound-button.js'; +import { styles } from './compound-button.styles.js'; +import { template } from './compound-button.template.js'; + +/** + * The Fluent Compound Button Element. Implements {@link @microsoft/fast-foundation#Button }, + * {@link @microsoft/fast-foundation#buttonTemplate} + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = CompoundButton.compose({ + name: `${FluentDesignSystem.prefix}-compound-button`, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); diff --git a/packages/web-components/src/compound-button/compound-button.options.ts b/packages/web-components/src/compound-button/compound-button.options.ts new file mode 100644 index 0000000000000..66f6c1090d42c --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.options.ts @@ -0,0 +1,40 @@ +import { ButtonOptions, ValuesOf } from '@microsoft/fast-foundation'; +import { ButtonAppearance, ButtonShape, ButtonSize } from '../button/button.options.js'; + +/** + * Compound Button Appearance constants + * @public + */ +export const CompoundButtonAppearance = ButtonAppearance; + +/** + * A Compound Button can be secondary, primary, outline, subtle, transparent + * @public + */ +export type CompoundButtonAppearance = ValuesOf; + +/** + * A Compound Button can be square, circular or rounded. + * @public + */ +export const CompoundButtonShape = ButtonShape; + +/** + * A Compound Button can be square, circular or rounded + * @public + */ +export type CompoundButtonShape = ValuesOf; + +/** + * A Compound Button can be a size of small, medium or large. + * @public + */ +export const CompoundButtonSize = ButtonSize; + +/** + * A Compound Button can be on of several preset sizes. + * @public + */ +export type CompoundButtonSize = ValuesOf; + +export { ButtonOptions as CompoundButtonOptions }; diff --git a/packages/web-components/src/compound-button/compound-button.stories.ts b/packages/web-components/src/compound-button/compound-button.stories.ts new file mode 100644 index 0000000000000..505cbb960703a --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.stories.ts @@ -0,0 +1,239 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../helpers.stories.js'; +import type { CompoundButton as FluentCompoundButton } from './compound-button.js'; +import { CompoundButtonAppearance, CompoundButtonShape, CompoundButtonSize } from './compound-button.options.js'; +import './define.js'; + +type CompoundButtonStoryArgs = Args & FluentCompoundButton; +type CompoundButtonStoryMeta = Meta; + +const storyTemplate = html` + + ${x => x.content} + ${x => x.description} + +`; + +export default { + title: 'Components/Button/Compound Button', + args: { + content: 'Button', + description: 'Secondary content', + disabled: false, + disabledFocusable: false, + }, + argTypes: { + appearance: { + options: Object.values(CompoundButtonAppearance), + control: { + type: 'select', + }, + }, + shape: { + options: Object.values(CompoundButtonShape), + control: { + type: 'select', + }, + }, + size: { + options: Object.values(CompoundButtonSize), + control: { + type: 'select', + }, + }, + disabled: { + control: 'boolean', + table: { + type: { + summary: 'Sets the disabled state of the component', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + disabledFocusable: { + control: 'boolean', + table: { + type: { + summary: 'The component is disabled but still focusable', + }, + defaultValue: { + summary: 'false', + }, + }, + }, + content: { + control: 'Button text', + }, + }, +} as CompoundButtonStoryMeta; + +export const Button = renderComponent(storyTemplate).bind({}); + +export const Appearance = renderComponent(html` + DefaultDescription content + PrimaryDescription content + OutlineDescription content + SubtleDescription content + TransparentDescription content +`); + +export const Shape = renderComponent(html` + RoundedDescription content + CircularDescription content + SquareDescription content +`); + +export const Size = renderComponent(html` + SmallDescription content + Small with calendar iconDescription content + + MediumDescription content + Medium with calendar icon + + LargeDescription content + Large with calendar iconDescription content + +`); + +export const Disabled = renderComponent(html` + Enabled stateDescription content + Disabled stateDescription content + Disabled focusable stateDescription content + Enabled stateDescription content + Disabled stateDescription content + Disabled focusable stateDescription content +`); + +export const WithLongText = renderComponent(html` + + Short textDescription content + Long text wraps after it hits the max width of the componentDescription content +`); diff --git a/packages/web-components/src/compound-button/compound-button.styles.ts b/packages/web-components/src/compound-button/compound-button.styles.ts new file mode 100644 index 0000000000000..66493e56950dd --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.styles.ts @@ -0,0 +1,122 @@ +import { css } from '@microsoft/fast-element'; +import { styles as ButtonStyles } from '../button/button.styles.js'; +import { + colorNeutralForeground2, + colorNeutralForeground2BrandHover, + colorNeutralForeground2BrandPressed, + colorNeutralForeground2Hover, + colorNeutralForeground2Pressed, + colorNeutralForegroundDisabled, + colorNeutralForegroundOnBrand, + fontSizeBase200, + fontSizeBase300, + fontSizeBase400, + fontWeightRegular, + lineHeightBase300, + lineHeightBase400, + spacingHorizontalS, + spacingHorizontalSNudge, + spacingHorizontalXS, +} from '../theme/design-tokens.js'; + +// Need to support icon hover styles +export const styles = css` + ${ButtonStyles} + + :host .control, + :host(:is([size])) .control { + gap: 12px; + height: auto; + padding-top: 14px; + padding-inline: 12px; + padding-bottom: 16px; + font-size: ${fontSizeBase300}; + line-height: ${lineHeightBase300}; + } + + .content { + display: flex; + flex-direction: column; + text-align: start; + } + + ::slotted([slot='description']) { + color: ${colorNeutralForeground2}; + line-height: 100%; + font-size: ${fontSizeBase200}; + font-weight: ${fontWeightRegular}; + } + + ::slotted(svg), + :host([size='large']) ::slotted(svg) { + font-size: 40px; + height: 40px; + width: 40px; + } + + :host(:hover) ::slotted([slot='description']) { + color: ${colorNeutralForeground2Hover}; + } + + :host(:active) ::slotted([slot='description']) { + color: ${colorNeutralForeground2Pressed}; + } + + :host(:is([appearance='primary'], [appearance='primary']:hover, [appearance='primary']:active)) + ::slotted([slot='description']) { + color: ${colorNeutralForegroundOnBrand}; + } + + :host(:is([appearance='subtle'], [appearance='subtle']:hover, [appearance='subtle']:active)) + ::slotted([slot='description']), + :host([appearance='transparent']) ::slotted([slot='description']) { + color: ${colorNeutralForeground2}; + } + + :host([appearance='transparent']:hover) ::slotted([slot='description']) { + color: ${colorNeutralForeground2BrandHover}; + } + + :host([appearance='transparent']:active) ::slotted([slot='description']) { + color: ${colorNeutralForeground2BrandPressed}; + } + + :host(:is([disabled], [disabled][appearance], [disabled-focusable], [disabled-focusable][appearance])) + ::slotted([slot='description']) { + color: ${colorNeutralForegroundDisabled}; + } + + :host([size='small']) .control { + padding: 8px; + padding-bottom: 10px; + } + + :host([icon-only]) .control { + min-width: 52px; + max-width: 52px; + padding: ${spacingHorizontalSNudge}; + } + + :host([icon-only][size='small']) .control { + min-width: 48px; + max-width: 48px; + padding: ${spacingHorizontalXS}; + } + + :host([icon-only][size='large']) .control { + min-width: 56px; + max-width: 56px; + padding: ${spacingHorizontalS}; + } + + :host([size='large']) .control { + padding-top: 18px; + padding-inline: 16px; + padding-bottom: 20px; + font-size: ${fontSizeBase400}; + line-height: ${lineHeightBase400}; + } + :host([size='large']) ::slotted([slot='description']) { + font-size: ${fontSizeBase300}; + } +`; diff --git a/packages/web-components/src/compound-button/compound-button.template.ts b/packages/web-components/src/compound-button/compound-button.template.ts new file mode 100644 index 0000000000000..015bf0089643e --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.template.ts @@ -0,0 +1,63 @@ +import { ElementViewTemplate, html, ref, slotted } from '@microsoft/fast-element'; +import { endSlotTemplate, startSlotTemplate } from '@microsoft/fast-foundation'; +import type { CompoundButton } from './compound-button.js'; +import type { CompoundButtonOptions } from './compound-button.options.js'; + +/** + * The template for the Compound Button component. + * @public + */ +export function buttonTemplate(options: CompoundButtonOptions = {}): ElementViewTemplate { + return html` + + `; +} + +/** + * The template for the Button component. + * @public + */ +export const template: ElementViewTemplate = buttonTemplate(); diff --git a/packages/web-components/src/compound-button/compound-button.ts b/packages/web-components/src/compound-button/compound-button.ts new file mode 100644 index 0000000000000..6efd06e50ef06 --- /dev/null +++ b/packages/web-components/src/compound-button/compound-button.ts @@ -0,0 +1,7 @@ +import { Button } from '../button/button.js'; + +/** + * The base class used for constructing a fluent-compound-button custom element + * @public + */ +export class CompoundButton extends Button {} diff --git a/packages/web-components/src/compound-button/define.ts b/packages/web-components/src/compound-button/define.ts new file mode 100644 index 0000000000000..c87e2bd253047 --- /dev/null +++ b/packages/web-components/src/compound-button/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { definition } from './compound-button.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/packages/web-components/src/compound-button/index.ts b/packages/web-components/src/compound-button/index.ts new file mode 100644 index 0000000000000..e4e0bc601f76e --- /dev/null +++ b/packages/web-components/src/compound-button/index.ts @@ -0,0 +1,5 @@ +export * from './compound-button.js'; +export * from './compound-button.options.js'; +export { template as CompoundButtonTemplate } from './compound-button.template.js'; +export { styles as CompoundButtonStyles } from './compound-button.styles.js'; +export { definition as CompoundButtonDefinition } from './compound-button.definition.js'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index 59cea6af59bb2..3d5135434b822 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -3,6 +3,7 @@ export * from './accordion-item/index.js'; export * from './avatar/index.js'; export * from './badge/index.js'; export * from './button/index.js'; +export * from './compound-button/index.js'; export * from './counter-badge/index.js'; export * from './divider/index.js'; export * from './image/index.js';