fix: readonly component style + TabList + required component (#61)
Release / release (push) Successful in 1m24s

| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [ ] Pas de régression
- [ ] TU/TI/TF rédigée
- [ ] TU/TI/TF OK
- [ ] CHANGELOG modifié

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #61
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #61.
This commit is contained in:
2026-06-04 06:45:24 +00:00
committed by Autin
parent b55050b2ad
commit 9ff3e83c03
58 changed files with 3192 additions and 167 deletions
+153
View File
@@ -15,6 +15,8 @@ type TabListProps = {
tabs: Tab[]
modelValue?: string
id?: string
maxVisibleTabs?: number
maxWidth?: number
}
const TabListForTest = TabList as DefineComponent<TabListProps>
@@ -185,3 +187,154 @@ 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('applies the default maxWidth (1100px) on the tabs container when windowed', () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1100px')
})
it('applies a custom maxWidth on the tabs container', () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5, maxWidth: 1200})
expect(wrapper.find('[role="tablist"]').attributes('style')).toContain('max-width: 1200px')
})
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('keeps exactly one rendered tab with tabindex=0 when the active tab scrolls out of the window', async () => {
const wrapper = mountComponent({tabs: sevenTabs, maxVisibleTabs: 5})
// active tab is the first one (t1) by default; scroll it out of the window
await wrapper.find('[data-test="tab-next"]').trigger('click')
await wrapper.find('[data-test="tab-next"]').trigger('click')
// t1 is no longer rendered
const labels = wrapper.findAll('[role="tab"]').map(b => b.text())
expect(labels.some(l => l.includes('Tab 1'))).toBe(false)
const focusable = wrapper.findAll('[role="tab"]').filter(b => b.attributes('tabindex') === '0')
expect(focusable).toHaveLength(1)
// falls back to the first visible tab (Tab 3)
expect(focusable[0].text()).toContain('Tab 3')
})
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')
})
})