Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a95cf8cdfb | |||
| ba2ecb5768 | |||
| 87940481d6 | |||
| 66fbbf8abe | |||
| 8de950c402 |
@@ -30,3 +30,5 @@ Liste des évolutions de la librairie Malio layer UI
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
* Hauteur des boutons de pagination du datatable alignée sur le select (40px)
|
||||
* Distribution de `tailwind.config.ts` aux projets consommateurs avec paths `content` absolus
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
--m-site-yellow: 243 203 0; /* #F3CB00 - Jaune Saint-Jean */
|
||||
--m-site-green: 116 191 4; /* #74BF04 - Vert Pommevic */
|
||||
|
||||
--m-radius: 8px;
|
||||
--m-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ const describedBy = computed(() => {
|
||||
|
||||
const mergedGroupClass = computed(() =>
|
||||
twMerge(
|
||||
'checkbox-wrapper-4 mt-4 w-full',
|
||||
'checkbox-wrapper-4 w-full',
|
||||
props.groupClass,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
variant="tertiary"
|
||||
label="Prev"
|
||||
:disabled="page <= 1"
|
||||
button-class="h-8 w-auto min-w-0 px-3 text-sm"
|
||||
button-class="h-10 w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page précédente"
|
||||
data-test="prev-button"
|
||||
@click="goToPage(page - 1)"
|
||||
@@ -95,7 +95,7 @@
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="h-8 min-w-[2rem] rounded px-2 text-sm transition-colors"
|
||||
class="h-10 min-w-[2.5rem] rounded px-2 text-sm transition-colors"
|
||||
:class="p === page
|
||||
? 'bg-m-btn-primary text-white font-semibold'
|
||||
: 'text-m-text hover:bg-m-bg'"
|
||||
@@ -111,7 +111,7 @@
|
||||
variant="tertiary"
|
||||
label="Next"
|
||||
:disabled="page >= totalPages"
|
||||
button-class="h-8 w-auto min-w-0 px-3 text-sm"
|
||||
button-class="h-10 w-auto min-w-0 px-3 text-sm"
|
||||
aria-label="Page suivante"
|
||||
data-test="next-button"
|
||||
@click="goToPage(page + 1)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative mt-4 w-full"
|
||||
class="relative w-full"
|
||||
>
|
||||
<textarea
|
||||
:id="inputId"
|
||||
|
||||
@@ -26,6 +26,7 @@ type SelectCheckboxProps = {
|
||||
displaySelectAll?: boolean
|
||||
selectAllLabel?: string
|
||||
disabled?: boolean
|
||||
groupClass?: string
|
||||
}
|
||||
|
||||
const SelectCheckboxForTest = SelectCheckbox as DefineComponent<SelectCheckboxProps>
|
||||
@@ -175,4 +176,21 @@ describe('MalioSelectCheckbox', () => {
|
||||
const checkboxes = wrapper.findAll('input[type="checkbox"]')
|
||||
expect((checkboxes[0].element as HTMLInputElement).checked).toBe(false)
|
||||
})
|
||||
|
||||
it('applies minWidth via twMerge so it overrides w-full (parity with MalioSelect)', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options: [], minWidth: 'w-80'},
|
||||
})
|
||||
const root = wrapper.find('button').element.parentElement
|
||||
expect(root?.className).toContain('w-80')
|
||||
expect(root?.className).not.toContain('w-full')
|
||||
})
|
||||
|
||||
it('applies groupClass via twMerge', () => {
|
||||
const wrapper = mount(SelectCheckboxForTest, {
|
||||
props: {modelValue: [], options: [], groupClass: 'mt-4'},
|
||||
})
|
||||
const root = wrapper.find('button').element.parentElement
|
||||
expect(root?.className).toContain('mt-4')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<div>
|
||||
<div
|
||||
ref="root"
|
||||
class="relative w-full"
|
||||
:class="[minWidth, maxWidth]"
|
||||
:class="mergedGroupClass"
|
||||
>
|
||||
<button
|
||||
:id="buttonId"
|
||||
@@ -26,7 +25,7 @@
|
||||
? openDirection === 'down'
|
||||
? 'rounded-b-none !border-2 !border-m-primary !border-b-0'
|
||||
: 'rounded-t-none !border-2 !border-m-primary !border-t-0'
|
||||
: isOptionSelected
|
||||
: isOptionSelected
|
||||
? 'border-black'
|
||||
: 'border-m-muted',
|
||||
disabled ? 'cursor-not-allowed border-m-muted text-black/60' : 'cursor-pointer',
|
||||
@@ -45,7 +44,7 @@
|
||||
v-if="label"
|
||||
class="floating-label pointer-events-none absolute left-3 inline-block origin-left transition-transform duration-150 font-medium"
|
||||
:class="[
|
||||
shouldFloatLabel ? 'top-2 z-30' : 'top-1/2 -translate-y-1/2',
|
||||
isOpen ? 'top-2 z-30' : 'top-2',
|
||||
hasError
|
||||
? 'text-m-danger'
|
||||
: hasSuccess
|
||||
@@ -206,6 +205,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick} from 'vue'
|
||||
import {Icon as IconifyIcon} from '@iconify/vue'
|
||||
import {twMerge} from 'tailwind-merge'
|
||||
import Checkbox from '../checkbox/Checkbox.vue'
|
||||
|
||||
defineOptions({name: 'MalioSelectCheckbox', inheritAttrs: false})
|
||||
@@ -232,6 +232,7 @@ const props = withDefaults(defineProps<{
|
||||
displaySelectAll?: boolean
|
||||
selectAllLabel?: string
|
||||
disabled?: boolean
|
||||
groupClass?: string
|
||||
}>(), {
|
||||
options: () => [],
|
||||
emptyOptionLabel: '',
|
||||
@@ -249,6 +250,7 @@ const props = withDefaults(defineProps<{
|
||||
displaySelectAll: false,
|
||||
selectAllLabel: 'Tout sélectionner',
|
||||
disabled: false,
|
||||
groupClass: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -264,6 +266,9 @@ const listboxId = `custom-select-listbox-${uid}`
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
const listHeight = ref(0)
|
||||
const normalizedOptions = computed<Option[]>(() => props.options)
|
||||
const mergedGroupClass = computed(() =>
|
||||
twMerge('relative w-full', props.minWidth, props.maxWidth, props.groupClass),
|
||||
)
|
||||
const hasError = computed(() => !!props.error)
|
||||
const hasSuccess = computed(() => !!props.success && !hasError.value)
|
||||
const isOptionSelected = computed(() =>
|
||||
@@ -281,6 +286,10 @@ const shouldFloatLabel = computed(() =>
|
||||
const selectionSummary = computed(() =>
|
||||
`${props.modelValue.length}/${normalizedOptions.value.length}`
|
||||
)
|
||||
const allSelected = computed(() =>
|
||||
normalizedOptions.value.length > 0
|
||||
&& normalizedOptions.value.every(opt => props.modelValue.includes(opt.value)),
|
||||
)
|
||||
const describedBy = computed(() =>
|
||||
(hasError.value || hasSuccess.value || !!props.hint) ? `${buttonId}-describedby` : undefined,
|
||||
)
|
||||
@@ -320,18 +329,22 @@ function open() {
|
||||
}
|
||||
|
||||
const labelTransformStyle = computed(() => {
|
||||
// label non flottant
|
||||
if (!shouldFloatLabel.value) {
|
||||
return undefined
|
||||
return {}
|
||||
}
|
||||
|
||||
// fermé ou ouverture vers le bas : comportement classique
|
||||
if (!isOpen.value || openDirection.value === 'down') {
|
||||
return {
|
||||
transform: 'translateY(-1.15rem) scale(0.9)',
|
||||
}
|
||||
}
|
||||
|
||||
// ouverture vers le haut : on remonte en fonction de la hauteur de la liste
|
||||
const extraOffset = 8 // marge visuelle au-dessus de la liste en px
|
||||
const total = 4 + listHeight.value + extraOffset
|
||||
const total = 4 +listHeight.value + extraOffset
|
||||
// 18 ≈ 1.15rem pour garder la même base que votre flottant actuel
|
||||
|
||||
return {
|
||||
transform: `translateY(-${total}px) scale(0.9)`,
|
||||
@@ -351,19 +364,6 @@ function toggle() {
|
||||
open()
|
||||
}
|
||||
|
||||
const allSelected = computed(() =>
|
||||
normalizedOptions.value.length > 0
|
||||
&& normalizedOptions.value.every(opt => props.modelValue.includes(opt.value)),
|
||||
)
|
||||
|
||||
function toggleAll() {
|
||||
if (allSelected.value) {
|
||||
emit('update:modelValue', [])
|
||||
} else {
|
||||
emit('update:modelValue', normalizedOptions.value.map(opt => opt.value))
|
||||
}
|
||||
}
|
||||
|
||||
function isChecked(value: string | number) {
|
||||
return props.modelValue.includes(value)
|
||||
}
|
||||
@@ -373,10 +373,17 @@ function toggleOption(value: string | number) {
|
||||
emit('update:modelValue', props.modelValue.filter(item => item !== value))
|
||||
return
|
||||
}
|
||||
|
||||
emit('update:modelValue', [...props.modelValue, value])
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
if (allSelected.value) {
|
||||
emit('update:modelValue', [])
|
||||
} else {
|
||||
emit('update:modelValue', normalizedOptions.value.map(opt => opt.value))
|
||||
}
|
||||
}
|
||||
|
||||
function onClickOutside(e: MouseEvent) {
|
||||
if (!root.value) return
|
||||
if (!root.value.contains(e.target as Node)) close()
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"files": [
|
||||
"app/**",
|
||||
"nuxt.config.ts",
|
||||
"tailwind.config.ts",
|
||||
"README.md",
|
||||
"COMPONENTS.md"
|
||||
],
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type {Config} from 'tailwindcss'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
import {dirname, join} from 'node:path'
|
||||
|
||||
const dir = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default {
|
||||
content: [
|
||||
'./app/**/*.{vue,js,ts}',
|
||||
'./app/**/*.story.{vue,js,ts}',
|
||||
'./.playground/**/*.{vue,js,ts}',
|
||||
'./histoire.setup.ts',
|
||||
'./histoire.config.ts',
|
||||
join(dir, 'app/**/*.{vue,js,ts}'),
|
||||
join(dir, 'app/**/*.story.{vue,js,ts}'),
|
||||
join(dir, '.playground/**/*.{vue,js,ts}'),
|
||||
join(dir, 'histoire.setup.ts'),
|
||||
join(dir, 'histoire.config.ts'),
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
Reference in New Issue
Block a user