diff --git a/README.md b/README.md index 09354f6..62103ab 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ If none of the tabs have `aria-selected=true`, then the first tab will be select +``` ### Events @@ -109,6 +110,7 @@ In those cases, apply `data-tab-container-no-tabstop` to the `tabpanel` element. ### Unmanaged slots `` aims to simplify complex markup away in the ShadowDOM, so that the HTML you end up writing is overall less. However sometimes it can be useful to have _full_ control over the markup. Each of the `::part` selectors are also ``s, this means you can take any part and slot it, overriding the built-in ShadowDOM. + #### Unmanaged `tablist` You are able to provide your own `role=tablist` and `` will accommodate. This can be useful if you need extra presentational markup in the tablist. But remember: @@ -148,6 +150,7 @@ You are able to slot the `tablist-tab-wrapper` part. This slot manages the tabs
``` + #### Unmanaged `tablist-wrapper` If you want to take full control over the entire tab region, including managing the content before and after the tabs, then you can slot the `tablist-wrapper` element. Bear in mind if you're supplying this element that: diff --git a/examples/index.html b/examples/index.html index 12fc8e7..238e319 100644 --- a/examples/index.html +++ b/examples/index.html @@ -67,27 +67,6 @@

Horizontal (custom tablist and tablist-wrapper)

-

Horizontal (custom tablist and tablist-tab-wrapper)

- - -
-
- - - -
-
-
- Panel 1 -
- - -
-

Vertical (shadow tablist)

@@ -144,7 +123,7 @@

Set initially selected tab

Set default tab

- + diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index 93e625a..ebccbd9 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -1,5 +1,20 @@ const HTMLElement = globalThis.HTMLElement || (null as unknown as (typeof window)['HTMLElement']) -const manualSlotsSupported = 'assign' in (globalThis.HTMLSlotElement?.prototype || {}) + +// Function to see if manual slots are supported and if not, manual assign the slot attribute +const assignSlotWithFallback = + 'assign' in (globalThis.HTMLSlotElement?.prototype || {}) + ? (slot: HTMLSlotElement, ...elements: Element[]) => { + slot.assign(...elements) + } + : (slot: HTMLSlotElement, ...elements: Element[]) => { + const host = (slot.getRootNode() as ShadowRoot).host + for (const element of host.querySelectorAll(`[slot="${slot.name}"]`)) { + element.removeAttribute('slot') + } + for (const element of elements) { + element.setAttribute('slot', slot.name) + } + } export class TabContainerChangeEvent extends Event { constructor( @@ -95,7 +110,7 @@ export class TabContainerElement extends HTMLElement { static observedAttributes = ['vertical'] get #tabList() { - const wrapper = this.querySelector('[slot=tablist-wrapper],[slot=tablist-tab-wrapper]') + const wrapper = this.querySelector('[slot=tablist-wrapper]') if (wrapper?.closest(this.tagName) === this) { return wrapper.querySelector('[role=tablist]') as HTMLElement } @@ -112,7 +127,7 @@ export class TabContainerElement extends HTMLElement { } get #tabListTabWrapper() { - return this.shadowRoot!.querySelector('slot[part="tablist-tab-wrapper"]')! + return this.shadowRoot!.querySelector('div[part="tablist-tab-wrapper"]')! } get #beforeTabsSlot() { @@ -174,7 +189,7 @@ export class TabContainerElement extends HTMLElement { tabListContainer.style.display = 'flex' tabListContainer.setAttribute('part', 'tablist-wrapper') tabListContainer.setAttribute('name', 'tablist-wrapper') - const tabListTabWrapper = document.createElement('slot') + const tabListTabWrapper = document.createElement('div') tabListTabWrapper.setAttribute('part', 'tablist-tab-wrapper') tabListTabWrapper.setAttribute('name', 'tablist-tab-wrapper') const tabListSlot = document.createElement('slot') @@ -184,7 +199,6 @@ export class TabContainerElement extends HTMLElement { const panelSlot = document.createElement('slot') panelSlot.setAttribute('part', 'panel') panelSlot.setAttribute('name', 'panel') - panelSlot.setAttribute('role', 'presentation') const beforeTabSlot = document.createElement('slot') beforeTabSlot.setAttribute('part', 'before-tabs') beforeTabSlot.setAttribute('name', 'before-tabs') @@ -285,37 +299,15 @@ export class TabContainerElement extends HTMLElement { if (!this.#setupComplete) { const tabListSlot = this.#tabListSlot const tabListWrapper = this.#tabListWrapper - const tabListTabWrapper = this.#tabListTabWrapper const customTabList = this.querySelector('[role=tablist]') const customTabListWrapper = this.querySelector('[slot=tablist-wrapper]') - const customTabListTabWrapper = this.querySelector('[slot=tablist-tab-wrapper]') if (customTabListWrapper && customTabListWrapper.closest(this.tagName) === this) { - if (manualSlotsSupported) { - tabListWrapper.assign(customTabListWrapper) - } else { - customTabListWrapper.setAttribute('slot', 'tablist-wrapper') - } - } else if (customTabListTabWrapper && customTabListTabWrapper.closest(this.tagName) === this) { - if (manualSlotsSupported) { - tabListTabWrapper.assign(customTabListTabWrapper) - } else { - customTabListTabWrapper.setAttribute('slot', 'tablist-tab-wrapper') - } + assignSlotWithFallback(tabListWrapper, customTabListWrapper) } else if (customTabList && customTabList.closest(this.tagName) === this) { - if (manualSlotsSupported) { - tabListSlot.assign(customTabList) - } else { - customTabList.setAttribute('slot', 'tablist') - } + assignSlotWithFallback(tabListSlot, customTabList) } else { this.#tabListTabWrapper.role = 'tablist' - if (manualSlotsSupported) { - tabListSlot.assign(...[...this.children].filter(e => e.matches('[role=tab]'))) - } else { - for (const e of this.children) { - if (e.matches('[role=tab]')) e.setAttribute('slot', 'tablist') - } - } + assignSlotWithFallback(tabListSlot, ...[...this.children].filter(e => e.matches('[role=tab]'))) } const tabList = this.#tabList this.#reflectAttributeToShadow('aria-description', tabList) @@ -330,11 +322,7 @@ export class TabContainerElement extends HTMLElement { const afterSlotted: Element[] = [] let autoSlotted = beforeSlotted for (const child of this.children) { - if ( - child.getAttribute('role') === 'tab' || - child.getAttribute('role') === 'tablist' || - child.getAttribute('slot') === 'tablist-tab-wrapper' - ) { + if (child.getAttribute('role') === 'tab' || child.getAttribute('role') === 'tablist') { autoSlotted = afterTabSlotted continue } @@ -350,15 +338,9 @@ export class TabContainerElement extends HTMLElement { autoSlotted.push(child) } } - if (manualSlotsSupported) { - this.#beforeTabsSlot.assign(...beforeSlotted) - this.#afterTabsSlot.assign(...afterTabSlotted) - this.#afterPanelsSlot.assign(...afterSlotted) - } else { - for (const el of beforeSlotted) el.setAttribute('slot', 'before-tabs') - for (const el of afterTabSlotted) el.setAttribute('slot', 'after-tabs') - for (const el of afterSlotted) el.setAttribute('slot', 'after-panels') - } + assignSlotWithFallback(this.#beforeTabsSlot, ...beforeSlotted) + assignSlotWithFallback(this.#afterTabsSlot, ...afterTabSlotted) + assignSlotWithFallback(this.#afterPanelsSlot, ...afterSlotted) } const defaultTab = this.defaultTabIndex const defaultIndex = defaultTab >= 0 ? defaultTab : this.selectedTabIndex @@ -401,18 +383,11 @@ export class TabContainerElement extends HTMLElement { if (!panel.hasAttribute('tabindex') && !panel.hasAttribute('data-tab-container-no-tabstop')) { panel.setAttribute('tabindex', '0') } - if (!manualSlotsSupported && panel.hasAttribute('slot')) { - panel.removeAttribute('slot') - } } selectedTab.setAttribute('aria-selected', 'true') selectedTab.setAttribute('tabindex', '0') - if (manualSlotsSupported) { - this.#panelSlot.assign(selectedPanel) - } else { - selectedPanel.setAttribute('slot', 'panel') - } + assignSlotWithFallback(this.#panelSlot, selectedPanel) selectedPanel.hidden = false if (this.#setupComplete) { diff --git a/test/test.js b/test/test.js index f5dce71..a65a03d 100644 --- a/test/test.js +++ b/test/test.js @@ -719,54 +719,4 @@ describe('tab-container', function () { assert.deepStrictEqual(panels.map(isHidden), [false, true, true], 'First panel is visible') }) }) - - describe('with custom tablist-tab-wrapper', function () { - beforeEach(function () { - document.body.innerHTML = ` - -
-
- - - -
-
- -
- Panel 2 -
- -
- ` - tabs = Array.from(document.querySelectorAll('button')) - panels = Array.from(document.querySelectorAll('[role="tabpanel"]')) - }) - - afterEach(function () { - // Check to make sure we still have accessible markup after the test finishes running. - expect(document.body).to.be.accessible() - - document.body.innerHTML = '' - }) - - it('has accessible markup', function () { - expect(document.body).to.be.accessible() - }) - - it('the second tab is still selected', function () { - assert.deepStrictEqual(tabs.map(isSelected), [false, true, false], 'Second tab is selected') - assert.deepStrictEqual(panels.map(isHidden), [true, false, true], 'Second panel is visible') - }) - - it('selects the clicked tab', function () { - tabs[0].click() - - assert.deepStrictEqual(tabs.map(isSelected), [true, false, false], 'First tab is selected') - assert.deepStrictEqual(panels.map(isHidden), [false, true, true], 'First panel is visible') - }) - }) })