update Button to work as tab

This commit is contained in:
Jordan Blasenhauer 2024-08-12 16:08:01 +02:00
parent bb0cfb74c6
commit 5e6fa1ecf4
6 changed files with 122 additions and 44 deletions

View file

@ -13,6 +13,8 @@ import { contentIndex } from "@utils/tabindex.js";
import Container from "@components/Widget/Container.vue";
import Icons from "@components/Widget/Icons.vue";
import { useUUID } from "@utils/global.js";
import { useDisplayStore } from "@store/global.js";
/**
* @name Widget/Button.vue
* @description This component is a standard button.
@ -29,12 +31,13 @@ import { useUUID } from "@utils/global.js";
* }
* @param {string} [id=uuidv4()] - Unique id of the button
* @param {string} text - Content of the button. Can be a translation key or by default raw text.
* @param {array} [display=[]] - Case display, we will update the display store with the given array. Useful when we want to use button as tabs.
* @param {string} [type="button"] - Can be of type button || submit
* @param {boolean} [disabled=false]
* @param {boolean} [hideText=false] - Hide text to only display icon
* @param {string} [color="primary"]
* @param {string} [iconColor=""] - Color we want to apply to the icon. If falsy value, default icon color is applied.
* @param {string} [size="normal"] - Can be of size sm || normal || lg || xl
* @param {string} [size="normal"] - Can be of size sm || normal || lg || xl or tab
* @param {string} [iconName=""] - Name in lowercase of icons store on /Icons. If falsy value, no icon displayed.
* @param {Object} [attrs={}] - List of attributes to add to the button. Some attributes will conduct to additional script
* @param {Object|boolean} [modal=false] - We can link the button to a Modal component. We need to pass the widgets inside the modal. Button click will open the modal.
@ -54,6 +57,11 @@ const props = defineProps({
required: true,
default: "",
},
emitValue: {
type: String,
required: false,
default: "",
},
type: {
type: String,
required: false,
@ -104,6 +112,11 @@ const props = defineProps({
required: false,
default: false,
},
display: {
type: Array,
required: false,
default: [],
},
containerClass: {
type: String,
required: false,
@ -116,19 +129,61 @@ const props = defineProps({
},
});
const displayStore = useDisplayStore();
const btn = reactive({
id: "",
openModal: false,
modalId: props.modal ? `${props.id}-modal` : "",
class:
props.color === "transparent"
? `${props.size}`
: `btn ${props.color} ${props.size}`,
isActive: false,
});
const btnEl = ref();
const buttonClass = computed(() => {
// transparent useful when we only want to display icon without background or shadow style
if (props.color === "transparent") return `${props.size}`;
return `btn ${props.color} ${props.size}`;
});
/**
* @name handleClick
* @description Wrap all the logic to handle the click event on the button.
* This will prevent submit if not a submit button, open a modal if one is attached and update the display store if needed.
* @param {event} e - Event object
* @returns {void}
*/
function handleClick(e) {
if (e.target.getAttribute("type") !== "submit") e.preventDefault();
if (typeof props.modal === "object") {
btn.openModal = true;
}
if (props.display.length) {
console.log("update", btn.isActive);
displayStore.setDisplay(props.display[0], props.display[1]);
}
}
// Add a11y attributs and update when needed in case the button is related to a display group
if (props.display.length) {
watch(displayStore.display, (val) => {
const isCurrent = displayStore.isCurrentDisplay(
props.display[0],
props.display[1]
);
btnEl.value.setAttribute("aria-controls", btn.modalId);
btnEl.value.setAttribute("aria-expanded", isCurrent ? "true" : "false");
btn.isActive = isCurrent ? true : false;
});
}
// watch openModal to add accessibility data
watch(
() => btn.openModal,
(value) => {
if (typeof props.modal === "object") {
btnEl.value.setAttribute("aria-expanded", value ? "true" : "false");
}
}
);
onBeforeMount(() => {
btn.id = useUUID(props.id);
@ -141,28 +196,18 @@ onMounted(() => {
btnEl.value.setAttribute("aria-controls", btn.modalId);
btnEl.value.setAttribute(
"aria-expanded",
props.openModal ? "true" : "false",
props.openModal ? "true" : "false"
);
}
});
// watch openModal to add accessibility data
watch(
() => btn.openModal,
(value) => {
if (typeof props.modal === "object") {
btnEl.value.setAttribute("aria-expanded", value ? "true" : "false");
}
},
);
if (btnEl.value?.closest("[data-is='tabs']")) {
btnEl.value.setAttribute("role", "tab");
}
});
</script>
<template>
<Container
data-is="button"
:containerClass="`${props.containerClass}`"
:columns="props.columns"
>
<Container data-is="button" :containerClass="`${props.containerClass}`">
<button
data-is="button"
:type="props.type"
@ -170,22 +215,23 @@ watch(
:id="btn.id"
@click="
(e) => {
if (e.target.getAttribute('type') !== 'submit') e.preventDefault();
if (typeof props.modal === 'object') {
btn.openModal = true;
}
handleClick(e);
}
"
v-bind="props.attrs || {}"
:tabindex="props.tabId"
:class="[buttonClass]"
:class="[
btn.class,
btn.isActive ? 'active' : '',
props.iconName && !props.hideText ? 'icon' : 'no-icon',
]"
:disabled="props.disabled || false"
:aria-labelledby="`text-${btn.id}`"
>
<span
:class="[
props.hideText ? 'sr-only' : '',
props.iconName ? 'mr-2' : '',
props.iconName && !props.hideText ? 'mr-2' : '',
'pointer-events-none',
]"
:id="`text-${btn.id}`"

