diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bce3456..000b7c9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.0.0-beta.10 +- Added button group component - Added community page to the docs - Fixed a bug where many components would erroneously receive an `id` when using the custom elements bundle - Fixed a bug where tab groups weren't scrollable with the mouse diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7d01c9d8..f5dc09c9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -13,6 +13,7 @@ - [Avatar](/components/avatar.md) - [Badge](/components/badge.md) - [Button](/components/button.md) + - [Button Group](/components/button-group.md) - [Card](/components/card.md) - [Checkbox](/components/checkbox.md) - [Color Picker](/components/color-picker.md) diff --git a/docs/components/button-group.md b/docs/components/button-group.md new file mode 100644 index 00000000..702160dd --- /dev/null +++ b/docs/components/button-group.md @@ -0,0 +1,189 @@ +# Button Group + +[component-header:sl-button-group] + +Button groups can be used to group related buttons into sections. + +```html preview + + Left + Center + Right + +``` + +## Examples + +### Button Sizes + +All button sizes are supported, but avoid mixing sizes within the same button group. + +```html preview + + Left + Center + Right + + +

+ + + Left + Center + Right + + +

+ + + Left + Center + Right + +``` + +### Theme Buttons + +Theme buttons are supported through the button's `type` attribute. Types can be mixed as needed. + +```html preview + + Left + Center + Right + + +

+ + + Left + Center + Right + + +

+ + + Create + Edit + Destroy + +``` + +### Pill Buttons + +Pill buttons are supported through the button's `pill` attribute. + +```html preview + + Left + Center + Right + + +

+ + + Left + Center + Right + + +

