From 65126e875df15cb1913c1ce22f8ef7b2c0d1ebed Mon Sep 17 00:00:00 2001 From: Shmuel Leider Date: Wed, 18 Sep 2024 09:50:47 -0400 Subject: [PATCH] Add ability to automatically hide tab-group scroll control (#2128) * Add ability to automatically hide tab-group scroll control when there are no longer any tabs to show * code review updates * update and document how scroll buttons are hidden * AUTO-HIDE: Simplify * AUTO-HIDE: extract to constant * update changelog * include pr number in changelog update * add line * apply suggested changes * Prevent tab-group scroll buttons from being focusable * prettier fix * Set default for 'auto-hide-scroll-buttons' to true * Make auto hiding scroll buttons the default behavior * Update changelog * update changelog --------- Co-authored-by: Shmuel Leider Co-authored-by: Yehuda Ringler Co-authored-by: Cory LaViska --- docs/pages/components/tab-group.md | 142 ++++++++++++++++++ docs/pages/resources/changelog.md | 1 + .../tab-group/tab-group.component.ts | 51 ++++++- src/components/tab-group/tab-group.styles.ts | 5 + 4 files changed, 195 insertions(+), 4 deletions(-) diff --git a/docs/pages/components/tab-group.md b/docs/pages/components/tab-group.md index f85b27b7ee..6fb3ac9d94 100644 --- a/docs/pages/components/tab-group.md +++ b/docs/pages/components/tab-group.md @@ -411,6 +411,148 @@ const App = () => ( ); ``` +### Fixed scroll controls + +When tabs are scrolled all the way to one side, the scroll button on that side can't be clicked. Set the `fixed-scroll-controls` attribute to keep the effected button visible in that case. + +```html:preview + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5 + Tab 6 + Tab 7 + Tab 8 + Tab 9 + Tab 10 + Tab 11 + Tab 12 + Tab 13 + Tab 14 + Tab 15 + Tab 16 + Tab 17 + Tab 18 + Tab 19 + Tab 20 + + Tab panel 1 + Tab panel 2 + Tab panel 3 + Tab panel 4 + Tab panel 5 + Tab panel 6 + Tab panel 7 + Tab panel 8 + Tab panel 9 + Tab panel 10 + Tab panel 11 + Tab panel 12 + Tab panel 13 + Tab panel 14 + Tab panel 15 + Tab panel 16 + Tab panel 17 + Tab panel 18 + Tab panel 19 + Tab panel 20 + +``` + +```jsx:react +import SlTab from '@shoelace-style/shoelace/dist/react/tab'; +import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group'; +import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel'; + +const App = () => ( + + + Tab 1 + + + Tab 2 + + + Tab 3 + + + Tab 4 + + + Tab 5 + + + Tab 6 + + + Tab 7 + + + Tab 8 + + + Tab 9 + + + Tab 10 + + + Tab 11 + + + Tab 12 + + + Tab 13 + + + Tab 14 + + + Tab 15 + + + Tab 16 + + + Tab 17 + + + Tab 18 + + + Tab 19 + + + Tab 20 + + + Tab panel 1 + Tab panel 2 + Tab panel 3 + Tab panel 4 + Tab panel 5 + Tab panel 6 + Tab panel 7 + Tab panel 8 + Tab panel 9 + Tab panel 10 + Tab panel 11 + Tab panel 12 + Tab panel 13 + Tab panel 14 + Tab panel 15 + Tab panel 16 + Tab panel 17 + Tab panel 18 + Tab panel 19 + Tab panel 20 + +); +``` + ### Manual Activation When focused, keyboard users can press [[Left]] or [[Right]] to select the desired tab. By default, the corresponding tab panel will be shown immediately (automatic activation). You can change this behavior by setting `activation="manual"` which will require the user to press [[Space]] or [[Enter]] before showing the tab panel (manual activation). diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index aa864124cd..2c531b893e 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -14,6 +14,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next +- Scroll buttons for `` auto hide when they are not clickable. The `fixed-scroll-controls` attribute can be included to prevent this behavior. [#2128] - Added support for using `` in `` default slot [#2015] - Added the `countdown` attribute to `` to show a visual indicator before the toast disappears [#1899] - Fixed a bug that caused errors to show in the console when components disconnect before before `firstUpdated()` executes [#2127] diff --git a/src/components/tab-group/tab-group.component.ts b/src/components/tab-group/tab-group.component.ts index c0806e8438..1a93bcd990 100644 --- a/src/components/tab-group/tab-group.component.ts +++ b/src/components/tab-group/tab-group.component.ts @@ -1,7 +1,8 @@ +import '../../internal/scrollend-polyfill.js'; import { classMap } from 'lit/directives/class-map.js'; +import { eventOptions, property, query, state } from 'lit/decorators.js'; import { html } from 'lit'; import { LocalizeController } from '../../utilities/localize.js'; -import { property, query, state } from 'lit/decorators.js'; import { scrollIntoView } from '../../internal/scroll.js'; import { watch } from '../../internal/watch.js'; import componentStyles from '../../styles/component.styles.js'; @@ -61,6 +62,9 @@ export default class SlTabGroup extends ShoelaceElement { @state() private hasScrollControls = false; + @state() private shouldHideScrollStartButton = false; + @state() private shouldHideScrollEndButton = false; + /** The placement of the tabs. */ @property() placement: 'top' | 'bottom' | 'start' | 'end' = 'top'; @@ -73,6 +77,9 @@ export default class SlTabGroup extends ShoelaceElement { /** Disables the scroll arrows that appear when tabs overflow. */ @property({ attribute: 'no-scroll-controls', type: Boolean }) noScrollControls = false; + /** Prevent scroll buttons from being hidden when inactive. */ + @property({ attribute: 'fixed-scroll-controls', type: Boolean }) fixedScrollControls = false; + connectedCallback() { const whenAllDefined = Promise.all([ customElements.whenDefined('sl-tab'), @@ -366,6 +373,28 @@ export default class SlTabGroup extends ShoelaceElement { return nextTab; } + /** + * The reality of the browser means that we can't expect the scroll position to be exactly what we want it to be, so + * we add one pixel of wiggle room to our calculations. + */ + private scrollOffset = 1; + + @eventOptions({ passive: true }) + private updateScrollButtons() { + if (this.hasScrollControls && !this.fixedScrollControls) { + this.shouldHideScrollStartButton = this.scrollFromStart() <= this.scrollOffset; + this.shouldHideScrollEndButton = this.isScrolledToEnd(); + } + } + + private isScrolledToEnd() { + return this.scrollFromStart() + this.nav.clientWidth >= this.nav.scrollWidth - this.scrollOffset; + } + + private scrollFromStart() { + return this.localize.dir() === 'rtl' ? -this.nav.scrollLeft : this.nav.scrollLeft; + } + @watch('noScrollControls', { waitUntilFirstUpdate: true }) updateScrollControls() { if (this.noScrollControls) { @@ -379,6 +408,8 @@ export default class SlTabGroup extends ShoelaceElement { this.hasScrollControls = ['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth + 1; } + + this.updateScrollButtons(); } @watch('placement', { waitUntilFirstUpdate: true }) @@ -426,16 +457,22 @@ export default class SlTabGroup extends ShoelaceElement { ` : ''} -
+
@@ -449,9 +486,15 @@ export default class SlTabGroup extends ShoelaceElement { diff --git a/src/components/tab-group/tab-group.styles.ts b/src/components/tab-group/tab-group.styles.ts index 7180ec331e..4330bf4732 100644 --- a/src/components/tab-group/tab-group.styles.ts +++ b/src/components/tab-group/tab-group.styles.ts @@ -31,6 +31,11 @@ export default css` padding: 0 var(--sl-spacing-x-large); } + .tab-group--has-scroll-controls .tab-group__scroll-button--start--hidden, + .tab-group--has-scroll-controls .tab-group__scroll-button--end--hidden { + visibility: hidden; + } + .tab-group__body { display: block; overflow: auto;