bug fixes, half-finished admin ui, and a more
This commit is contained in:
39
ui/components/vlAccordion/content.vue
Executable file
39
ui/components/vlAccordion/content.vue
Executable file
@@ -0,0 +1,39 @@
|
||||
<script setup>
|
||||
const item = inject('accordionItem');
|
||||
const contentHeight = ref(0);
|
||||
const content = ref(null);
|
||||
|
||||
let timeout;
|
||||
watch(item.hidden, () => {
|
||||
if (!item.hidden.value) {
|
||||
timeout = setTimeout(() => {
|
||||
let styles = window.getComputedStyle(content.value);
|
||||
let margin = parseFloat(styles['marginTop']) +
|
||||
parseFloat(styles['marginBottom']);
|
||||
|
||||
contentHeight.value = content.value.offsetHeight + margin;
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(timeout)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="`vueless-${item.index}`" role="region" :aria-labelledby="`vueless-${item.index}`"
|
||||
class="vl-accordion-content" :style="`--vueless-accordion-content-height: ${contentHeight}px`"
|
||||
:data-state="(item.isOpen.value) ? 'open' : 'closed'"
|
||||
@animationend="(!item.isOpen.value) ? item.hidden.value = true : ''" :hidden="item.hidden.value">
|
||||
<div ref="content" v-bind="attrs">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
139
ui/components/vlAccordion/index.vue
Executable file
139
ui/components/vlAccordion/index.vue
Executable file
@@ -0,0 +1,139 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: "multiple"
|
||||
},
|
||||
defaultValue: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
const accordion = ref(null);
|
||||
|
||||
const accordionItems = ref([])
|
||||
|
||||
function toggleAccordion(index) {
|
||||
const item = accordionItems.value[index];
|
||||
if (props.type === "single") {
|
||||
// close everything but the one we just opened
|
||||
accordionItems.value.forEach((item, i) => {
|
||||
if (i === index) return;
|
||||
item.isOpen = false;
|
||||
})
|
||||
}
|
||||
if (item.hidden) {
|
||||
item.hidden = false;
|
||||
}
|
||||
item.isOpen = !item.isOpen;
|
||||
}
|
||||
|
||||
const registerAccordionItem = (value) => {
|
||||
const item = { isOpen: ref(false), hidden: ref(true), value }
|
||||
accordionItems.value.push(item);
|
||||
|
||||
return { index: accordionItems.value.indexOf(item), isOpen: item.isOpen, hidden: item.hidden, value };
|
||||
};
|
||||
|
||||
const unregisterAccordionItem = (index) => {
|
||||
accordionItems.value.splice(index, 1);
|
||||
};
|
||||
|
||||
provide('accordion', { registerAccordionItem, unregisterAccordionItem, toggleAccordion })
|
||||
|
||||
function keydown(event) {
|
||||
const headers = Array.from(accordion.value.querySelectorAll(".vl-accordion-header"));
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
const focusedElement = document.activeElement;
|
||||
const currentIndex = headers.indexOf(focusedElement);
|
||||
const nextIndex = currentIndex > 0 ? currentIndex - 1 : headers.length - 1;
|
||||
console.log(nextIndex, headers)
|
||||
const nextButton = headers[nextIndex];
|
||||
nextButton.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
const focusedElement = document.activeElement;
|
||||
const currentIndex = headers.indexOf(focusedElement);
|
||||
const nextIndex = currentIndex < headers.length - 1 ? currentIndex + 1 : 0;
|
||||
const nextButton = headers[nextIndex];
|
||||
console.log(nextIndex, headers)
|
||||
nextButton.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "End") {
|
||||
event.preventDefault();
|
||||
return headers[headers.length - 1].focus();
|
||||
}
|
||||
|
||||
if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
return headers[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!!props.defaultValue) {
|
||||
const item = accordionItems.value.filter(item => item.value === props.defaultValue)[0];
|
||||
item.isOpen = true;
|
||||
item.hidden = false;
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
watch(props, () => {
|
||||
if (!!props.defaultValue) {
|
||||
const item = accordionItems.value.filter(item => item.value === props.defaultValue)[0];
|
||||
item.isOpen = true;
|
||||
item.hidden = false;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vl-accordion" ref="accordion" @keydown="keydown($event)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vl-accordion-content {
|
||||
overflow: hidden;
|
||||
transform-origin: top center;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
|
||||
.vl-accordion-content[data-state="closed"] {
|
||||
animation: 300ms cubic-bezier(0.25, 1, 0.5, 1) 0s 1 normal forwards running closeAccordion;
|
||||
}
|
||||
|
||||
.vl-accordion-content[data-state="open"] {
|
||||
animation: 300ms cubic-bezier(0.25, 1, 0.5, 1) 0s 1 normal forwards running openAccordion;
|
||||
}
|
||||
|
||||
@keyframes closeAccordion {
|
||||
0% {
|
||||
height: var(--vueless-accordion-content-height);
|
||||
}
|
||||
|
||||
100% {
|
||||
height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes openAccordion {
|
||||
0% {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
100% {
|
||||
height: var(--vueless-accordion-content-height);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
ui/components/vlAccordion/item.vue
Executable file
19
ui/components/vlAccordion/item.vue
Executable file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
}
|
||||
})
|
||||
const accordion = inject('accordion')
|
||||
|
||||
const item = accordion.registerAccordionItem(props.value);
|
||||
|
||||
provide('accordionItem', item);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vl-accordion-item" :data-state="(item.isOpen.value) ? 'open' : 'closed'">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
29
ui/components/vlAccordion/trigger.vue
Executable file
29
ui/components/vlAccordion/trigger.vue
Executable file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
const item = inject('accordionItem');
|
||||
const { toggleAccordion } = inject('accordion');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="vl-accordion-header focus-visible:outline-none focus-visible:ring focus-visible:ring-inset select-none cursor-pointer w-full"
|
||||
@click="toggleAccordion(item.index)">
|
||||
<div class="flex flex-1 justify-between items-center w-full" :id="`vueless-${item.index}`"
|
||||
:aria-controls="`vueless-${item.index}`" :aria-expanded="item.isOpen.value">
|
||||
<slot />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="m6 9l6 6l6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.vl-accordion-item button div svg {
|
||||
transition: transform 300ms ease;
|
||||
}
|
||||
|
||||
.vl-accordion-item[data-state="open"] button div svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user