Skip to content

Commit

Permalink
Add ability to automatically hide tab-group scroll control (#2128)
Browse files Browse the repository at this point in the history
* 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 <shmuel.leider@chabad.org>
Co-authored-by: Yehuda Ringler <yehuda.ringler@chabad.org>
Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
  • Loading branch information
4 people authored Sep 18, 2024
1 parent 6ce6e22 commit 65126e8
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 4 deletions.
142 changes: 142 additions & 0 deletions docs/pages/components/tab-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<sl-tab-group fixed-scroll-controls>
<sl-tab slot="nav" panel="tab-1">Tab 1</sl-tab>
<sl-tab slot="nav" panel="tab-2">Tab 2</sl-tab>
<sl-tab slot="nav" panel="tab-3">Tab 3</sl-tab>
<sl-tab slot="nav" panel="tab-4">Tab 4</sl-tab>
<sl-tab slot="nav" panel="tab-5">Tab 5</sl-tab>
<sl-tab slot="nav" panel="tab-6">Tab 6</sl-tab>
<sl-tab slot="nav" panel="tab-7">Tab 7</sl-tab>
<sl-tab slot="nav" panel="tab-8">Tab 8</sl-tab>
<sl-tab slot="nav" panel="tab-9">Tab 9</sl-tab>
<sl-tab slot="nav" panel="tab-10">Tab 10</sl-tab>
<sl-tab slot="nav" panel="tab-11">Tab 11</sl-tab>
<sl-tab slot="nav" panel="tab-12">Tab 12</sl-tab>
<sl-tab slot="nav" panel="tab-13">Tab 13</sl-tab>
<sl-tab slot="nav" panel="tab-14">Tab 14</sl-tab>
<sl-tab slot="nav" panel="tab-15">Tab 15</sl-tab>
<sl-tab slot="nav" panel="tab-16">Tab 16</sl-tab>
<sl-tab slot="nav" panel="tab-17">Tab 17</sl-tab>
<sl-tab slot="nav" panel="tab-18">Tab 18</sl-tab>
<sl-tab slot="nav" panel="tab-19">Tab 19</sl-tab>
<sl-tab slot="nav" panel="tab-20">Tab 20</sl-tab>
<sl-tab-panel name="tab-1">Tab panel 1</sl-tab-panel>
<sl-tab-panel name="tab-2">Tab panel 2</sl-tab-panel>
<sl-tab-panel name="tab-3">Tab panel 3</sl-tab-panel>
<sl-tab-panel name="tab-4">Tab panel 4</sl-tab-panel>
<sl-tab-panel name="tab-5">Tab panel 5</sl-tab-panel>
<sl-tab-panel name="tab-6">Tab panel 6</sl-tab-panel>
<sl-tab-panel name="tab-7">Tab panel 7</sl-tab-panel>
<sl-tab-panel name="tab-8">Tab panel 8</sl-tab-panel>
<sl-tab-panel name="tab-9">Tab panel 9</sl-tab-panel>
<sl-tab-panel name="tab-10">Tab panel 10</sl-tab-panel>
<sl-tab-panel name="tab-11">Tab panel 11</sl-tab-panel>
<sl-tab-panel name="tab-12">Tab panel 12</sl-tab-panel>
<sl-tab-panel name="tab-13">Tab panel 13</sl-tab-panel>
<sl-tab-panel name="tab-14">Tab panel 14</sl-tab-panel>
<sl-tab-panel name="tab-15">Tab panel 15</sl-tab-panel>
<sl-tab-panel name="tab-16">Tab panel 16</sl-tab-panel>
<sl-tab-panel name="tab-17">Tab panel 17</sl-tab-panel>
<sl-tab-panel name="tab-18">Tab panel 18</sl-tab-panel>
<sl-tab-panel name="tab-19">Tab panel 19</sl-tab-panel>
<sl-tab-panel name="tab-20">Tab panel 20</sl-tab-panel>
</sl-tab-group>
```

```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 = () => (
<SlTabGroup auto-hide-scroll-buttons>
<SlTab slot="nav" panel="tab-1">
Tab 1
</SlTab>
<SlTab slot="nav" panel="tab-2">
Tab 2
</SlTab>
<SlTab slot="nav" panel="tab-3">
Tab 3
</SlTab>
<SlTab slot="nav" panel="tab-4">
Tab 4
</SlTab>
<SlTab slot="nav" panel="tab-5">
Tab 5
</SlTab>
<SlTab slot="nav" panel="tab-6">
Tab 6
</SlTab>
<SlTab slot="nav" panel="tab-7">
Tab 7
</SlTab>
<SlTab slot="nav" panel="tab-8">
Tab 8
</SlTab>
<SlTab slot="nav" panel="tab-9">
Tab 9
</SlTab>
<SlTab slot="nav" panel="tab-10">
Tab 10
</SlTab>
<SlTab slot="nav" panel="tab-11">
Tab 11
</SlTab>
<SlTab slot="nav" panel="tab-12">
Tab 12
</SlTab>
<SlTab slot="nav" panel="tab-13">
Tab 13
</SlTab>
<SlTab slot="nav" panel="tab-14">
Tab 14
</SlTab>
<SlTab slot="nav" panel="tab-15">
Tab 15
</SlTab>
<SlTab slot="nav" panel="tab-16">
Tab 16
</SlTab>
<SlTab slot="nav" panel="tab-17">
Tab 17
</SlTab>
<SlTab slot="nav" panel="tab-18">
Tab 18
</SlTab>
<SlTab slot="nav" panel="tab-19">
Tab 19
</SlTab>
<SlTab slot="nav" panel="tab-20">
Tab 20
</SlTab>
<SlTabPanel name="tab-1">Tab panel 1</SlTabPanel>
<SlTabPanel name="tab-2">Tab panel 2</SlTabPanel>
<SlTabPanel name="tab-3">Tab panel 3</SlTabPanel>
<SlTabPanel name="tab-4">Tab panel 4</SlTabPanel>
<SlTabPanel name="tab-5">Tab panel 5</SlTabPanel>
<SlTabPanel name="tab-6">Tab panel 6</SlTabPanel>
<SlTabPanel name="tab-7">Tab panel 7</SlTabPanel>
<SlTabPanel name="tab-8">Tab panel 8</SlTabPanel>
<SlTabPanel name="tab-9">Tab panel 9</SlTabPanel>
<SlTabPanel name="tab-10">Tab panel 10</SlTabPanel>
<SlTabPanel name="tab-11">Tab panel 11</SlTabPanel>
<SlTabPanel name="tab-12">Tab panel 12</SlTabPanel>
<SlTabPanel name="tab-13">Tab panel 13</SlTabPanel>
<SlTabPanel name="tab-14">Tab panel 14</SlTabPanel>
<SlTabPanel name="tab-15">Tab panel 15</SlTabPanel>
<SlTabPanel name="tab-16">Tab panel 16</SlTabPanel>
<SlTabPanel name="tab-17">Tab panel 17</SlTabPanel>
<SlTabPanel name="tab-18">Tab panel 18</SlTabPanel>
<SlTabPanel name="tab-19">Tab panel 19</SlTabPanel>
<SlTabPanel name="tab-20">Tab panel 20</SlTabPanel>
</SlTabGroup>
);
```

### 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).
Expand Down
1 change: 1 addition & 0 deletions docs/pages/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti

## Next

- Scroll buttons for `<sl-tab-group>` auto hide when they are not clickable. The `fixed-scroll-controls` attribute can be included to prevent this behavior. [#2128]
- Added support for using `<sl-dropdown>` in `<sl-breadcrumb-item>` default slot [#2015]
- Added the `countdown` attribute to `<sl-alert>` 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]
Expand Down
51 changes: 47 additions & 4 deletions src/components/tab-group/tab-group.component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';

Expand All @@ -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'),
Expand Down Expand Up @@ -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) {
Expand All @@ -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 })
Expand Down Expand Up @@ -426,16 +457,22 @@ export default class SlTabGroup extends ShoelaceElement {
<sl-icon-button
part="scroll-button scroll-button--start"
exportparts="base:scroll-button__base"
class="tab-group__scroll-button tab-group__scroll-button--start"
class=${classMap({
'tab-group__scroll-button': true,
'tab-group__scroll-button--start': true,
'tab-group__scroll-button--start--hidden': this.shouldHideScrollStartButton
})}
name=${isRtl ? 'chevron-right' : 'chevron-left'}
library="system"
tabindex="-1"
aria-hidden="true"
label=${this.localize.term('scrollToStart')}
@click=${this.handleScrollToStart}
></sl-icon-button>
`
: ''}
<div class="tab-group__nav">
<div class="tab-group__nav" @scrollend=${this.updateScrollButtons}>
<div part="tabs" class="tab-group__tabs" role="tablist">
<div part="active-tab-indicator" class="tab-group__indicator"></div>
<sl-resize-observer @sl-resize=${this.syncIndicator}>
Expand All @@ -449,9 +486,15 @@ export default class SlTabGroup extends ShoelaceElement {
<sl-icon-button
part="scroll-button scroll-button--end"
exportparts="base:scroll-button__base"
class="tab-group__scroll-button tab-group__scroll-button--end"
class=${classMap({
'tab-group__scroll-button': true,
'tab-group__scroll-button--end': true,
'tab-group__scroll-button--end--hidden': this.shouldHideScrollEndButton
})}
name=${isRtl ? 'chevron-left' : 'chevron-right'}
library="system"
tabindex="-1"
aria-hidden="true"
label=${this.localize.term('scrollToEnd')}
@click=${this.handleScrollToEnd}
></sl-icon-button>
Expand Down
5 changes: 5 additions & 0 deletions src/components/tab-group/tab-group.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 65126e8

Please sign in to comment.