enhance select dropdown + combobox

This commit is contained in:
Jordan Blasenhauer 2024-06-10 11:57:34 +02:00
parent 197436c9d5
commit c18c23e785
7 changed files with 85 additions and 121 deletions

View file

@ -177,11 +177,11 @@ body {
}
.input-header-label {
@apply relative lowercase capitalize-first mb-1 transition duration-300 ease-in-out text-sm font-bold m-0 dark:text-gray-300 mr-2;
@apply relative lowercase capitalize-first mb-1 transition duration-300 ease-in-out text-sm font-bold m-0 dark:text-gray-300 mr-1;
}
.input-header-required-sign {
@apply font-bold text-red-500 absolute ml-1;
@apply font-bold text-red-500 absolute;
}
.input-error-msg {
@ -229,8 +229,12 @@ body {
@apply pointer-events-none transition-transform h-4 w-4 fill-gray-600 dark:fill-gray-500;
}
.select-combobox-dropdown-container {
@apply flex w-fit mt-2;
}
.select-combobox-list {
@apply flex flex-col;
@apply flex flex-col max-h-[200px] overflow-x-hidden overflow-y-auto;
}
.combobox-no-match {
@ -238,15 +242,15 @@ body {
}
.select-dropdown-container {
@apply flex fixed h-full max-h-[200px] overflow-x-hidden overflow-y-auto flex-col w-fit mt-2;
@apply flex max-h-[200px] overflow-x-hidden overflow-y-auto flex-col w-fit mt-2;
}
.open.select-dropdown-container {
@apply z-[20] opacity-100;
@apply absolute z-[20] opacity-100;
}
.close.select-dropdown-container {
@apply -z-10 opacity-0 pointer-events-none;
@apply -z-10 fixed opacity-0 pointer-events-none;
}
.select-dropdown-btn {

File diff suppressed because one or more lines are too long

View file

@ -135,7 +135,7 @@ watch(props, (newProp, oldProp) => {
});
const inp = reactive({
value: props.value,
value: "",
isValid: false,
});
@ -166,66 +166,37 @@ function toggleSelect() {
select.isOpen = select.isOpen ? false : true;
// Position dropdown relative to btn on open on fixed position
if (select.isOpen) {
// focus on combobox and reset value
// Reset input value
inp.value = "";
inp.isValid = inputEl.value.checkValidity();
inputEl.value.focus();
// setup dropdown position
// Get field container rect
const fieldContainer = selectBtn.value.closest("[data-field-container]");
const parent = fieldContainer.parentElement;
// Update position only if parent has overflow
const isOverflow = parent.scrollHeight > parent.clientHeight ? true : false;
if (!isOverflow) return;
// Get all rect
const selectBtnRect = selectBtn.value.getBoundingClientRect();
const selectDropdownRect = selectDropdown.value.getBoundingClientRect();
const fieldContainerRect = fieldContainer.getBoundingClientRect();
const selectDropRect = selectDropdown.value.getBoundingClientRect();
// We need to take care of parent padding and margin that will affect dropdown position but aren't calculate in rect
const parents = [selectBtn.value.parentElement];
let isParent = selectBtn.value.parentElement ? true : false;
while (isParent) {
parents.push(parents[parents.length - 1].parentElement);
isParent = parents[parents.length - 1].parentElement ? true : false;
}
const parentRect = parent.getBoundingClientRect();
let noRectParentHeight = 0;
for (let i = 0; i < parents.length; i++) {
try {
noRectParentHeight += +window
.getComputedStyle(parents[i], null)
.getPropertyValue("padding-top")
.replace("px", "");
} catch (e) {}
try {
noRectParentHeight += +window
.getComputedStyle(parents[i], null)
.getPropertyValue("margin-top")
.replace("px", "");
} catch (e) {}
}
// If dropdown is too close to bottom, we need to drop it up
const canDropBeDown =
selectBtnRect.top + selectDropdownRect.height + 20 < window.innerHeight
const canBeDown =
fieldContainerRect.bottom + selectDropRect.height < parentRect.bottom
? true
: false;
if (canDropBeDown) {
selectDropdown.value.style.top = `${
window.scrollY +
selectBtnRect.bottom +
selectBtnRect.height * 2 -
selectDropdownRect.height -
noRectParentHeight +
16
if (!canBeDown) {
selectDropdown.value.style.top = `-${
selectDropRect.height + selectBtnRect.height
}px`;
}
if (!canDropBeDown) {
selectDropdown.value.style.top = `${
window.scrollY +
selectBtnRect.top +
selectBtnRect.height * 2 -
selectDropdownRect.height * 2 -
noRectParentHeight -
24
}px`;
if (canBeDown) {
selectDropdown.value.style.top = `${selectBtnRect.height}px`;
}
}
}
@ -295,6 +266,8 @@ const emits = defineEmits(["inp"]);
<template>
<Container
data-field-container
:class="[select.isOpen ? 'z-[100]' : '']"
:containerClass="`field-container ${props.containerClass}`"
:columns="props.columns"
>
@ -334,7 +307,6 @@ const emits = defineEmits(["inp"]);
:aria-controls="`${props.id}-custom`"
:aria-expanded="select.isOpen ? 'true' : 'false'"
:aria-description="$t('inp_select_dropdown_button_desc')"
data-select-dropdown
:disabled="props.disabled || false"
@click.prevent="toggleSelect()"
:class="[
@ -380,6 +352,7 @@ const emits = defineEmits(["inp"]);
:tabindex="select.isOpen ? props.tabId : '-1'"
ref="inputEl"
v-model="inp.value"
:placeholder="$t('inp_combobox_placeholder')"
@input="
() => {
inp.isValid = inputEl.checkValidity();
@ -393,7 +366,6 @@ const emits = defineEmits(["inp"]);
inp.isValid ? 'valid' : 'invalid',
props.inpClass,
]"
:placeholder="props.placeholder || ''"
:pattern="props.pattern || '(?s).*'"
:name="`${props.id}-combobox`"
:value="inp.value"
@ -415,6 +387,7 @@ const emits = defineEmits(["inp"]);
</div>
</div>
<div
data-select-dropdown
:id="`${props.id}-list`"
:aria-hidden="select.isOpen ? 'false' : 'true'"
role="radiogroup"
@ -434,8 +407,6 @@ const emits = defineEmits(["inp"]);
role="radio"
@click.prevent="$emit('inp', changeValue(value))"
:class="[
id === 0 ? 'first' : '',
id === props.values.length - 1 ? 'last' : '',
(value === select.value && select.value === value) ||
(!select.value && value === props.value)
? 'active'

View file

@ -636,6 +636,7 @@ function setIndex(calendarEl, tabindex) {
<template>
<Container
:class="[picker.isOpen ? 'z-[100]' : '']"
v-if="props.inpType === 'datepicker'"
:containerClass="`field-container ${props.containerClass}`"
:columns="props.columns"

View file

@ -30,7 +30,7 @@ import { v4 as uuidv4 } from "uuid";
iconColor: "info",
},]
}
@param {string} [id=uuidv4()] - Unique id
@param {string} [id=uuidv4()] - Unique id
@param {string} label - The label of the field. Can be a translation key or by default raw text.
@param {string} name - The name of the field. Case no label, this is the fallback. Can be a translation key or by default raw text.
@param {string} value
@ -158,62 +158,35 @@ const selectDropdown = ref();
// EVENTS
function toggleSelect() {
select.isOpen = select.isOpen ? false : true;
// Position dropdown relative to btn on open on fixed position
// Check if parent has overflow
if (select.isOpen) {
// Get field container rect
const fieldContainer = selectBtn.value.closest("[data-field-container]");
const parent = fieldContainer.parentElement;
// Update position only if parent has overflow
const isOverflow = parent.scrollHeight > parent.clientHeight ? true : false;
if (!isOverflow) return;
// Get all rect
const selectBtnRect = selectBtn.value.getBoundingClientRect();
const selectDropdownRect = selectDropdown.value.getBoundingClientRect();
const fieldContainerRect = fieldContainer.getBoundingClientRect();
const selectDropRect = selectDropdown.value.getBoundingClientRect();
// We need to take care of parent padding and margin that will affect dropdown position but aren't calculate in rect
const parents = [selectBtn.value.parentElement];
let isParent = selectBtn.value.parentElement ? true : false;
while (isParent) {
parents.push(parents[parents.length - 1].parentElement);
isParent = parents[parents.length - 1].parentElement ? true : false;
}
const parentRect = parent.getBoundingClientRect();
let noRectParentHeight = 0;
for (let i = 0; i < parents.length; i++) {
try {
noRectParentHeight += +window
.getComputedStyle(parents[i], null)
.getPropertyValue("padding-top")
.replace("px", "");
} catch (e) {}
try {
noRectParentHeight += +window
.getComputedStyle(parents[i], null)
.getPropertyValue("margin-top")
.replace("px", "");
} catch (e) {}
}
// If dropdown is too close to bottom, we need to drop it up
const canDropBeDown =
selectBtnRect.top + selectDropdownRect.height + 20 < window.innerHeight
const canBeDown =
fieldContainerRect.bottom + selectDropRect.height < parentRect.bottom
? true
: false;
if (canDropBeDown) {
selectDropdown.value.style.top = `${
window.scrollY +
selectBtnRect.bottom +
selectBtnRect.height * 2 -
selectDropdownRect.height -
noRectParentHeight +
16
if (!canBeDown) {
selectDropdown.value.style.top = `-${
selectDropRect.height + selectBtnRect.height
}px`;
}
if (!canDropBeDown) {
selectDropdown.value.style.top = `${
window.scrollY +
selectBtnRect.top +
selectBtnRect.height * 2 -
selectDropdownRect.height * 2 -
noRectParentHeight -
24
}px`;
if (canBeDown) {
selectDropdown.value.style.top = `${selectBtnRect.height}px`;
}
}
}
@ -283,6 +256,8 @@ const emits = defineEmits(["inp"]);
<template>
<Container
:class="[select.isOpen ? 'z-[100]' : '']"
data-field-container
:containerClass="`field-container ${props.containerClass}`"
:columns="props.columns"
>

View file

@ -86,7 +86,12 @@
"inp_popover_multisite": "This setting is multisite.",
"inp_popover_global": "This setting is global.",
"inp_combobox": "Combobox input for relate select radio group.",
"inp_combobox_no_match": "No match found.",
"inp_combobox_no_match": "No match found",
"inp_select_dropdown_button_desc": "Toggle hide/show radio group (dropdown) to change value.",
"inp_select_dropdown_desc": "Radio group (dropdown) to change value.",
"inp_input_clipboard_desc": "Copy to clipboard.",
"inp_input_password_desc": "Toggle hide/show password.",
"inp_combobox_placeholder": "Search",
"action_send": "send {name}",
"action_start": "start {name}",
"action_disable": "disable {name}",

View file

@ -6,6 +6,9 @@ import Input from "@components/Forms/Field/Input.vue";
import Datepicker from "@components/Forms/Field/Datepicker.vue";
import Combobox from "@components/Forms/Field/Combobox.vue";
import Button from "@components/Widget/Button.vue";
import DashboardLayout from "@components/Dashboard/Layout.vue";
import GridLayout from "@components/Widget/GridLayout.vue";
import Grid from "@components/Widget/Grid.vue";
const checkboxData = {
id: "test-checkbox",
@ -27,6 +30,7 @@ const selectData = {
requiredValues: ["no"],
label: "Test select",
tabId: "1",
columns: { pc: 12, tablet: 12, mobile: 12 },
};
const inputData = {
@ -39,6 +43,7 @@ const inputData = {
label: "Test input",
pattern: "(test)",
tabId: "1",
columns: { pc: 12, tablet: 12, mobile: 12 },
};
const datepickerData = {
@ -104,27 +109,30 @@ const buttonOnlySVGData = {
const comboboxData = {
id: "test-combobox",
value: "yes",
values: ["yes", "no"],
values: ["yes", "no", "tes", "f", "fes", "esd", "fesfse"],
name: "test-combobox",
disabled: false,
required: false,
requiredValues: ["no"],
label: "Test combobox",
tabId: "1",
columns: { pc: 12, tablet: 12, mobile: 12 },
};
</script>
<template>
<div class="bg-secondary flex flex-col items-center justify-center h-full">
<div style="width: 300px">
<Checkbox v-bind="checkboxData" />
<Select v-bind="selectData" />
<Input v-bind="inputData" />
<Datepicker v-bind="datepickerData" />
<Combobox v-bind="comboboxData" />
<Button v-bind="buttonData" />
<Button v-bind="buttonSVGData" />
<Button v-bind="buttonOnlySVGData" />
</div>
</div>
<DashboardLayout>
<GridLayout :columns="{ pc: 4, tablet: 6, mobile: 12 }">
<!-- widget grid -->
<Grid>
<Input v-bind="inputData" />
</Grid>
</GridLayout>
<GridLayout :columns="{ pc: 4, tablet: 6, mobile: 12 }">
<!-- widget grid -->
<Grid>
<Combobox v-bind="comboboxData" />
</Grid>
</GridLayout>
</DashboardLayout>
</template>