diff --git a/app/components/malio/tab/TabList.test.ts b/app/components/malio/tab/TabList.test.ts index 48874ac..7e91ad2 100644 --- a/app/components/malio/tab/TabList.test.ts +++ b/app/components/malio/tab/TabList.test.ts @@ -15,6 +15,7 @@ type TabListProps = { tabs: Tab[] modelValue?: string id?: string + maxVisibleTabs?: number } const TabListForTest = TabList as DefineComponent @@ -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: '

Panel 1

'}, + ) + + 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') + }) +}) diff --git a/app/components/malio/tab/TabList.vue b/app/components/malio/tab/TabList.vue index efa4f3d..969e26a 100644 --- a/app/components/malio/tab/TabList.vue +++ b/app/components/malio/tab/TabList.vue @@ -1,36 +1,68 @@