feat(tab) : onglets visibles adaptés à la largeur + survol = style actif
- Le nombre d'onglets affichés en mode fenêtré s'adapte automatiquement à la largeur réelle (ResizeObserver + ligne de mesure cachée). Les chevrons restent fixés aux bords ; le nombre est choisi pour que les onglets tiennent (pas de chevauchement ni de rognage). Calcul isolé en fonction pure testable (tabFit.ts, basée sur les vraies largeurs). maxVisibleTabs devient un plafond optionnel. - BREAKING : suppression de la prop maxWidth (la barre prend toute la largeur). - Survol d'un onglet inactif : même style que l'actif (texte m-primary + barre). - Playground : bac à sable interactif (nb onglets, plafond, icônes, labels longs, cadre redimensionnable) pour tester tous les cas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,65 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 items-start gap-6">
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-1 text-xl font-bold">Bac à sable (tous les cas)</h2>
|
||||
<p class="mb-4 text-sm text-m-muted">
|
||||
Règle les paramètres puis <strong>redimensionne le cadre en pointillés</strong>
|
||||
(poignée en bas à droite) pour voir le nombre d'onglets s'adapter et les flèches apparaître.
|
||||
</p>
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-4 text-sm">
|
||||
<label class="flex flex-col gap-1">Nb onglets : {{ sbCount }}
|
||||
<input
|
||||
v-model.number="sbCount"
|
||||
type="range"
|
||||
min="1"
|
||||
max="15"
|
||||
class="w-40"
|
||||
>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">maxVisibleTabs (0 = auto)
|
||||
<input
|
||||
v-model.number="sbMax"
|
||||
type="number"
|
||||
min="0"
|
||||
max="15"
|
||||
class="w-20 rounded border px-2 py-1"
|
||||
>
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="sbIcons"
|
||||
type="checkbox"
|
||||
> Icônes
|
||||
</label>
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="sbLong"
|
||||
type="checkbox"
|
||||
> Labels longs
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="resize-x overflow-hidden rounded border-2 border-dashed border-m-muted p-3"
|
||||
style="width: 100%; min-width: 280px;"
|
||||
>
|
||||
<MalioTabList
|
||||
v-model="sbValue"
|
||||
:tabs="sbTabs"
|
||||
:max-visible-tabs="sbMaxProp"
|
||||
>
|
||||
<template
|
||||
v-for="t in sbTabs"
|
||||
#[t.key]
|
||||
:key="t.key"
|
||||
>
|
||||
<p class="p-4">Contenu : {{ t.label }}</p>
|
||||
</template>
|
||||
</MalioTabList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-4">
|
||||
<h2 class="mb-4 text-xl font-bold">Simple</h2>
|
||||
<MalioTabList v-model="simpleValue" :tabs="tabs">
|
||||
@@ -70,7 +130,35 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// --- Bac à sable interactif ---
|
||||
const sbCount = ref(9)
|
||||
const sbMax = ref(0)
|
||||
const sbIcons = ref(true)
|
||||
const sbLong = ref(false)
|
||||
|
||||
const SB_LABELS = [
|
||||
'Informations', 'Adresses', 'Contacts', 'Comptabilité', 'Documents',
|
||||
'Historique', 'Paramètres', 'Qualité', 'Facturation', 'Accueil',
|
||||
'Notifications', 'Statistiques', 'Équipe', 'Sécurité', 'Étiquettes',
|
||||
]
|
||||
const SB_ICONS = [
|
||||
'mdi:information-outline', 'mdi:map-marker-outline', 'mdi:account-box-outline', 'mdi:web',
|
||||
'mdi:file-document-outline', 'mdi:history', 'mdi:cog-outline', 'mdi:check-decagram-outline',
|
||||
'mdi:receipt-text-outline', 'mdi:home', 'mdi:bell-outline', 'mdi:chart-bar',
|
||||
'mdi:account-group-outline', 'mdi:lock-outline', 'mdi:tag-outline',
|
||||
]
|
||||
|
||||
const sbTabs = computed(() =>
|
||||
Array.from({ length: sbCount.value }, (_, i) => ({
|
||||
key: `sb${i}`,
|
||||
label: sbLong.value ? `${SB_LABELS[i % SB_LABELS.length]} détaillé` : SB_LABELS[i % SB_LABELS.length],
|
||||
icon: sbIcons.value ? SB_ICONS[i % SB_ICONS.length] : undefined,
|
||||
})),
|
||||
)
|
||||
const sbMaxProp = computed(() => (sbMax.value > 0 ? sbMax.value : undefined))
|
||||
const sbValue = ref('sb0')
|
||||
|
||||
const tabs = [
|
||||
{ key: 'qualimat', label: 'Qualimat', icon: 'mdi:certificate-outline' },
|
||||
|
||||
@@ -58,6 +58,8 @@ Liste des évolutions de la librairie Malio layer UI
|
||||
* [#MUI-45] MalioDate : prop `markedDates` (`Record<"YYYY-MM-DD", 'success' | 'danger'>`) appliquant un fond tokenisé par jour dans la grille (générique, fourni par le consommateur ; précédence sélection/`today` > variante marquée > défaut) + event `month-change` (`{ month: 0-11, year }`) émis à l'ouverture du popover et à chaque navigation de mois. Sert l'écran *Heures* de SIRH (jours validés en vert, chargement du mois visible à la volée).
|
||||
|
||||
### Changed
|
||||
* TabList : le nombre d'onglets visibles en mode fenêtré s'**adapte automatiquement à la largeur réelle** (mesure via `ResizeObserver` + ligne de mesure cachée), au lieu d'un `maxVisibleTabs` fixe qui pouvait faire déborder les onglets sur les chevrons. Les chevrons restent fixés aux bords et le nombre affiché est choisi pour que les onglets tiennent (pas de chevauchement ni de rognage). `maxVisibleTabs` devient un **plafond optionnel**. Calcul isolé dans une fonction pure testable (`tabFit.ts`, basée sur les largeurs réelles des onglets). Sans layout (SSR), repli sur le plafond / tous les onglets. **Breaking** : la prop `maxWidth` est supprimée (la barre utilise désormais toute la largeur disponible au lieu d'être plafonnée à 1100px).
|
||||
* TabList : au **survol** d'un onglet inactif, on applique désormais le même style que l'onglet actif — texte `m-primary` plein + barre soulignée `m-primary` (`hover:after:*`) — au lieu du discret `text-m-primary/70`, pour bien marquer la cible.
|
||||
* Sidebar : états visuels des liens de navigation — **survol** : highlight pleine largeur entièrement porté par le `<li>` (fond `m-primary` à 10 % + texte `m-primary` + semi-bold, `hover:bg-m-primary/10 hover:text-m-primary hover:font-semibold`, espacement `pt-1 pb-1`). La couleur de base (`text-black`) est aussi sur le `<li>` et le `<a>` ne fige plus sa couleur (il hérite) : sinon, sur les bandes `pt-1`/`pb-1` situées hors du `<a>`, le fond devenait bleu mais le texte restait noir. **Lien actif** : texte `m-primary` + semi-bold, sans fond (`active-class="!text-m-primary font-semibold"` ; `!important` car `active-class` est hors `twMerge`).
|
||||
* DataTable : libellés de pagination en français — `Préc.` / `Suiv.` (étaient `Prev` / `Next`) ; aria-labels déjà en français inchangés.
|
||||
* MalioButton : dimensions par défaut `w-[180px]` / `h-[38px]` (étaient `w-[200px]` / `h-[40px]`).
|
||||
|
||||
+2
-3
@@ -786,10 +786,9 @@ Navigation par onglets avec contenu dynamique.
|
||||
|------|------|--------|-------------|
|
||||
| `modelValue` | `string` | `undefined` | Onglet actif (v-model) |
|
||||
| `tabs` | `Tab[]` | **requis** | Liste des onglets (voir type ci-dessous) |
|
||||
| `maxVisibleTabs` | `number` | `undefined` | Nombre max d'onglets affichés à la fois. Au-delà, un carrousel avec flèches gauche/droite apparaît (décalage 1 par 1). Non défini = tous les onglets. |
|
||||
| `maxWidth` | `number` | `1100` | Largeur max (px) du bloc d'onglets en mode fenêtré. |
|
||||
| `maxVisibleTabs` | `number` | `undefined` | **Plafond** optionnel du nombre d'onglets visibles. Non défini = uniquement limité par la largeur. |
|
||||
|
||||
Quand `maxVisibleTabs` est défini et que le nombre d'onglets le dépasse, la barre passe en mode fenêtré : seuls `maxVisibleTabs` onglets sont visibles à la fois, encadrés par des flèches gauche/droite qui font défiler la fenêtre un onglet à la fois (largeur du bloc bornée par `maxWidth`).
|
||||
Le nombre d'onglets affichés s'**adapte automatiquement à la largeur disponible** (mesurée au runtime via `ResizeObserver`). Quand tous les onglets ne tiennent pas, la barre passe en mode fenêtré : les flèches gauche/droite (fixées aux bords) font défiler la fenêtre un onglet à la fois, et le nombre visible est choisi pour que les onglets tiennent (jamais de chevauchement ni de rognage). `maxVisibleTabs`, s'il est fourni, plafonne ce nombre.
|
||||
|
||||
Type `Tab` :
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ type TabListProps = {
|
||||
modelValue?: string
|
||||
id?: string
|
||||
maxVisibleTabs?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
|
||||
const TabListForTest = TabList as DefineComponent<TabListProps>
|
||||
@@ -157,7 +156,16 @@ describe('MalioTabList', () => {
|
||||
const wrapper = mountComponent({tabs: disabledTabs})
|
||||
const buttons = wrapper.findAll('[role="tab"]')
|
||||
expect(buttons[1].classes()).toContain('cursor-not-allowed')
|
||||
expect(buttons[1].classes()).not.toContain('hover:text-m-primary/70')
|
||||
expect(buttons[1].classes()).not.toContain('hover:text-m-primary')
|
||||
})
|
||||
|
||||
it('hover sur un onglet inactif applique le même style que l\'actif (texte plein + barre)', () => {
|
||||
const wrapper = mountComponent({tabs})
|
||||
const inactive = wrapper.findAll('[role="tab"]')[1]
|
||||
expect(inactive.attributes('aria-selected')).toBe('false')
|
||||
expect(inactive.classes()).toContain('hover:text-m-primary')
|
||||
expect(inactive.classes()).toContain('hover:after:bg-m-primary')
|
||||
expect(inactive.classes()).toContain('hover:after:h-[3px]')
|
||||
})
|
||||
|
||||
it('does not emit update:modelValue when clicking a disabled tab', async () => {
|
||||
@@ -199,16 +207,6 @@ describe('MalioTabList — fenêtrage maxVisibleTabs', () => {
|
||||
{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"]')
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
<template>
|
||||
<div v-bind="$attrs">
|
||||
<div
|
||||
ref="rootRef"
|
||||
v-bind="$attrs"
|
||||
>
|
||||
<!-- Ligne de mesure cachée : largeur réelle de chaque onglet (mêmes classes de
|
||||
layout, placeholder à la place de l'icône pour ne pas fausser les tests),
|
||||
afin de calculer combien d'onglets tiennent. Invisible et hors flux. -->
|
||||
<div
|
||||
ref="measureRef"
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none invisible absolute left-0 top-0 flex"
|
||||
>
|
||||
<span
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="flex items-center gap-[18px] text-[24px] font-[600]"
|
||||
>
|
||||
<span
|
||||
v-if="tab.icon"
|
||||
class="inline-block shrink-0"
|
||||
:style="{ width: `${tab.iconSize ?? 24}px`, height: `${tab.iconSize ?? 24}px` }"
|
||||
/>
|
||||
{{ tab.label }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isWindowed" class="flex items-center justify-center gap-[36px] border-b border-m-primary">
|
||||
<button
|
||||
type="button"
|
||||
@@ -20,7 +45,6 @@
|
||||
<div
|
||||
role="tablist"
|
||||
class="flex flex-1 justify-center gap-[60px]"
|
||||
:style="{ maxWidth: `${maxWidth}px` }"
|
||||
>
|
||||
<button
|
||||
v-for="tab in visibleTabs"
|
||||
@@ -39,7 +63,7 @@
|
||||
? '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',
|
||||
: 'cursor-pointer text-m-primary/50 hover:text-m-primary hover:after:content-[\'\'] hover:after:absolute hover:after:-bottom-[3px] hover:after:left-0 hover:after:right-0 hover:after:h-[3px] hover:after:bg-m-primary',
|
||||
]"
|
||||
@click="selectTab(tab.key)"
|
||||
>
|
||||
@@ -91,7 +115,7 @@
|
||||
? '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',
|
||||
: 'cursor-pointer text-m-primary/50 hover:text-m-primary hover:after:content-[\'\'] hover:after:absolute hover:after:-bottom-[3px] hover:after:left-0 hover:after:right-0 hover:after:h-[3px] hover:after:bg-m-primary',
|
||||
]"
|
||||
@click="selectTab(tab.key)"
|
||||
>
|
||||
@@ -119,8 +143,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, useId, watch} from 'vue'
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch} from 'vue'
|
||||
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||
import {computeVisibleCount} from './tabFit'
|
||||
|
||||
defineOptions({name: 'MalioTabList', inheritAttrs: false})
|
||||
|
||||
@@ -137,12 +162,10 @@ const props = withDefaults(defineProps<{
|
||||
modelValue?: string
|
||||
id?: string
|
||||
maxVisibleTabs?: number
|
||||
maxWidth?: number
|
||||
}>(), {
|
||||
modelValue: undefined,
|
||||
id: '',
|
||||
maxVisibleTabs: undefined,
|
||||
maxWidth: 1100,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -159,22 +182,69 @@ const activeTab = computed(() =>
|
||||
isControlled.value ? props.modelValue! : localValue.value,
|
||||
)
|
||||
|
||||
const isWindowed = computed(() =>
|
||||
props.maxVisibleTabs != null && props.tabs.length > props.maxVisibleTabs,
|
||||
)
|
||||
const TAB_GAP = 60
|
||||
const CHEVRON_RESERVE = 110
|
||||
|
||||
const maxStartIndex = computed(() =>
|
||||
isWindowed.value ? Math.max(0, props.tabs.length - props.maxVisibleTabs!) : 0,
|
||||
)
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const measureRef = ref<HTMLElement | null>(null)
|
||||
const containerWidth = ref(0)
|
||||
const tabWidths = ref<number[]>([])
|
||||
|
||||
// Nombre d'onglets affichés, calculé pour qu'ils tiennent dans la largeur réelle
|
||||
// (cf. tabFit.ts) — la structure « flèches fixes » est conservée, et comme le
|
||||
// nombre est choisi pour tenir, pas de débordement sur les flèches ni de rognage.
|
||||
// `maxVisibleTabs` reste un plafond optionnel. Sans mesure (SSR/jsdom), repli sur
|
||||
// ce plafond / tous les onglets.
|
||||
const visibleCount = computed(() => computeVisibleCount({
|
||||
count: props.tabs.length,
|
||||
containerWidth: containerWidth.value,
|
||||
tabWidths: tabWidths.value,
|
||||
gap: TAB_GAP,
|
||||
chevronReserve: CHEVRON_RESERVE,
|
||||
maxVisibleTabs: props.maxVisibleTabs,
|
||||
}))
|
||||
|
||||
const isWindowed = computed(() => props.tabs.length > visibleCount.value)
|
||||
|
||||
const maxStartIndex = computed(() => Math.max(0, props.tabs.length - visibleCount.value))
|
||||
|
||||
const startIndex = ref(0)
|
||||
|
||||
const visibleTabs = computed(() =>
|
||||
isWindowed.value
|
||||
? props.tabs.slice(startIndex.value, startIndex.value + props.maxVisibleTabs!)
|
||||
? props.tabs.slice(startIndex.value, startIndex.value + visibleCount.value)
|
||||
: props.tabs,
|
||||
)
|
||||
|
||||
function measureTabWidths() {
|
||||
const el = measureRef.value
|
||||
if (!el) return
|
||||
tabWidths.value = Array.from(el.children).map(c => (c as HTMLElement).offsetWidth)
|
||||
}
|
||||
|
||||
function measureContainer() {
|
||||
if (rootRef.value) containerWidth.value = rootRef.value.clientWidth
|
||||
}
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
onMounted(() => {
|
||||
measureTabWidths()
|
||||
measureContainer()
|
||||
if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
|
||||
resizeObserver = new ResizeObserver(() => measureContainer())
|
||||
resizeObserver.observe(rootRef.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => resizeObserver?.disconnect())
|
||||
|
||||
// Re-mesure quand la liste change (labels/icônes → largeurs différentes).
|
||||
watch(() => props.tabs, () => nextTick(() => {
|
||||
measureTabWidths()
|
||||
measureContainer()
|
||||
}), {deep: true})
|
||||
|
||||
const focusedKey = computed(() => {
|
||||
if (!isWindowed.value) return activeTab.value
|
||||
const inView = visibleTabs.value.some(t => t.key === activeTab.value)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import {describe, it, expect} from 'vitest'
|
||||
import {computeVisibleCount} from './tabFit'
|
||||
|
||||
const base = {gap: 60, chevronReserve: 110}
|
||||
const widths = (n: number, w = 180) => Array.from({length: n}, () => w)
|
||||
|
||||
describe('computeVisibleCount', () => {
|
||||
it('sans layout : respecte maxVisibleTabs', () => {
|
||||
expect(computeVisibleCount({...base, count: 5, containerWidth: 0, tabWidths: [], maxVisibleTabs: 3})).toBe(3)
|
||||
})
|
||||
|
||||
it('sans layout ni maxVisibleTabs : tous les onglets', () => {
|
||||
expect(computeVisibleCount({...base, count: 5, containerWidth: 0, tabWidths: []})).toBe(5)
|
||||
})
|
||||
|
||||
it('tout tient : retourne le total (pas de chevrons)', () => {
|
||||
// 4×180 + 3×60 = 900 <= 1000
|
||||
expect(computeVisibleCount({...base, count: 4, containerWidth: 1000, tabWidths: widths(4)})).toBe(4)
|
||||
})
|
||||
|
||||
it('trop large : additionne les vraies largeurs (pas la pire) — pas d\'effondrement à 1', () => {
|
||||
// total 7×180+6×60=1620 > 1400 ; avail=1400-110=1290 ; 180,420,660,900,1140,(1380>1290) → 5
|
||||
expect(computeVisibleCount({...base, count: 7, containerWidth: 1400, tabWidths: widths(7)})).toBe(5)
|
||||
})
|
||||
|
||||
it('largeur étroite : montre ce qui tient (≥ 2 ici, pas 1)', () => {
|
||||
// avail=570-110=460 ; 180,(420),(660>460) → 2
|
||||
expect(computeVisibleCount({...base, count: 7, containerWidth: 570, tabWidths: widths(7)})).toBe(2)
|
||||
})
|
||||
|
||||
it('maxVisibleTabs plafonne le résultat', () => {
|
||||
expect(computeVisibleCount({...base, count: 7, containerWidth: 1400, tabWidths: widths(7), maxVisibleTabs: 3})).toBe(3)
|
||||
})
|
||||
|
||||
it('au moins 1 onglet si rien ne tient', () => {
|
||||
expect(computeVisibleCount({...base, count: 5, containerWidth: 150, tabWidths: widths(5, 300)})).toBe(1)
|
||||
})
|
||||
|
||||
it('gère des largeurs hétérogènes', () => {
|
||||
// total 1080 > 1000 → fenêtré ; avail=1000-110=890 ; 300,560,820,(1080>890) → 3
|
||||
expect(computeVisibleCount({...base, count: 4, containerWidth: 1000, tabWidths: [300, 200, 200, 200]})).toBe(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
// Calcule combien d'onglets afficher pour qu'ils tiennent dans la largeur dispo,
|
||||
// en gardant la structure « flèches fixes aux bords » : le nombre est choisi pour
|
||||
// que les onglets visibles tiennent → pas de débordement sur les flèches, pas de
|
||||
// rognage, barre d'onglet actif intacte.
|
||||
//
|
||||
// On additionne les VRAIES largeurs d'onglets (pas la pire), donc le résultat
|
||||
// n'est pas sur-conservateur (évite de tomber à 1 onglet inutilement).
|
||||
//
|
||||
// Fonction pure → testable sans DOM. Sans layout (SSR / jsdom : largeurs à 0),
|
||||
// on retombe sur le plafond `maxVisibleTabs` (ou tous les onglets).
|
||||
export interface TabFitInput {
|
||||
count: number // nombre total d'onglets
|
||||
containerWidth: number // largeur dispo mesurée (0 si inconnue)
|
||||
tabWidths: number[] // largeur mesurée de chaque onglet (vide si inconnu)
|
||||
gap: number // espace entre onglets (px)
|
||||
chevronReserve: number // place des chevrons + marges quand fenêtré (px)
|
||||
maxVisibleTabs?: number // plafond optionnel imposé par le consommateur
|
||||
}
|
||||
|
||||
export function computeVisibleCount(input: TabFitInput): number {
|
||||
const {count, containerWidth, tabWidths, gap, chevronReserve, maxVisibleTabs} = input
|
||||
|
||||
// Pas d'info de layout : on respecte le plafond explicite, sinon tout afficher.
|
||||
if (containerWidth <= 0 || tabWidths.length === 0) {
|
||||
return maxVisibleTabs != null ? Math.min(maxVisibleTabs, count) : count
|
||||
}
|
||||
|
||||
const fullAvail = containerWidth
|
||||
const total = tabWidths.reduce((s, w) => s + w, 0) + gap * Math.max(0, count - 1)
|
||||
|
||||
let fit: number
|
||||
if (total <= fullAvail) {
|
||||
fit = count // tout tient, pas de chevrons
|
||||
}
|
||||
else {
|
||||
const avail = fullAvail - chevronReserve
|
||||
let used = 0
|
||||
let n = 0
|
||||
for (const w of tabWidths) {
|
||||
const add = w + (n > 0 ? gap : 0)
|
||||
if (used + add > avail) break
|
||||
used += add
|
||||
n++
|
||||
}
|
||||
fit = Math.max(1, n)
|
||||
}
|
||||
|
||||
return maxVisibleTabs != null ? Math.min(maxVisibleTabs, fit) : fit
|
||||
}
|
||||
Reference in New Issue
Block a user