View file

@ -59,7 +59,7 @@ const groupEl = ref(null);
onMounted(() => {
group.class =
props.boutonGroupClass || groupEl?.value?.closest("[data-is]")
props.boutonGroupClass || groupEl?.value.closest("[data-is]")
? `button-group-${groupEl.value
.closest("[data-is]")
.getAttribute("data-is")}`

View file

@ -97,6 +97,7 @@ if (props.display.length) {
const containerClass = computed(() => {
if (props.type === "card") return "layout-card";
if (props.type === "tabs") return "layout-tabs";
return "";
});
@ -129,12 +130,7 @@ onMounted(() => {
:id="container.id"
:is="props.link ? 'a' : 'div'"
:data-is="`${props.type}`"
:class="[
containerClass,
gridClass,
props.gridLayoutClass,
'layout-grid-layout',
]"
:class="[containerClass, gridClass, props.gridLayoutClass]"
>
<slot></slot>
</component>

View file

@ -4,6 +4,7 @@ import Grid from "@components/Widget/Grid.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import Tabulator from "@components/Widget/Tabulator.vue";
import ButtonGroup from "@components/Widget/ButtonGroup.vue";
import { useEqualStr } from "@utils/global.js";
import { useTableStore } from "@store/global.js";
import { onMounted } from "vue";
@ -139,9 +140,27 @@ const items = [
];
const builder = [
{
type: "tabs",
widgets: [
{
type: "ButtonGroup",
data: {
buttons: [
{
text: "test",
display: ["main", 0],
isTab: true,
size: "tab",
iconName: "globe",
},
],
},
},
],
},
{
type: "card",
gridLayoutClass: "transparent",
widgets: [
{
type: "Tabulator",
@ -167,7 +186,6 @@ const builder = [
<template>
<DashboardLayout>
<div class="col-span-12 w-full border-b borber-b-white">f</div>
<GridLayout
v-for="(container, index) in builder"
:key="index"
@ -186,6 +204,10 @@ const builder = [
v-if="useEqualStr(widget.type, 'Tabulator')"
v-bind="widget.data"
/>
<ButtonGroup
v-if="useEqualStr(widget.type, 'ButtonGroup')"
v-bind="widget.data"
/>
</template>
</Grid>
</GridLayout>

View file

@ -508,16 +508,16 @@ body {
/* LAYOUT */
.layout-grid-layout {
@apply px-6 py-4 overflow-hidden break-words grid grid-cols-12 w-full;
}
.layout-grid {
@apply col-span-12 grid grid-cols-12 w-full relative;
}
.layout-card {
@apply overflow-visible relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border transform duration-300 ease-in-out;
@apply px-6 py-4 grid grid-cols-12 w-full overflow-visible relative transition dark:brightness-110 shadow-md bg-white dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border transform duration-300 ease-in-out;
}
.layout-tabs {
@apply overflow-visible relative transition dark:brightness-110 transform duration-300 ease-in-out;
}
.layout-modal-container {
@ -1150,6 +1150,10 @@ body {
@apply flex justify-center items-center mx-4;
}
.button-group-tabs {
@apply col-span-12 flex justify-center items-center;
}
.button-group-multiple {
@apply col-span-12 flex justify-center items-center mx-4;
}
@ -1391,7 +1395,17 @@ body {
/* BTN */
.btn {
@apply flex justify-center items-end leading-none tracking-wide dark:brightness-90 inline-block font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0;
@apply w-full flex justify-center items-end tracking-wide dark:brightness-90 font-bold text-center text-white uppercase align-middle transition-all rounded-lg cursor-pointer leading-normal ease-in shadow-xs hover:-translate-y-px active:opacity-85 hover:shadow-md disabled:cursor-not-allowed dark:disabled:text-gray-300 disabled:text-gray-700 disabled:bg-gray-400 disabled:border-gray-400/0 dark:disabled:bg-gray-700 dark:disabled:border-gray-700/0 disabled:hover:translate-y-0 disabled:hover:bg-gray-400 disabled:hover:border-gray-400/0 dark:disabled:hover:translate-y-0 dark:disabled:hover:bg-gray-700 dark:disabled:hover:border-gray-700/0;
}
.btn.active:not([disabled]) {
@apply shadow-md brightness-90 dark:shadow-md dark:brightness-75;
}
/* BTN SIZE */
.btn.tab {
@apply px-3 py-2.5;
}
.btn.xl {

File diff suppressed because one or more lines are too long