fix(accordion): overflow visible une fois ouvert pour ne pas rogner les popovers enfants [#MUI-37]
Le panneau garde overflow-hidden pendant l'animation puis passe en overflow-visible à la fin de l'ouverture (et re-clippe à la fermeture), afin qu'un datepicker/select ouvert dans une section ne soit pas coupé. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -222,3 +222,35 @@ describe('MalioAccordion — defaultOpen, disabled & clavier', () => {
|
|||||||
root.remove()
|
root.remove()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('MalioAccordion — overflow du panneau (popovers enfants)', () => {
|
||||||
|
const ONE = `<MalioAccordionItem title="A" value="a"><p>contenu</p></MalioAccordionItem>`
|
||||||
|
const ONE_OPEN = `<MalioAccordionItem title="A" value="a" :default-open="true"><p>contenu</p></MalioAccordionItem>`
|
||||||
|
|
||||||
|
it('clips the panel (overflow-hidden) while collapsed', () => {
|
||||||
|
const wrapper = mountAccordion({}, ONE)
|
||||||
|
const inner = wrapper.find('[role="region"] > div')
|
||||||
|
expect(inner.classes()).toContain('overflow-hidden')
|
||||||
|
expect(inner.classes()).not.toContain('overflow-visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('lets the panel overflow once open at mount (defaultOpen)', async () => {
|
||||||
|
const wrapper = mountAccordion({}, ONE_OPEN)
|
||||||
|
await nextTick()
|
||||||
|
expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to overflow-visible after the open transition ends', async () => {
|
||||||
|
const wrapper = mountAccordion({}, ONE)
|
||||||
|
await wrapper.find('button[aria-expanded]').trigger('click')
|
||||||
|
await wrapper.find('[role="region"]').trigger('transitionend', {propertyName: 'grid-template-rows'})
|
||||||
|
expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-clips (overflow-hidden) as soon as it closes', async () => {
|
||||||
|
const wrapper = mountAccordion({}, ONE_OPEN)
|
||||||
|
await nextTick()
|
||||||
|
await wrapper.find('button[aria-expanded]').trigger('click')
|
||||||
|
expect(wrapper.find('[role="region"] > div').classes()).toContain('overflow-hidden')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -29,8 +29,12 @@
|
|||||||
:aria-labelledby="headerId"
|
:aria-labelledby="headerId"
|
||||||
class="grid transition-[grid-template-rows] duration-200 ease-out"
|
class="grid transition-[grid-template-rows] duration-200 ease-out"
|
||||||
:class="open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
:class="open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
||||||
|
@transitionend="onPanelTransitionEnd"
|
||||||
>
|
>
|
||||||
<div class="overflow-hidden" :inert="!open || undefined">
|
<div
|
||||||
|
:class="overflowVisible ? 'overflow-visible' : 'overflow-hidden'"
|
||||||
|
:inert="!open || undefined"
|
||||||
|
>
|
||||||
<div :class="panelInnerClass">
|
<div :class="panelInnerClass">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +44,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, inject, onBeforeUnmount, onMounted, ref, useId} from 'vue'
|
import {computed, inject, onBeforeUnmount, onMounted, ref, useId, watch} from 'vue'
|
||||||
import {Icon as IconifyIcon} from '@iconify/vue'
|
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||||
import {twMerge} from 'tailwind-merge'
|
import {twMerge} from 'tailwind-merge'
|
||||||
import {accordionContextKey} from './context'
|
import {accordionContextKey} from './context'
|
||||||
@@ -74,6 +78,22 @@ const headerId = computed(() => `${ctx.baseId.value}-header-${value.value}`)
|
|||||||
const panelId = computed(() => `${ctx.baseId.value}-panel-${value.value}`)
|
const panelId = computed(() => `${ctx.baseId.value}-panel-${value.value}`)
|
||||||
const open = computed(() => ctx.isOpen(value.value))
|
const open = computed(() => ctx.isOpen(value.value))
|
||||||
|
|
||||||
|
// Le panneau garde `overflow-hidden` pendant l'animation (clipping requis par
|
||||||
|
// la transition grid-template-rows), puis passe en `overflow-visible` une fois
|
||||||
|
// complètement ouvert pour qu'un popover enfant (datepicker, select…) ne soit
|
||||||
|
// pas rogné. On re-clippe dès le début de la fermeture.
|
||||||
|
const overflowVisible = ref(false)
|
||||||
|
|
||||||
|
watch(open, (isOpen) => {
|
||||||
|
if (!isOpen) overflowVisible.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
function onPanelTransitionEnd(e: TransitionEvent) {
|
||||||
|
if (e.propertyName === 'grid-template-rows' && open.value) {
|
||||||
|
overflowVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onToggle() {
|
function onToggle() {
|
||||||
if (props.disabled) return
|
if (props.disabled) return
|
||||||
ctx.toggle(value.value)
|
ctx.toggle(value.value)
|
||||||
@@ -81,13 +101,13 @@ function onToggle() {
|
|||||||
|
|
||||||
const headerClasses = computed(() =>
|
const headerClasses = computed(() =>
|
||||||
twMerge(
|
twMerge(
|
||||||
'flex w-full items-center justify-between gap-4 px-7 pt-[28px] pb-[34px] text-left font-[600] text-[20px] transition-colors',
|
'flex w-full items-center justify-between gap-4 px-7 pt-[28px] pb-[20px] text-left font-[600] text-[20px] transition-colors',
|
||||||
props.disabled ? 'cursor-not-allowed text-m-muted' : 'cursor-pointer hover:bg-m-surface',
|
props.disabled ? 'cursor-not-allowed text-m-muted' : 'cursor-pointer hover:bg-m-surface',
|
||||||
props.headerClass,
|
props.headerClass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const panelInnerClass = computed(() => twMerge('px-7 pb-[10px]', props.panelClass))
|
const panelInnerClass = computed(() => twMerge('px-7 pt-[10px] pb-[20px]', props.panelClass))
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ctx.register(
|
ctx.register(
|
||||||
@@ -98,6 +118,8 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
props.defaultOpen,
|
props.defaultOpen,
|
||||||
)
|
)
|
||||||
|
// Ouvert au montage (defaultOpen / contrôlé) : pas d'animation, overflow visible direct.
|
||||||
|
if (open.value) overflowVisible.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => ctx.unregister(value.value))
|
onBeforeUnmount(() => ctx.unregister(value.value))
|
||||||
|
|||||||
Reference in New Issue
Block a user