Skip to content

Commit 029187e

Browse files
committed
allow overriding of the tablist-wrapper
1 parent 66334bc commit 029187e

File tree

4 files changed

+133
-34
lines changed

4 files changed

+133
-34
lines changed

custom-elements.json

+6
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,12 @@
439439
"privacy": "private",
440440
"readonly": true
441441
},
442+
{
443+
"kind": "field",
444+
"name": "#tabListWrapper",
445+
"privacy": "private",
446+
"readonly": true
447+
},
442448
{
443449
"kind": "field",
444450
"name": "#tabListTabWrapper",

examples/index.html

+21
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,27 @@ <h2>Horizontal (custom tablist)</h2>
4848

4949
<h2>Horizontal (custom tablist and tablist-wrapper)</h2>
5050

51+
<tab-container>
52+
<div slot="tablist-wrapper">
53+
<div role="tablist" aria-label="Horizontal Tabs Example">
54+
<button type="button" id="tab-one" role="tab">Tab one</button>
55+
<button type="button" id="tab-two" role="tab">Tab two</button>
56+
<button type="button" id="tab-three" role="tab">Tab three</button>
57+
</div>
58+
</div>
59+
<div role="tabpanel" aria-labelledby="tab-one">
60+
Panel 1
61+
</div>
62+
<div role="tabpanel" aria-labelledby="tab-two" hidden>
63+
Panel 2
64+
</div>
65+
<div role="tabpanel" aria-labelledby="tab-three" hidden>
66+
Panel 3
67+
</div>
68+
</tab-container>
69+
70+
<h2>Horizontal (custom tablist and tablist-tab-wrapper)</h2>
71+
5172
<tab-container>
5273
<div slot="tablist-tab-wrapper">
5374
<div role="tablist" aria-label="Horizontal Tabs Example">

src/tab-container-element.ts

+55-34
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export class TabContainerElement extends HTMLElement {
9595
static observedAttributes = ['vertical']
9696

9797
get #tabList() {
98+
const wrapper = this.querySelector('[slot=tablist-wrapper],[slot=tablist-tab-wrapper]')
99+
if (wrapper?.closest(this.tagName) === this) {
100+
return wrapper.querySelector('[role=tablist]') as HTMLElement
101+
}
98102
const slot = this.#tabListSlot
99103
if (this.#tabListTabWrapper.hasAttribute('role')) {
100104
return this.#tabListTabWrapper
@@ -103,6 +107,10 @@ export class TabContainerElement extends HTMLElement {
103107
}
104108
}
105109

110+
get #tabListWrapper() {
111+
return this.shadowRoot!.querySelector<HTMLSlotElement>('slot[part="tablist-wrapper"]')!
112+
}
113+
106114
get #tabListTabWrapper() {
107115
return this.shadowRoot!.querySelector<HTMLSlotElement>('slot[part="tablist-tab-wrapper"]')!
108116
}
@@ -162,9 +170,10 @@ export class TabContainerElement extends HTMLElement {
162170
connectedCallback(): void {
163171
this.#internals ||= this.attachInternals ? this.attachInternals() : null
164172
const shadowRoot = this.shadowRoot || this.attachShadow({mode: 'open', slotAssignment: 'manual'})
165-
const tabListContainer = document.createElement('div')
173+
const tabListContainer = document.createElement('slot')
166174
tabListContainer.style.display = 'flex'
167175
tabListContainer.setAttribute('part', 'tablist-wrapper')
176+
tabListContainer.setAttribute('name', 'tablist-wrapper')
168177
const tabListTabWrapper = document.createElement('slot')
169178
tabListTabWrapper.setAttribute('part', 'tablist-tab-wrapper')
170179
tabListTabWrapper.setAttribute('name', 'tablist-tab-wrapper')
@@ -275,13 +284,22 @@ export class TabContainerElement extends HTMLElement {
275284
selectTab(index: number): void {
276285
if (!this.#setupComplete) {
277286
const tabListSlot = this.#tabListSlot
287+
const tabListWrapper = this.#tabListWrapper
288+
const tabListTabWrapper = this.#tabListTabWrapper
278289
const customTabList = this.querySelector('[role=tablist]')
279-
const customTabListWrapper = this.querySelector('[slot=tablist-tab-wrapper]')
290+
const customTabListWrapper = this.querySelector('[slot=tablist-wrapper]')
291+
const customTabListTabWrapper = this.querySelector('[slot=tablist-tab-wrapper]')
280292
if (customTabListWrapper && customTabListWrapper.closest(this.tagName) === this) {
281293
if (manualSlotsSupported) {
282-
tabListSlot.assign(customTabListWrapper)
294+
tabListWrapper.assign(customTabListWrapper)
283295
} else {
284-
customTabListWrapper.setAttribute('slot', 'tablist')
296+
customTabListWrapper.setAttribute('slot', 'tablist-wrapper')
297+
}
298+
} else if (customTabListTabWrapper && customTabListTabWrapper.closest(this.tagName) === this) {
299+
if (manualSlotsSupported) {
300+
tabListTabWrapper.assign(customTabListTabWrapper)
301+
} else {
302+
customTabListTabWrapper.setAttribute('slot', 'tablist-tab-wrapper')
285303
}
286304
} else if (customTabList && customTabList.closest(this.tagName) === this) {
287305
if (manualSlotsSupported) {
@@ -305,40 +323,43 @@ export class TabContainerElement extends HTMLElement {
305323
if (this.vertical) {
306324
this.#tabList.setAttribute('aria-orientation', 'vertical')
307325
}
308-
const beforeSlotted: Element[] = []
309-
const afterTabSlotted: Element[] = []
310-
const afterSlotted: Element[] = []
311-
let autoSlotted = beforeSlotted
312-
for (const child of this.children) {
313-
if (
314-
child.getAttribute('role') === 'tab' ||
315-
child.getAttribute('role') === 'tablist' ||
316-
child.getAttribute('slot') === 'tablist-tab-wrapper'
317-
) {
318-
autoSlotted = afterTabSlotted
319-
continue
320-
}
321-
if (child.getAttribute('role') === 'tabpanel') {
322-
autoSlotted = afterSlotted
323-
continue
326+
const bringsOwnWrapper = this.querySelector('[slot=tablist-wrapper]')?.closest(this.tagName) === this
327+
if (!bringsOwnWrapper) {
328+
const beforeSlotted: Element[] = []
329+
const afterTabSlotted: Element[] = []
330+
const afterSlotted: Element[] = []
331+
let autoSlotted = beforeSlotted
332+
for (const child of this.children) {
333+
if (
334+
child.getAttribute('role') === 'tab' ||
335+
child.getAttribute('role') === 'tablist' ||
336+
child.getAttribute('slot') === 'tablist-tab-wrapper'
337+
) {
338+
autoSlotted = afterTabSlotted
339+
continue
340+
}
341+
if (child.getAttribute('role') === 'tabpanel') {
342+
autoSlotted = afterSlotted
343+
continue
344+
}
345+
if (child.getAttribute('slot') === 'before-tabs') {
346+
beforeSlotted.push(child)
347+
} else if (child.getAttribute('slot') === 'after-tabs') {
348+
afterTabSlotted.push(child)
349+
} else {
350+
autoSlotted.push(child)
351+
}
324352
}
325-
if (child.getAttribute('slot') === 'before-tabs') {
326-
beforeSlotted.push(child)
327-
} else if (child.getAttribute('slot') === 'after-tabs') {
328-
afterTabSlotted.push(child)
353+
if (manualSlotsSupported) {
354+
this.#beforeTabsSlot.assign(...beforeSlotted)
355+
this.#afterTabsSlot.assign(...afterTabSlotted)
356+
this.#afterPanelsSlot.assign(...afterSlotted)
329357
} else {
330-
autoSlotted.push(child)
358+
for (const el of beforeSlotted) el.setAttribute('slot', 'before-tabs')
359+
for (const el of afterTabSlotted) el.setAttribute('slot', 'after-tabs')
360+
for (const el of afterSlotted) el.setAttribute('slot', 'after-panels')
331361
}
332362
}
333-
if (manualSlotsSupported) {
334-
this.#beforeTabsSlot.assign(...beforeSlotted)
335-
this.#afterTabsSlot.assign(...afterTabSlotted)
336-
this.#afterPanelsSlot.assign(...afterSlotted)
337-
} else {
338-
for (const el of beforeSlotted) el.setAttribute('slot', 'before-tabs')
339-
for (const el of afterTabSlotted) el.setAttribute('slot', 'after-tabs')
340-
for (const el of afterSlotted) el.setAttribute('slot', 'after-panels')
341-
}
342363
const defaultTab = this.defaultTabIndex
343364
const defaultIndex = defaultTab >= 0 ? defaultTab : this.selectedTabIndex
344365
index = index >= 0 ? index : Math.max(0, defaultIndex)

test/test.js

+51
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,57 @@ describe('tab-container', function () {
669669
)
670670
})
671671
})
672+
673+
describe('with custom tablist-wrapper', function () {
674+
beforeEach(function () {
675+
document.body.innerHTML = `
676+
<tab-container>
677+
<div slot="tablist-wrapper">
678+
<div role="tablist">
679+
<button type="button" role="tab">Tab one</button>
680+
<button type="button" role="tab" aria-selected="true">Tab two</button>
681+
<button type="button" role="tab">Tab three</button>
682+
</div>
683+
</div>
684+
<div role="tabpanel" hidden>
685+
Panel 1
686+
</div>
687+
<div role="tabpanel">
688+
Panel 2
689+
</div>
690+
<div role="tabpanel" hidden data-tab-container-no-tabstop>
691+
Panel 3
692+
</div>
693+
</tab-container>
694+
`
695+
tabs = Array.from(document.querySelectorAll('button'))
696+
panels = Array.from(document.querySelectorAll('[role="tabpanel"]'))
697+
})
698+
699+
afterEach(function () {
700+
// Check to make sure we still have accessible markup after the test finishes running.
701+
expect(document.body).to.be.accessible()
702+
703+
document.body.innerHTML = ''
704+
})
705+
706+
it('has accessible markup', function () {
707+
expect(document.body).to.be.accessible()
708+
})
709+
710+
it('the second tab is still selected', function () {
711+
assert.deepStrictEqual(tabs.map(isSelected), [false, true, false], 'Second tab is selected')
712+
assert.deepStrictEqual(panels.map(isHidden), [true, false, true], 'Second panel is visible')
713+
})
714+
715+
it('selects the clicked tab', function () {
716+
tabs[0].click()
717+
718+
assert.deepStrictEqual(tabs.map(isSelected), [true, false, false], 'First tab is selected')
719+
assert.deepStrictEqual(panels.map(isHidden), [false, true, true], 'First panel is visible')
720+
})
721+
})
722+
672723
describe('with custom tablist-tab-wrapper', function () {
673724
beforeEach(function () {
674725
document.body.innerHTML = `

0 commit comments

Comments
 (0)