9ff3e83c03
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>
218 lines
6.4 KiB
Vue
218 lines
6.4 KiB
Vue
<template>
|
|
<div v-bind="$attrs">
|
|
<div v-if="isWindowed" class="flex items-center justify-center gap-[36px] border-b border-m-primary">
|
|
<button
|
|
type="button"
|
|
aria-label="Onglets précédents"
|
|
data-test="tab-prev"
|
|
:disabled="!canPrev"
|
|
:class="[
|
|
'transition-colors',
|
|
canPrev
|
|
? 'cursor-pointer text-m-btn-primary hover:text-m-btn-primary-hover active:text-m-btn-primary-active'
|
|
: 'cursor-not-allowed text-m-disabled',
|
|
]"
|
|
@click="prev"
|
|
>
|
|
<IconifyIcon icon="mdi:chevron-left" :width="28" />
|
|
</button>
|
|
|
|
<div
|
|
role="tablist"
|
|
class="flex flex-1 justify-center gap-[60px]"
|
|
:style="{ maxWidth: `${maxWidth}px` }"
|
|
>
|
|
<button
|
|
v-for="tab in visibleTabs"
|
|
:id="`${componentId}-tab-${tab.key}`"
|
|
:key="tab.key"
|
|
role="tab"
|
|
type="button"
|
|
:aria-selected="activeTab === tab.key"
|
|
:aria-controls="`${componentId}-panel-${tab.key}`"
|
|
:aria-disabled="!!tab.disabled"
|
|
:tabindex="focusedKey === tab.key ? 0 : -1"
|
|
:disabled="tab.disabled"
|
|
:class="[
|
|
'relative flex items-center gap-[18px] text-[24px] font-[600] transition-colors',
|
|
activeTab === tab.key
|
|
? 'cursor-pointer text-m-primary after:content-[\'\'] after:absolute after:-bottom-[3px] after:left-0 after:right-0 after:h-[3px] after:bg-m-primary'
|
|
: tab.disabled
|
|
? 'cursor-not-allowed text-m-primary/50'
|
|
: 'cursor-pointer text-m-primary/50 hover:text-m-primary/70',
|
|
]"
|
|
@click="selectTab(tab.key)"
|
|
>
|
|
<IconifyIcon
|
|
v-if="tab.icon"
|
|
:icon="tab.icon"
|
|
:width="tab.iconSize ?? 24"
|
|
/>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
aria-label="Onglets suivants"
|
|
data-test="tab-next"
|
|
:disabled="!canNext"
|
|
:class="[
|
|
'transition-colors',
|
|
canNext
|
|
? 'cursor-pointer text-m-btn-primary hover:text-m-btn-primary-hover active:text-m-btn-primary-active'
|
|
: 'cursor-not-allowed text-m-disabled',
|
|
]"
|
|
@click="next"
|
|
>
|
|
<IconifyIcon icon="mdi:chevron-right" :width="28" />
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
role="tablist"
|
|
class="flex justify-center gap-[60px] border-b border-m-primary"
|
|
>
|
|
<button
|
|
v-for="tab in visibleTabs"
|
|
:id="`${componentId}-tab-${tab.key}`"
|
|
:key="tab.key"
|
|
role="tab"
|
|
type="button"
|
|
:aria-selected="activeTab === tab.key"
|
|
:aria-controls="`${componentId}-panel-${tab.key}`"
|
|
:aria-disabled="!!tab.disabled"
|
|
:tabindex="focusedKey === tab.key ? 0 : -1"
|
|
:disabled="tab.disabled"
|
|
:class="[
|
|
'relative flex items-center gap-[18px] text-[24px] font-[600] transition-colors',
|
|
activeTab === tab.key
|
|
? 'cursor-pointer text-m-primary after:content-[\'\'] after:absolute after:-bottom-[3px] after:left-0 after:right-0 after:h-[3px] after:bg-m-primary'
|
|
: tab.disabled
|
|
? 'cursor-not-allowed text-m-primary/50'
|
|
: 'cursor-pointer text-m-primary/50 hover:text-m-primary/70',
|
|
]"
|
|
@click="selectTab(tab.key)"
|
|
>
|
|
<IconifyIcon
|
|
v-if="tab.icon"
|
|
:icon="tab.icon"
|
|
:width="tab.iconSize ?? 24"
|
|
/>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
v-for="tab in tabs"
|
|
v-show="activeTab === tab.key"
|
|
:id="`${componentId}-panel-${tab.key}`"
|
|
:key="tab.key"
|
|
role="tabpanel"
|
|
:aria-labelledby="isTabRendered(tab.key) ? `${componentId}-tab-${tab.key}` : undefined"
|
|
:aria-label="isTabRendered(tab.key) ? undefined : tab.label"
|
|
>
|
|
<slot :name="tab.key" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {computed, ref, useId, watch} from 'vue'
|
|
import {Icon as IconifyIcon} from '@iconify/vue'
|
|
|
|
defineOptions({name: 'MalioTabList', inheritAttrs: false})
|
|
|
|
type Tab = {
|
|
key: string
|
|
label: string
|
|
icon?: string
|
|
iconSize?: string
|
|
disabled?: boolean
|
|
}
|
|
|
|
const props = withDefaults(defineProps<{
|
|
tabs: Tab[]
|
|
modelValue?: string
|
|
id?: string
|
|
maxVisibleTabs?: number
|
|
maxWidth?: number
|
|
}>(), {
|
|
modelValue: undefined,
|
|
id: '',
|
|
maxVisibleTabs: undefined,
|
|
maxWidth: 1100,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'update:modelValue', value: string): void
|
|
}>()
|
|
|
|
const generatedId = useId()
|
|
const componentId = computed(() => props.id || `malio-tab-list-${generatedId}`)
|
|
|
|
const isControlled = computed(() => props.modelValue !== undefined)
|
|
const localValue = ref(props.tabs.length > 0 ? props.tabs[0].key : '')
|
|
|
|
const activeTab = computed(() =>
|
|
isControlled.value ? props.modelValue! : localValue.value,
|
|
)
|
|
|
|
const isWindowed = computed(() =>
|
|
props.maxVisibleTabs != null && props.tabs.length > props.maxVisibleTabs,
|
|
)
|
|
|
|
const maxStartIndex = computed(() =>
|
|
isWindowed.value ? Math.max(0, props.tabs.length - props.maxVisibleTabs!) : 0,
|
|
)
|
|
|
|
const startIndex = ref(0)
|
|
|
|
const visibleTabs = computed(() =>
|
|
isWindowed.value
|
|
? props.tabs.slice(startIndex.value, startIndex.value + props.maxVisibleTabs!)
|
|
: props.tabs,
|
|
)
|
|
|
|
const focusedKey = computed(() => {
|
|
if (!isWindowed.value) return activeTab.value
|
|
const inView = visibleTabs.value.some(t => t.key === activeTab.value)
|
|
return inView ? activeTab.value : (visibleTabs.value[0]?.key ?? '')
|
|
})
|
|
|
|
const isTabRendered = (key: string) => !isWindowed.value || visibleTabs.value.some(t => t.key === key)
|
|
|
|
const canPrev = computed(() => isWindowed.value && startIndex.value > 0)
|
|
const canNext = computed(() => isWindowed.value && startIndex.value < maxStartIndex.value)
|
|
|
|
function prev() {
|
|
if (!canPrev.value) return
|
|
startIndex.value -= 1
|
|
}
|
|
|
|
function next() {
|
|
if (!canNext.value) return
|
|
startIndex.value += 1
|
|
}
|
|
|
|
// Clamp startIndex back in range if tabs or maxVisibleTabs change.
|
|
watch(maxStartIndex, (max) => {
|
|
if (startIndex.value > max) startIndex.value = max
|
|
})
|
|
|
|
// Reset the window to the start when the tab list is replaced.
|
|
watch(() => props.tabs, () => {
|
|
startIndex.value = 0
|
|
})
|
|
|
|
function selectTab(key: string) {
|
|
const tab = props.tabs.find(t => t.key === key)
|
|
if (tab?.disabled) return
|
|
if (!isControlled.value) {
|
|
localValue.value = key
|
|
}
|
|
emit('update:modelValue', key)
|
|
}
|
|
</script>
|