diff --git a/docs/pages/components/tab.md b/docs/pages/components/tab.md index 0c04e0ce..9b4d9e61 100644 --- a/docs/pages/components/tab.md +++ b/docs/pages/components/tab.md @@ -5,26 +5,6 @@ meta: layout: component --- -```html:preview -Tab -Active -Closable -Disabled -``` - -```jsx:react -import SlTab from '@shoelace-style/shoelace/dist/react/tab'; - -const App = () => ( - <> - Tab - Active - Closable - Disabled - -); -``` - :::tip Additional demonstrations can be found in the [tab group examples](/components/tab-group). ::: diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index 833af4a3..44204ac1 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -14,6 +14,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next +- `` `closable` property now reflects. [#2041] +- `` now implements a proper "roving tabindex" and `` is no longer tabbable by default. This aligns closer to the APG pattern for tabs. [#2041] - Fixed a bug in the submenu controller that prevented submenus from rendering in RTL without explicitly setting `dir` on the parent menu item [#1992] ## 2.15.1 diff --git a/src/components/tab-group/tab-group.component.ts b/src/components/tab-group/tab-group.component.ts index be582806..1485f31c 100644 --- a/src/components/tab-group/tab-group.component.ts +++ b/src/components/tab-group/tab-group.component.ts @@ -206,10 +206,16 @@ export default class SlTabGroup extends ShoelaceElement { index = 0; } - this.tabs[index].focus({ preventScroll: true }); + const currentTab = this.tabs[index]; + currentTab.tabIndex = 0; + currentTab.focus({ preventScroll: true }); if (this.activation === 'auto') { - this.setActiveTab(this.tabs[index], { scrollBehavior: 'smooth' }); + this.setActiveTab(currentTab, { scrollBehavior: 'smooth' }); + } else { + this.tabs.forEach(tabEl => { + tabEl.tabIndex = tabEl === currentTab ? 0 : -1; + }); } if (['top', 'bottom'].includes(this.placement)) { @@ -253,7 +259,10 @@ export default class SlTabGroup extends ShoelaceElement { this.activeTab = tab; // Sync active tab and panel - this.tabs.forEach(el => (el.active = el === this.activeTab)); + this.tabs.forEach(el => { + el.active = el === this.activeTab; + el.tabIndex = el === this.activeTab ? 0 : -1; + }); this.panels.forEach(el => (el.active = el.name === this.activeTab?.panel)); this.syncIndicator(); @@ -326,6 +335,7 @@ export default class SlTabGroup extends ShoelaceElement { // This stores tabs and panels so we can refer to a cache instead of calling querySelectorAll() multiple times. private syncTabsAndPanels() { this.tabs = this.getAllTabs({ includeDisabled: false }); + this.panels = this.getAllPanels(); this.syncIndicator(); diff --git a/src/components/tab/tab.component.ts b/src/components/tab/tab.component.ts index 1aa14fc8..d059f343 100644 --- a/src/components/tab/tab.component.ts +++ b/src/components/tab/tab.component.ts @@ -45,11 +45,13 @@ export default class SlTab extends ShoelaceElement { @property({ type: Boolean, reflect: true }) active = false; /** Makes the tab closable and shows a close button. */ - @property({ type: Boolean }) closable = false; + @property({ type: Boolean, reflect: true }) closable = false; /** Disables the tab and prevents selection. */ @property({ type: Boolean, reflect: true }) disabled = false; + tabIndex = -1; + connectedCallback() { super.connectedCallback(); this.setAttribute('role', 'tab'); @@ -68,16 +70,7 @@ export default class SlTab extends ShoelaceElement { @watch('disabled') handleDisabledChange() { this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false'); - } - - /** Sets focus to the tab. */ - focus(options?: FocusOptions) { - this.tab.focus(options); - } - - /** Removes focus from the tab. */ - blur() { - this.tab.blur(); + this.tabIndex = -1; } render() { @@ -93,7 +86,6 @@ export default class SlTab extends ShoelaceElement { 'tab--closable': this.closable, 'tab--disabled': this.disabled })} - tabindex=${this.disabled ? '-1' : '0'} > ${this.closable diff --git a/src/components/tab/tab.styles.ts b/src/components/tab/tab.styles.ts index e2e1e3a0..0473b3c6 100644 --- a/src/components/tab/tab.styles.ts +++ b/src/components/tab/tab.styles.ts @@ -27,15 +27,15 @@ export default css` color: var(--sl-color-primary-600); } - .tab:focus { - outline: none; + :host(:focus) { + outline: transparent; } - .tab:focus-visible:not(.tab--disabled) { + :host(:focus-visible):not([disabled]) { color: var(--sl-color-primary-600); } - .tab:focus-visible { + :host(:focus-visible) { outline: var(--sl-focus-ring); outline-offset: calc(-1 * var(--sl-focus-ring-width) - var(--sl-focus-ring-offset)); } diff --git a/src/components/tab/tab.test.ts b/src/components/tab/tab.test.ts index 48cc660a..362dcd77 100644 --- a/src/components/tab/tab.test.ts +++ b/src/components/tab/tab.test.ts @@ -12,6 +12,7 @@ describe('', () => { Test `); + await expect(el).to.be.accessible(); }); @@ -23,7 +24,7 @@ describe('', () => { expect(el.getAttribute('role')).to.equal('tab'); expect(el.getAttribute('aria-disabled')).to.equal('false'); expect(el.getAttribute('aria-selected')).to.equal('false'); - expect(base.getAttribute('tabindex')).to.equal('0'); + expect(el.getAttribute('tabindex')).to.equal('-1'); expect(base.getAttribute('class')).to.equal(' tab '); expect(el.active).to.equal(false); expect(el.closable).to.equal(false); @@ -38,7 +39,7 @@ describe('', () => { expect(el.disabled).to.equal(true); expect(el.getAttribute('aria-disabled')).to.equal('true'); expect(base.getAttribute('class')).to.equal(' tab tab--disabled '); - expect(base.getAttribute('tabindex')).to.equal('-1'); + expect(el.getAttribute('tabindex')).to.equal('-1'); }); it('should set active tab by attribute', async () => { @@ -49,7 +50,7 @@ describe('', () => { expect(el.active).to.equal(true); expect(el.getAttribute('aria-selected')).to.equal('true'); expect(base.getAttribute('class')).to.equal(' tab tab--active '); - expect(base.getAttribute('tabindex')).to.equal('0'); + expect(el.getAttribute('tabindex')).to.equal('-1'); }); it('should set closable by attribute', async () => { @@ -59,34 +60,34 @@ describe('', () => { const closeButton = el.shadowRoot!.querySelector('[part~="close-button"]'); expect(el.closable).to.equal(true); - expect(base.getAttribute('class')).to.equal(' tab tab--closable '); + expect(base.getAttribute('class')).to.match(/tab tab--closable/); expect(closeButton).not.to.be.null; }); describe('focus', () => { - it('should focus inner div', async () => { + it('should focus itself', async () => { const el = await fixture(html` Test `); - const base = el.shadowRoot!.querySelector('[part~="base"]')!; - el.focus(); await el.updateComplete; - expect(el.shadowRoot!.activeElement).to.equal(base); + expect(document.activeElement).to.equal(el); }); }); describe('blur', () => { - it('should blur inner div', async () => { + it('should blur itself', async () => { const el = await fixture(html` Test `); el.focus(); await el.updateComplete; + expect(document.activeElement).to.equal(el); + el.blur(); await el.updateComplete; - expect(el.shadowRoot!.activeElement).to.equal(null); + expect(document.activeElement).to.not.equal(el); }); });