Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(button): add button to web components v3 #27278

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": "[email protected]",
"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 = {
chrisdholt marked this conversation as resolved.
Show resolved Hide resolved
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