diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 3e271d61..e2743e37 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -16,6 +16,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis - Improved a11y for disabled buttons that are rendered as links - Improved a11y for `sl-button-group` - Removed `sl-show`, `sl-hide`, `sl-after-show`, `sl-after-hide` events from `sl-color-picker` (the color picker's visibility cannot be controlled programmatically so these shouldn't have been exposed; the dropdown events now bubble up so you can listen for those instead) +- Reworked `sl-button-group` so it doesn't require light DOM styles ## 2.0.0-beta.36 diff --git a/src/components/button-group/button-group.light-dom.scss b/src/components/button-group/button-group.light-dom.scss deleted file mode 100644 index 205bb386..00000000 --- a/src/components/button-group/button-group.light-dom.scss +++ /dev/null @@ -1,62 +0,0 @@ -// -// 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)); - - // Add a visual separator between solid buttons - &:not([type='default'])::part(base):not(:hover):not(:active):not(:focus):after { - content: ''; - position: absolute; - top: 0; - left: 0; - bottom: 0; - border-left: solid 1px #ffffff40; - } - } - - // 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 index b454a220..0b495403 100644 --- a/src/components/button-group/button-group.scss +++ b/src/components/button-group/button-group.scss @@ -7,9 +7,4 @@ .button-group { display: flex; flex-wrap: nowrap; - position: relative; -} - -::slotted(.sl-focus) { - z-index: 1; } diff --git a/src/components/button-group/button-group.ts b/src/components/button-group/button-group.ts index 33385f9b..b169bcd9 100644 --- a/src/components/button-group/button-group.ts +++ b/src/components/button-group/button-group.ts @@ -1,5 +1,5 @@ import { LitElement, html, unsafeCSS } from 'lit'; -import { customElement, property } from 'lit/decorators'; +import { customElement, property, query } from 'lit/decorators'; import styles from 'sass:./button-group.scss'; /** @@ -14,17 +14,45 @@ import styles from 'sass:./button-group.scss'; export default class SlButtonGroup extends LitElement { static styles = unsafeCSS(styles); + @query('slot') defaultSlot: HTMLSlotElement; + /** A label to use for the button group's `aria-label` attribute. */ @property() label = ''; handleFocus(event: CustomEvent) { - const button = event.target as HTMLElement; - button.classList.add('sl-focus'); + const button = findButton(event.target as HTMLElement); + button?.classList.add('sl-button-group__button--focus'); } handleBlur(event: CustomEvent) { - const button = event.target as HTMLElement; - button.classList.remove('sl-focus'); + const button = findButton(event.target as HTMLElement); + button?.classList.remove('sl-button-group__button--focus'); + } + + handleMouseOver(event: CustomEvent) { + const button = findButton(event.target as HTMLElement); + button?.classList.add('sl-button-group__button--hover'); + } + + handleMouseOut(event: CustomEvent) { + const button = findButton(event.target as HTMLElement); + button?.classList.remove('sl-button-group__button--hover'); + } + + handleSlotChange() { + const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[]; + + slottedElements.map(el => { + const index = slottedElements.indexOf(el); + const button = findButton(el); + + if (button) { + button.classList.add('sl-button-group__button'); + button.classList.toggle('sl-button-group__button--first', index === 0); + button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1); + button.classList.toggle('sl-button-group__button--last', index === slottedElements.length - 1); + } + }); } render() { @@ -36,13 +64,19 @@ export default class SlButtonGroup extends LitElement { aria-label=${this.label} @focusout=${this.handleBlur} @focusin=${this.handleFocus} + @mouseover=${this.handleMouseOver} + @mouseout=${this.handleMouseOut} > - + `; } } +function findButton(el: HTMLElement) { + return el.tagName.toLowerCase() === 'sl-button' ? el : el.querySelector('sl-button'); +} + declare global { interface HTMLElementTagNameMap { 'sl-button-group': SlButtonGroup; diff --git a/src/components/button/button.scss b/src/components/button/button.scss index 14c13367..df536b79 100644 --- a/src/components/button/button.scss +++ b/src/components/button/button.scss @@ -446,3 +446,49 @@ } } } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// Button groups support a variety of button types (e.g. buttons with tooltips, buttons as dropdown triggers, etc.). +// This means buttons aren't always direct descendants of the button group, thus we can't target them with the ::slotted +// selector. To work around this, the button group component does some magic to add these special classes to buttons and +// we style them here instead. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +:host(.sl-button-group__button--first) .button { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +:host(.sl-button-group__button--inner) .button { + border-radius: 0; +} + +:host(.sl-button-group__button--last) .button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +// All except the first +:host(.sl-button-group__button:not(.sl-button-group__button--first)) { + margin-left: calc(-1 * var(--sl-input-border-width)); +} + +// Add a visual separator between solid buttons +:host(.sl-button-group__button:not(.sl-button-group__button--focus, .sl-button-group__button--first, [type='default']):not(:hover, :active, :focus)) + .button:after { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + border-left: solid 1px #ffffff40; +} + +// Bump focused buttons up so their focus ring isn't clipped +:host(.sl-button-group__button--hover) { + z-index: 1; +} + +:host(.sl-button-group__button--focus) { + z-index: 2; +} diff --git a/src/styles/base.scss b/src/styles/base.scss index 5e752238..cc2bac0e 100644 --- a/src/styles/base.scss +++ b/src/styles/base.scss @@ -5,7 +5,6 @@ //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @use '../components/alert/alert.light-dom'; -@use '../components/button-group/button-group.light-dom'; :root { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////