feat(ui) : TabList — fenêtrage maxVisibleTabs + flèches navigation
Ajout d'une prop maxVisibleTabs activant un fenêtrage carousel (sans animation) avec flèches gauche/droite. Les flèches sont désactivées aux bornes, les panneaux hors fenêtre restent montés, et startIndex est clampé si tabs/maxVisibleTabs changent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ type TabListProps = {
|
||||
tabs: Tab[]
|
||||
modelValue?: string
|
||||
id?: string
|
||||
maxVisibleTabs?: number
|
||||
}
|
||||
|
||||
const TabListForTest = TabList as DefineComponent<TabListProps>
|
||||
@@ -185,3 +186,127 @@ describe('MalioTabList', () => {
|
||||
expect(buttons[1].attributes('aria-selected')).toBe('false')
|
||||
})
|
||||
})
|
||||
|
||||
describe('MalioTabList — fenêtrage maxVisibleTabs', () => {
|
||||
const sevenTabs: Tab[] = [
|
||||
{key: 't1', label: 'Tab 1'},
|
||||
{key: 't2', label: 'Tab 2'},
|
||||
{key: 't3', label: 'Tab 3'},
|
||||
{key: 't4', label: 'Tab 4'},
|
||||
{key: 't5', label: 'Tab 5'},
|
||||
{key: 't6', label: 'Tab 6'},
|
||||
{key: 't7', label: 'Tab 7'},
|
||||
]
|
||||
|
||||
it('renders only maxVisibleTabs buttons and disables prev at start', () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
const buttons = wrapper.findAll('[role="tab"]')
|
||||
expect(buttons).toHaveLength(5)
|
||||
expect(buttons[0].text()).toContain('Tab 1')
|
||||
expect(buttons[4].text()).toContain('Tab 5')
|
||||
|
||||
const prev = wrapper.find('[data-test="tab-prev"]')
|
||||
const next = wrapper.find('[data-test="tab-next"]')
|
||||
expect(prev.exists()).toBe(true)
|
||||
expect(next.exists()).toBe(true)
|
||||
expect(prev.attributes('disabled')).toBeDefined()
|
||||
expect(next.attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('shifts the window by 1 on next click', async () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
|
||||
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
||||
|
||||
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
|
||||
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
|
||||
expect(labels.some(l => l.includes('Tab 6'))).toBe(true)
|
||||
expect(labels).toHaveLength(5)
|
||||
|
||||
expect(wrapper.find('[data-test="tab-prev"]').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disables next at the end and shows the last window', async () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
|
||||
// 7 - 5 = 2 clicks to reach the end
|
||||
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
||||
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
||||
|
||||
const next = wrapper.find('[data-test="tab-next"]')
|
||||
expect(next.attributes('disabled')).toBeDefined()
|
||||
|
||||
const buttons = wrapper.findAll('[role="tab"]')
|
||||
expect(buttons).toHaveLength(5)
|
||||
// last window starts at tabs[length-5] = tabs[2] = Tab 3
|
||||
expect(buttons[0].text()).toContain('Tab 3')
|
||||
expect(buttons[4].text()).toContain('Tab 7')
|
||||
})
|
||||
|
||||
it('clicking next past the end does not overshoot', async () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
const next = wrapper.find('[data-test="tab-next"]')
|
||||
|
||||
await next.trigger('click')
|
||||
await next.trigger('click')
|
||||
await next.trigger('click') // guarded, no-op
|
||||
await next.trigger('click') // guarded, no-op
|
||||
|
||||
const buttons = wrapper.findAll('[role="tab"]')
|
||||
expect(buttons).toHaveLength(5)
|
||||
expect(buttons[0].text()).toContain('Tab 3')
|
||||
expect(buttons[4].text()).toContain('Tab 7')
|
||||
})
|
||||
|
||||
it('renders no arrows and all tabs when maxVisibleTabs is undefined', () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs})
|
||||
expect(wrapper.findAll('[role="tab"]')).toHaveLength(7)
|
||||
expect(wrapper.find('[data-test="tab-prev"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-test="tab-next"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no arrows when maxVisibleTabs >= tabs.length', () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 7})
|
||||
expect(wrapper.findAll('[role="tab"]')).toHaveLength(7)
|
||||
expect(wrapper.find('[data-test="tab-prev"]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-test="tab-next"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('selecting a visible tab activates it without moving the window', async () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
const buttons = wrapper.findAll('[role="tab"]')
|
||||
|
||||
await buttons[2].trigger('click')
|
||||
|
||||
const after = wrapper.findAll('[role="tab"]')
|
||||
expect(after[2].attributes('aria-selected')).toBe('true')
|
||||
// window unchanged
|
||||
expect(after[0].text()).toContain('Tab 1')
|
||||
expect(after).toHaveLength(5)
|
||||
})
|
||||
|
||||
it('keeps the active panel rendered even when its tab is outside the window', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{tabs: sevenTabs, maxVisibleTabs: 5, modelValue: 't1'},
|
||||
{t1: '<p>Panel 1</p>'},
|
||||
)
|
||||
|
||||
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
||||
await wrapper.find('[data-test="tab-next"]').trigger('click')
|
||||
|
||||
// Tab 1 is no longer in the window
|
||||
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
|
||||
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
|
||||
|
||||
// but its panel is still rendered and visible
|
||||
const panels = wrapper.findAll('[role="tabpanel"]')
|
||||
expect(panels).toHaveLength(7)
|
||||
expect(wrapper.text()).toContain('Panel 1')
|
||||
})
|
||||
|
||||
it('arrows expose aria-labels', () => {
|
||||
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
|
||||
expect(wrapper.find('[data-test="tab-prev"]').attributes('aria-label')).toBe('Onglets précédents')
|
||||
expect(wrapper.find('[data-test="tab-next"]').attributes('aria-label')).toBe('Onglets suivants')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user