+ + + Left + Center + Right + +``` + +### Dropdowns in Button Groups + +Dropdowns can be placed inside button groups as long as the trigger is a `` element. + +```html preview + + Button + Button + + + Dropdown + + Item 1 + Item 2 + Item 3 + + + + +``` + +### Tooltips in Button Groups + +Buttons can be wrapped in tooltips to provide more detail when the user interacts with them. + +```html preview + + + Left + + + + Center + + + + Right + + +``` + +### Toolbar Example + +Create interactive toolbars with button groups. + +```html preview +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +``` + +[component-metadata:sl-button-group] diff --git a/src/components.d.ts b/src/components.d.ts index d2404766..ee6b3f2f 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -110,6 +110,12 @@ export namespace Components { */ "value": string; } + interface SlButtonGroup { + /** + * A label to use for the button groups `aria-label` attribute. + */ + "label": string; + } interface SlCard { } interface SlCheckbox { @@ -930,6 +936,12 @@ declare global { prototype: HTMLSlButtonElement; new (): HTMLSlButtonElement; }; + interface HTMLSlButtonGroupElement extends Components.SlButtonGroup, HTMLStencilElement { + } + var HTMLSlButtonGroupElement: { + prototype: HTMLSlButtonGroupElement; + new (): HTMLSlButtonGroupElement; + }; interface HTMLSlCardElement extends Components.SlCard, HTMLStencilElement { } var HTMLSlCardElement: { @@ -1115,6 +1127,7 @@ declare global { "sl-avatar": HTMLSlAvatarElement; "sl-badge": HTMLSlBadgeElement; "sl-button": HTMLSlButtonElement; + "sl-button-group": HTMLSlButtonGroupElement; "sl-card": HTMLSlCardElement; "sl-checkbox": HTMLSlCheckboxElement; "sl-color-picker": HTMLSlColorPickerElement; @@ -1260,6 +1273,12 @@ declare namespace LocalJSX { */ "value"?: string; } + interface SlButtonGroup { + /** + * A label to use for the button groups `aria-label` attribute. + */ + "label"?: string; + } interface SlCard { } interface SlCheckbox { @@ -2143,6 +2162,7 @@ declare namespace LocalJSX { "sl-avatar": SlAvatar; "sl-badge": SlBadge; "sl-button": SlButton; + "sl-button-group": SlButtonGroup; "sl-card": SlCard; "sl-checkbox": SlCheckbox; "sl-color-picker": SlColorPicker; @@ -2183,6 +2203,7 @@ declare module "@stencil/core" { "sl-avatar": LocalJSX.SlAvatar & JSXBase.HTMLAttributes; "sl-badge": LocalJSX.SlBadge & JSXBase.HTMLAttributes; "sl-button": LocalJSX.SlButton & JSXBase.HTMLAttributes; + "sl-button-group": LocalJSX.SlButtonGroup & JSXBase.HTMLAttributes; "sl-card": LocalJSX.SlCard & JSXBase.HTMLAttributes; "sl-checkbox": LocalJSX.SlCheckbox & JSXBase.HTMLAttributes; "sl-color-picker": LocalJSX.SlColorPicker & JSXBase.HTMLAttributes; diff --git a/src/components/button-group/button-group.light-dom.scss b/src/components/button-group/button-group.light-dom.scss new file mode 100644 index 00000000..35833f3a --- /dev/null +++ b/src/components/button-group/button-group.light-dom.scss @@ -0,0 +1,52 @@ +// +// In general, we should avoid placing styles in the light DOM. However, we don't have a way to target slotted element +// parts, e.g. `::slotted(sl-button)::part(base)`, so we need these styles to make buttons groups work. +// +// The alternative approach is to set the styles with JavaScript, but this is more expensive because it requires +// multiple listeners + DOM traversals. +// +sl-button-group { + // First + > sl-button:first-child::part(base), + > sl-dropdown:first-child > sl-button[slot='trigger']::part(base), + > sl-tooltip:first-child > sl-button::part(base) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + // Last + > sl-button:last-child::part(base), + > sl-dropdown:last-child > sl-button[slot='trigger']::part(base), + > sl-tooltip:last-child > sl-button::part(base) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + // Interior + > sl-button:not(:first-child):not(:last-child)::part(base), + > sl-dropdown:not(:first-child):not(:last-child) > sl-button[slot='trigger']::part(base), + > sl-tooltip:not(:first-child):not(:last-child) > sl-button::part(base) { + border-radius: 0; + } + + // All except the first + > sl-button:not(:first-child), + > sl-dropdown:not(:first-child) > sl-button[slot='trigger'], + > sl-tooltip:not(:first-child) > sl-button { + margin-left: calc(-1 * var(--sl-input-border-width)); + } + + // Hover + > sl-button:hover, + > sl-dropdown:hover > sl-button[slot='trigger'], + > sl-tooltip:hover > sl-button { + z-index: 1; + } + + // Focus + > sl-button.sl-focus, + > sl-dropdown > sl-button[slot='trigger'].sl-focus, + > sl-tooltip > sl-button.sl-focus { + z-index: 2; + } +} diff --git a/src/components/button-group/button-group.scss b/src/components/button-group/button-group.scss new file mode 100644 index 00000000..b15f66ca --- /dev/null +++ b/src/components/button-group/button-group.scss @@ -0,0 +1,19 @@ +@import 'component'; + +:host { + display: inline-block; +} + +.button-group { + display: flex; + flex-wrap: nowrap; + position: relative; +} + +::slotted(.sl-hover) { + z-index: 1; +} + +::slotted(.sl-focus) { + z-index: 2; +} diff --git a/src/components/button-group/button-group.tsx b/src/components/button-group/button-group.tsx new file mode 100644 index 00000000..a239a5a2 --- /dev/null +++ b/src/components/button-group/button-group.tsx @@ -0,0 +1,55 @@ +import { Component, Prop, h } from '@stencil/core'; + +/** + * @since 2.0 + * @status stable + * + * @slot - One or more `` elements to display in the button group. + * + * @part base - The component's base wrapper. + */ + +@Component({ + tag: 'sl-button-group', + styleUrl: 'button-group.scss', + shadow: true +}) +export class ButtonGroup { + buttonGroup: HTMLElement; + + /** A label to use for the button groups `aria-label` attribute. */ + @Prop() label = ''; + + connectedCallback() { + this.handleFocus = this.handleFocus.bind(this); + this.handleBlur = this.handleBlur.bind(this); + } + + componentDidLoad() { + this.buttonGroup.addEventListener('slFocus', this.handleFocus); + this.buttonGroup.addEventListener('slBlur', this.handleBlur); + } + + disconnectedCallback() { + this.buttonGroup.removeEventListener('slFocus', this.handleFocus); + this.buttonGroup.removeEventListener('slBlur', this.handleBlur); + } + + handleFocus(event: CustomEvent) { + const button = event.target as HTMLElement; + button.classList.add('sl-focus'); + } + + handleBlur(event: CustomEvent) { + const button = event.target as HTMLElement; + button.classList.remove('sl-focus'); + } + + render() { + return ( +
(this.buttonGroup = el)} part="base" class="button-group" aria-label={this.label}> + +
+ ); + } +} diff --git a/src/styles/shoelace.scss b/src/styles/shoelace.scss index 120c21bc..e4755ae3 100644 --- a/src/styles/shoelace.scss +++ b/src/styles/shoelace.scss @@ -265,3 +265,9 @@ .sl-scroll-lock { overflow: hidden !important; } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Component light DOM styles - only follow this pattern when absolutely necessary! +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@import '../components/button-group/button-group.light-dom';