From 459a83285a87d95715982e691e7879b81388aa00 Mon Sep 17 00:00:00 2001 From: Chris Holt Date: Wed, 15 Mar 2023 16:27:17 -0700 Subject: [PATCH 01/10] add button as a new web component --- .../src/button/button.definition.ts | 21 ++ .../src/button/button.options.ts | 53 +++ .../src/button/button.stories.ts | 207 +++++++++++ .../src/button/button.styles.ts | 336 ++++++++++++++++++ .../src/button/button.template.ts | 9 + packages/web-components/src/button/button.ts | 66 ++++ packages/web-components/src/button/define.ts | 4 + packages/web-components/src/button/index.ts | 5 + packages/web-components/src/index.ts | 1 + 9 files changed, 702 insertions(+) create mode 100644 packages/web-components/src/button/button.definition.ts create mode 100644 packages/web-components/src/button/button.options.ts create mode 100644 packages/web-components/src/button/button.stories.ts create mode 100644 packages/web-components/src/button/button.styles.ts create mode 100644 packages/web-components/src/button/button.template.ts create mode 100644 packages/web-components/src/button/button.ts create mode 100644 packages/web-components/src/button/define.ts create mode 100644 packages/web-components/src/button/index.ts diff --git a/packages/web-components/src/button/button.definition.ts b/packages/web-components/src/button/button.definition.ts new file mode 100644 index 00000000000000..dad785060b687b --- /dev/null +++ b/packages/web-components/src/button/button.definition.ts @@ -0,0 +1,21 @@ +import { FluentDesignSystem } from '../fluent-design-system.js'; +import { Button } from './button.js'; +import { styles } from './button.styles.js'; +import { template } from './button.template.js'; + +/** + * The Fluent Button Element. Implements {@link @microsoft/fast-foundation#Button }, + * {@link @microsoft/fast-foundation#buttonTemplate} + * + * @public + * @remarks + * HTML Element: \ + */ +export const definition = Button.compose({ + name: `${FluentDesignSystem.prefix}-button`, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); diff --git a/packages/web-components/src/button/button.options.ts b/packages/web-components/src/button/button.options.ts new file mode 100644 index 00000000000000..4d0bda85cb1947 --- /dev/null +++ b/packages/web-components/src/button/button.options.ts @@ -0,0 +1,53 @@ +import { ButtonOptions, ValuesOf } from '@microsoft/fast-foundation'; + +/** + * ButtonAppearance constants + * @public + */ +export const ButtonAppearance = { + primary: 'primary', + outline: 'outline', + subtle: 'subtle', + secondary: 'secondary', + transparent: 'transparent', +} as const; + +/** + * A Button can be filled, outline, ghost, inverted + * @public + */ +export type ButtonAppearance = ValuesOf; + +/** + * A Button can be square, circular or rounded. + * @public + */ +export const ButtonShape = { + circular: 'circular', + rounded: 'rounded', + square: 'square', +} as const; + +/** + * A Button can be square, circular or rounded + * @public + */ +export type ButtonShape = ValuesOf; + +/** + * A Button can be a size of small, medium or large. + * @public + */ +export const ButtonSize = { + small: 'small', + medium: 'medium', + large: 'large', +} as const; + +/** + * A Button can be on of several preset sizes. + * @public + */ +export type ButtonSize = ValuesOf; + +export { ButtonOptions }; diff --git a/packages/web-components/src/button/button.stories.ts b/packages/web-components/src/button/button.stories.ts new file mode 100644 index 00000000000000..dcacb5c49cd04a --- /dev/null +++ b/packages/web-components/src/button/button.stories.ts @@ -0,0 +1,207 @@ +import { html } from '@microsoft/fast-element'; +import type { Args, Meta } from '@storybook/html'; +import { renderComponent } from '../helpers.stories.js'; +import type { Button as FluentButton } from './button.js'; +import { ButtonAppearance, ButtonShape, ButtonSize } from './button.options.js'; +import './define.js'; + +type ButtonStoryArgs = Args & FluentButton; +type ButtonStoryMeta = Meta; + +const storyTemplate = html` + + ${x => x.content} + +`; + +export default { + title: 'Components/Button/Button', + args: { + content: 'Button', + disabled: false, + disabledFocusable: false, + }, + argTypes: { + appearance: { + options: Object.values(ButtonAppearance), + control: { + type: 'select', + }, + }, + shape: { + options: Object.values(ButtonShape), + control: { + type: 'select', + }, + }, + size: { + options: Object.values(ButtonSize), + 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 ButtonStoryMeta; + +export const Button = renderComponent(storyTemplate).bind({}); + +export const Appearance = renderComponent(html` + Default + Primary + Outline + Subtle + Transparent +`); + +export const Shape = renderComponent(html` + Rounded + Circular + Square +`); + +export const Size = renderComponent(html` + Small + Small with calendar icon + + Medium + Medium with calendar icon + + Large + Large with calendar icon + +`); + +export const Disabled = renderComponent(html` + Enabled state + Disabled state + Disabled focusable state + Enabled state + Disabled state + Disabled focusable state +`); + +export const WithLongText = renderComponent(html` + + Short text + Long text wraps after it hits the max width of the component +`); diff --git a/packages/web-components/src/button/button.styles.ts b/packages/web-components/src/button/button.styles.ts new file mode 100644 index 00000000000000..bcab2fd7bcf75c --- /dev/null +++ b/packages/web-components/src/button/button.styles.ts @@ -0,0 +1,336 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '@microsoft/fast-foundation'; +import { + borderRadiusCircular, + borderRadiusLarge, + borderRadiusMedium, + borderRadiusNone, + borderRadiusSmall, + colorBrandBackground, + colorBrandBackgroundHover, + colorBrandBackgroundPressed, + colorNeutralBackground1, + colorNeutralBackground1Hover, + colorNeutralBackground1Pressed, + colorNeutralBackgroundDisabled, + colorNeutralForeground1, + colorNeutralForeground1Hover, + colorNeutralForeground1Pressed, + colorNeutralForeground2, + colorNeutralForeground2BrandHover, + colorNeutralForeground2BrandPressed, + colorNeutralForeground2Hover, + colorNeutralForeground2Pressed, + colorNeutralForegroundDisabled, + colorNeutralForegroundOnBrand, + colorNeutralStroke1, + colorNeutralStroke1Hover, + colorNeutralStroke1Pressed, + colorNeutralStrokeDisabled, + colorStrokeFocus2, + colorSubtleBackground, + colorSubtleBackgroundHover, + colorSubtleBackgroundPressed, + colorTransparentBackground, + colorTransparentBackgroundHover, + colorTransparentBackgroundPressed, + colorTransparentStroke, + curveEasyEase, + durationFaster, + fontFamilyBase, + fontSizeBase200, + fontSizeBase300, + fontSizeBase400, + fontWeightRegular, + fontWeightSemibold, + lineHeightBase200, + lineHeightBase300, + lineHeightBase400, + shadow2, + shadow4, + spacingHorizontalL, + spacingHorizontalM, + spacingHorizontalS, + spacingHorizontalSNudge, + spacingHorizontalXS, + strokeWidthThick, + strokeWidthThin, +} from '../theme/design-tokens.js'; + +const buttonSpacingSmall = '3px'; +const buttonSpacingSmallWithIcon = '1px'; +const buttonSpacingMedium = '5px'; +const buttonSpacingLarge = '8px'; +const buttonSpacingLargeWithIcon = '7px'; + +// Need to support icon hover styles +export const styles = css` + ${display('inline-flex')} + + :host { + --icon-spacing: ${spacingHorizontalSNudge}; + contain: layout style; + } + + :host .control { + display: inline-flex; + align-items: center; + box-sizing: border-box; + justify-content: center; + text-decoration-line: none; + margin: 0; + outline-style: none; + background-color: ${colorNeutralBackground1}; + color: ${colorNeutralForeground1}; + border: ${strokeWidthThin} solid ${colorNeutralStroke1}; + padding: ${buttonSpacingMedium} ${spacingHorizontalM}; + min-width: 96px; + border-radius: ${borderRadiusMedium}; + font-size: ${fontSizeBase300}; + font-family: ${fontFamilyBase}; + font-weight: ${fontWeightSemibold}; + line-height: ${lineHeightBase300}; + transition-duration: ${durationFaster}; + transition-property: background, border, color; + transition-timing-function: ${curveEasyEase}; + cursor: pointer; + } + + .content { + display: inherit; + } + + :host(:hover) .control { + background-color: ${colorNeutralBackground1Hover}; + color: ${colorNeutralForeground1Hover}; + border-color: ${colorNeutralStroke1Hover}; + } + + :host(:hover:active) .control { + background-color: ${colorNeutralBackground1Pressed}; + border-color: ${colorNeutralStroke1Pressed}; + color: ${colorNeutralForeground1Pressed}; + outline-style: none; + } + + :host .control:focus-visible { + border-color: ${colorTransparentStroke}; + border-radius: ${borderRadiusMedium}; + outline: ${strokeWidthThick} solid ${colorTransparentStroke}; + box-shadow: ${shadow4}, 0 0 0 2px ${colorStrokeFocus2}; + z-index: 1; + } + + @media screen and (prefers-reduced-motion: reduce) { + transition-duration: 0.01ms; + } + + ::slotted(svg) { + font-size: 20px; + height: 20px; + width: 20px; + fill: currentColor; + } + + ::slotted([slot='start']) { + margin-inline-end: var(--icon-spacing); + } + + ::slotted([slot='end']) { + margin-inline-start: var(--icon-spacing); + } + + :host([icon-only]) .control { + min-width: 32px; + max-width: 32px; + } + + :host([size='small']) .control:focus-visible { + border-radius: ${borderRadiusSmall}; + } + + :host([size='large']) .control:focus-visible { + border-radius: ${borderRadiusLarge}; + } + + :host([shape='circular']) .control, + :host([shape='circular']) .control:focus-visible { + border-radius: ${borderRadiusCircular}; + } + + :host([shape='square']) .control, + :host([shape='square']) .control:focus-visible { + border-radius: ${borderRadiusNone}; + } + + :host([size='small']) { + --icon-spacing: ${spacingHorizontalXS}; + } + + :host([size='small']) .control { + min-width: 64px; + padding: ${buttonSpacingSmall} ${spacingHorizontalS}; + border-radius: ${buttonSpacingSmall}; + font-size: ${fontSizeBase200}; + line-height: ${lineHeightBase200}; + font-weight: ${fontWeightRegular}; + } + + :host([size='small'][icon-only]) .control { + min-width: 24px; + max-width: 24px; + } + + :host([size='small'][icon]) .control, + :host([size='small'][icon-only]) .control { + padding-block: ${buttonSpacingSmallWithIcon}; + } + + :host([size='large'][icon]) .control, + :host([size='large'][icon-only]) .control { + padding-block: ${buttonSpacingLargeWithIcon}; + } + + :host([size='large']) .control { + padding: ${buttonSpacingLarge} ${spacingHorizontalL}; + font-size: ${fontSizeBase400}; + line-height: ${lineHeightBase400}; + } + + :host([size='large'][icon-only]) .control { + min-width: 40px; + max-width: 40px; + } + + :host([size='large']) ::slotted(svg) { + font-size: 24px; + height: 24px; + width: 24px; + } + + :host([appearance='primary']) .control { + background-color: ${colorBrandBackground}; + color: ${colorNeutralForegroundOnBrand}; + border-color: transparent; + } + + :host([appearance='primary']:hover) .control { + background-color: ${colorBrandBackgroundHover}; + } + + :host([appearance='primary']:hover) .control, + :host([appearance='primary']:hover:active) .control { + border-color: transparent; + color: ${colorNeutralForegroundOnBrand}; + } + + :host([appearance='primary']:hover:active) .control { + background-color: ${colorBrandBackgroundPressed}; + } + + :host([appearance='primary']) .control:focus-visible { + border-color: ${colorNeutralForegroundOnBrand}; + box-shadow: ${shadow2}, 0 0 0 2px ${colorStrokeFocus2}; + } + + :host([disabled][appearance='primary']) .control, + :host([disabled][appearance='primary']:hover) .control, + :host([disabled][appearance='primary']:hover:active) .control, + :host([disabled-focusable][appearance='primary']) .control, + :host([disabled-focusable][appearance='primary']:hover) .control, + :host([disabled-focusable][appearance='primary']:hover:active) .control { + border-color: transparent; + } + + :host([appearance='outline']) .control { + background-color: ${colorTransparentBackground}; + } + + :host([appearance='outline']:hover) .control { + background-color: ${colorTransparentBackgroundHover}; + } + + :host([appearance='outline']:hover:active) .control { + background-color: ${colorTransparentBackgroundPressed}; + } + + :host([disabled][appearance='outline']) .control, + :host([disabled][appearance='outline']:hover) .control, + :host([disabled][appearance='outline']:hover:active) .control { + background-color: ${colorTransparentBackground}; + } + + :host([appearance='subtle']) .control { + background-color: ${colorSubtleBackground}; + color: ${colorNeutralForeground2}; + border-color: transparent; + } + + :host([appearance='subtle']:hover) .control { + background-color: ${colorSubtleBackgroundHover}; + color: ${colorNeutralForeground2Hover}; + border-color: transparent; + } + + :host([appearance='subtle']:hover:active) .control { + background-color: ${colorSubtleBackgroundPressed}; + color: ${colorNeutralForeground2Pressed}; + border-color: transparent; + } + + :host([disabled][appearance='subtle']) .control, + :host([disabled][appearance='subtle']:hover) .control, + :host([disabled][appearance='subtle']:hover:active) .control, + :host([disabled-focusable][appearance='subtle']) .control, + :host([disabled-focusable][appearance='subtle']:hover) .control, + :host([disabled-focusable][appearance='subtle']:hover:active) .control { + background-color: ${colorTransparentBackground}; + border-color: transparent; + } + + :host([appearance='subtle']:hover) ::slotted(svg) { + color: ${colorNeutralForeground2BrandHover}; + } + + :host([appearance='subtle']:hover:active) ::slotted(svg) { + color: ${colorNeutralForeground2BrandPressed}; + } + + :host([appearance='transparent']) .control { + background-color: ${colorTransparentBackground}; + color: ${colorNeutralForeground2}; + } + + :host([appearance='transparent']:hover) .control { + background-color: ${colorTransparentBackgroundHover}; + color: ${colorNeutralForeground2BrandHover}; + } + + :host([appearance='transparent']:hover:active) .control { + background-color: ${colorTransparentBackgroundPressed}; + color: ${colorNeutralForeground2BrandPressed}; + } + + :host([appearance='transparent']) .control, + :host([appearance='transparent']:hover) .control, + :host([appearance='transparent']:hover:active) .control { + border-color: transparent; + } + + :host(is:([disabled][appearance='transparent'], [disabled-focusabale][appearance="transparent"])) .control, + :host(is:([disabled][appearance='transparent'], [disabled-focusabale][appearance="transparent"]):hover) .control, + :host(is:([disabled][appearance='transparent'], [disabled-focusabale][appearance="transparent"]):hover:active) .control { + border-color: transparent; + background-color: ${colorTransparentBackground}; + } + + :host(:is([disabled], [disabled-focusable], [appearance][disabled], [appearance][disabled-focusable])) .control, + :host(:is([disabled], [disabled-focusable], [appearance][disabled], [appearance][disabled-focusable]):hover) .control, + :host(:is([disabled], [disabled-focusable], [appearance][disabled], [appearance][disabled-focusable]):hover:active) + .control { + background-color: ${colorNeutralBackgroundDisabled}; + border-color: ${colorNeutralStrokeDisabled}; + color: ${colorNeutralForegroundDisabled}; + cursor: not-allowed; + } +`; diff --git a/packages/web-components/src/button/button.template.ts b/packages/web-components/src/button/button.template.ts new file mode 100644 index 00000000000000..d001a16c8134a3 --- /dev/null +++ b/packages/web-components/src/button/button.template.ts @@ -0,0 +1,9 @@ +import { ElementViewTemplate } from '@microsoft/fast-element'; +import { buttonTemplate } from '@microsoft/fast-foundation'; +import type { Button } from './button.js'; + +/** + * The template for the Button component. + * @public + */ +export const template: ElementViewTemplate