Skip to content

Commit

Permalink
feat(button): add button to web components v3 (microsoft#27278)
Browse files Browse the repository at this point in the history
* add button as a new web component

* change files

* add package export

* add icon attr, fixup disabled styles

* update color => fill

* add disabled focusable click handling

* move to attribute syntax for aria-disabled

* remove icon attribute in favor of min-height to accommodate icons

* use long form for padding to ensure we catch browser defaults

* add basic readme with deltas
  • Loading branch information
chrisdholt authored and radium-v committed Apr 29, 2024
1 parent fc26116 commit c2bc2aa
Show file tree
Hide file tree
Showing 12 changed files with 760 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "feat(button): add button web component",
"packageName": "@fluentui/web-components",
"email": "chhol@microsoft.com",
"dependentChangeType": "patch"
}
4 changes: 4 additions & 0 deletions packages/web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
"types": "./dist/esm/badge/define.d.ts",
"default": "./dist/esm/badge/define.js"
},
"./button": {
"types": "./dist/esm/button/define.d.ts",
"default": "./dist/esm/button/define.js"
},
"./counter-badge": {
"types": "./dist/esm/counter-badge/define.d.ts",
"default": "./dist/esm/counter-badge/define.js"
Expand Down
48 changes: 48 additions & 0 deletions packages/web-components/src/button/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Button

The `Button` component allows users to commit a change or trigger an action via a single click or tap and is often found inside forms, dialogs, panels and/or pages.

## **Design Spec**

[Link to Button in Figma](https://www.figma.com/file/Nj9EBBvOZmS11zKNJfilVR/Button?node-id=1723%3A380&t=PNVwuI4rLXjxAFNJ-1)

<br />

## **Engineering Spec**

Fluent WC3 Button has feature parity with the Fluent UI React 9 Button implementation but not direct parity.

<br />

## Class: `Button`

<br />

### **Component Name**

`<fluent-button></fluent-button>`

<br />

## **Preparation**

<br />

### **Fluent Web Component v3 v.s Fluent React 9**

<br />

**Component and Slot Mapping**

| Fluent UI React 9 | Fluent Web Components 3 |
| ----------------- | ----------------------- |
| `<Button>` | `<fluent-button>` |

<br />

**Property Mapping**
| Fluent UI React 9 | Fluent Web Components 3 | Description of difference |
| ------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- |
| `icon`is a slot | The default slot or `start`, and `end` | In FUIR9, `icon` is a slot. In the web components implementation, an icon can be passed into the default slot and paired with an `icon-only` attribute, or supplementally in the `start` and/or `end` slots |
| `defaultValue` | `current-value` | In React, `defaultValue` sets the default value of form controls. In HTML, `value` is the default value, it doesn't update. [This RFC](https://github.com/microsoft/fast/issues/5119) provides more detail on how we came to decide to go with `current-value` instead of alternatives. Ultimately, the decision stems on staying aligned to the web platform as deviating would likely mean deviation for good. |
| `as` | A separate web component for anchor implementations | In FUIR9, HTML is returned so interpolating tags in the virtual DOM doesn't present a problem. In WC's, we can't safely interpolate tags and the cost to provide two sets of API's, one form associated and one not`icon` is a slot. In the web components implementation, conditional rendering brings with it a cost as both templates need to be enumerated. Additionally, button is a form associated element whereas anchors are not. For this reason, we'll provide an "anchor-button" as a separate component. |
21 changes: 21 additions & 0 deletions packages/web-components/src/button/button.definition.ts
Original file line number Diff line number Diff line change
@@ -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: \<fluent-button\>
*/
export const definition = Button.compose({
name: `${FluentDesignSystem.prefix}-button`,
template,
styles,
shadowOptions: {
delegatesFocus: true,
},
});
53 changes: 53 additions & 0 deletions packages/web-components/src/button/button.options.ts
Original file line number Diff line number Diff line change
@@ -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 secondary, primary, outline, subtle, transparent
* @public
*/
export type ButtonAppearance = ValuesOf<typeof ButtonAppearance>;

/**
* 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<typeof ButtonShape>;

/**
* 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<typeof ButtonSize>;

export { ButtonOptions };
208 changes: 208 additions & 0 deletions packages/web-components/src/button/button.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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<ButtonStoryArgs>;

const storyTemplate = html<ButtonStoryArgs>`
<fluent-button
appearance="${x => x.appearance}"
shape="${x => x.shape}"
size="${x => x.size}"
?disabled="${x => x.disabled}"
?disabled-focusable="${x => x.disabledFocusable}"
?icon-only="${x => x.iconOnly}"
?icon="${x => x.icon}"
>
${x => x.content}
</fluent-button>
`;

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<ButtonStoryArgs>`
<fluent-button>Default</fluent-button>
<fluent-button appearance="primary">Primary</fluent-button>
<fluent-button appearance="outline">Outline</fluent-button>
<fluent-button appearance="subtle">Subtle</fluent-button>
<fluent-button appearance="transparent">Transparent</fluent-button>
`);

export const Shape = renderComponent(html<ButtonStoryArgs>`
<fluent-button shape="rounded">Rounded</fluent-button>
<fluent-button shape="circular">Circular</fluent-button>
<fluent-button shape="square">Square</fluent-button>
`);

export const Size = renderComponent(html<ButtonStoryArgs>`
<fluent-button size="small">Small</fluent-button>
<fluent-button size="small" icon
><svg
fill="currentColor"
slot="start"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
>Small with calendar icon</fluent-button
>
<fluent-button size="small" icon-only aria-label="Small icon only button"
><svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
></fluent-button>
<fluent-button size="medium">Medium</fluent-button>
<fluent-button size="medium" icon
><svg
fill="currentColor"
slot="start"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
>Medium with calendar icon</fluent-button
>
<fluent-button size="medium" icon-only aria-label="Medium icon only button"
><svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
></fluent-button>
<fluent-button size="large">Large</fluent-button>
<fluent-button size="large" icon
><svg
fill="currentColor"
slot="start"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
>Large with calendar icon</fluent-button
>
<fluent-button size="large" icon-only aria-label="Large icon only button"
><svg
fill="currentColor"
aria-hidden="true"
width="1em"
height="1em"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 3A2.5 2.5 0 0117 5.5v9a2.5 2.5 0 01-2.5 2.5h-9A2.5 2.5 0 013 14.5v-9A2.5 2.5 0 015.5 3h9zm0 1h-9C4.67 4 4 4.67 4 5.5v9c0 .83.67 1.5 1.5 1.5h9c.83 0 1.5-.67 1.5-1.5v-9c0-.83-.67-1.5-1.5-1.5zM7 11a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zM7 7a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2zm3 0a1 1 0 110 2 1 1 0 010-2z"
fill="currentColor"
></path></svg
></fluent-button>
`);

export const Disabled = renderComponent(html<ButtonStoryArgs>`
<fluent-button>Enabled state</fluent-button>
<fluent-button disabled>Disabled state</fluent-button>
<fluent-button disabled-focusable>Disabled focusable state</fluent-button>
<fluent-button appearance="primary">Enabled state</fluent-button>
<fluent-button appearance="primary" disabled>Disabled state</fluent-button>
<fluent-button appearance="primary" disabled-focusable>Disabled focusable state</fluent-button>
`);

export const WithLongText = renderComponent(html<ButtonStoryArgs>`
<style>
.max-width {
width: 280px;
}
</style>
<fluent-button>Short text</fluent-button>
<fluent-button class="max-width">Long text wraps after it hits the max width of the component</fluent-button>
`);
Loading

0 comments on commit c2bc2aa

Please sign in to comment.