acd531f69e
Release / release (push) Successful in 2m38s
| 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: #56 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
110 lines
2.9 KiB
Vue
110 lines
2.9 KiB
Vue
<template>
|
|
<div v-bind="$attrs" :class="rootClass">
|
|
<slot />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {computed, provide, ref, useId} from 'vue'
|
|
import {twMerge} from 'tailwind-merge'
|
|
import {accordionContextKey, type AccordionItemRegistration} from './context'
|
|
|
|
defineOptions({name: 'MalioAccordion', inheritAttrs: false})
|
|
|
|
const props = withDefaults(defineProps<{
|
|
mode?: 'single' | 'multiple'
|
|
modelValue?: string | string[]
|
|
id?: string
|
|
groupClass?: string
|
|
}>(), {
|
|
mode: 'multiple',
|
|
modelValue: undefined,
|
|
id: '',
|
|
groupClass: '',
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: string | string[]): void
|
|
}>()
|
|
|
|
const generatedId = useId()
|
|
const baseId = computed(() => props.id || `malio-accordion-${generatedId}`)
|
|
const mode = computed(() => props.mode)
|
|
|
|
const isControlled = computed(() => props.modelValue !== undefined)
|
|
const localOpen = ref<string[]>([])
|
|
|
|
const items = ref<AccordionItemRegistration[]>([])
|
|
|
|
const openKeys = computed<string[]>(() => {
|
|
if (isControlled.value) {
|
|
const v = props.modelValue
|
|
if (props.mode === 'single') return v ? [v as string] : []
|
|
if (Array.isArray(v)) return v
|
|
return v ? [v as string] : []
|
|
}
|
|
return localOpen.value
|
|
})
|
|
|
|
function isOpen(value: string) {
|
|
return openKeys.value.includes(value)
|
|
}
|
|
|
|
function toggle(value: string) {
|
|
const current = openKeys.value
|
|
let next: string[]
|
|
if (props.mode === 'single') {
|
|
next = current.includes(value) ? [] : [value]
|
|
} else {
|
|
next = current.includes(value)
|
|
? current.filter(v => v !== value)
|
|
: [...current, value]
|
|
}
|
|
if (!isControlled.value) {
|
|
localOpen.value = next
|
|
}
|
|
emit('update:modelValue', props.mode === 'single' ? (next[0] ?? '') : next)
|
|
}
|
|
|
|
function register(item: AccordionItemRegistration, defaultOpen: boolean) {
|
|
items.value.push(item)
|
|
if (defaultOpen && !isControlled.value) {
|
|
if (props.mode === 'single') {
|
|
if (localOpen.value.length === 0) localOpen.value = [item.value]
|
|
} else if (!localOpen.value.includes(item.value)) {
|
|
localOpen.value.push(item.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
function unregister(value: string) {
|
|
items.value = items.value.filter(i => i.value !== value)
|
|
}
|
|
|
|
// `items` est ordonné par ordre de montage (= ordre du DOM pour des sections
|
|
// statiques/ajoutées en fin). Si un consommateur réordonne dynamiquement les
|
|
// items, cet ordre peut diverger de l'ordre visuel ; trier par position DOM
|
|
// serait alors nécessaire (hors périmètre v1).
|
|
function focusSibling(value: string, offset: 1 | -1) {
|
|
const enabled = items.value.filter(i => !i.isDisabled())
|
|
const idx = enabled.findIndex(i => i.value === value)
|
|
if (idx === -1) return
|
|
const next = enabled[(idx + offset + enabled.length) % enabled.length]
|
|
next?.getHeaderEl()?.focus()
|
|
}
|
|
|
|
const rootClass = computed(() =>
|
|
twMerge('divide-y divide-black border-y border-black', props.groupClass),
|
|
)
|
|
|
|
provide(accordionContextKey, {
|
|
mode,
|
|
baseId,
|
|
isOpen,
|
|
toggle,
|
|
register,
|
|
unregister,
|
|
focusSibling,
|
|
})
|
|
</script>
|