initial commit

This commit is contained in:
Zoe
2023-05-30 15:04:51 -05:00
commit 539805dacb
20 changed files with 14788 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<script setup>
const item = inject('accordionItem');
const contentHeight = ref(0);
const content = ref(null);
watch(item.hidden, () => {
if (!item.hidden.value) {
setTimeout(() => {
contentHeight.value = content.value.offsetHeight;
})
}
})
</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">
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,194 @@
<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.parentElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : headers.length - 1;
const nextButton = headers[nextIndex].querySelector("button");
nextButton.focus();
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
const focusedElement = document.activeElement;
const currentIndex = headers.indexOf(focusedElement.parentElement);
const nextIndex = currentIndex < headers.length - 1 ? currentIndex + 1 : 0;
const nextButton = headers[nextIndex].querySelector("button");
nextButton.focus();
return;
}
if (event.key === "End") {
event.preventDefault();
return headers[headers.length - 1].querySelector("button").focus();
}
if (event.key === "Home") {
event.preventDefault();
return headers[0].querySelector("button").focus();
}
}
onMounted(() => {
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 {
border-radius: 6px;
background-color: #1D1E1F;
box-shadow: 0 0 16px 0 #07070738;
width: 70%;
user-select: none;
cursor: default;
}
.vl-accordion-content {
width: var(--vueless-accordion-content-width);
background-color: #0B0C0D;
overflow: hidden;
transform-origin: top center;
height: 0;
}
.vl-accordion-item:focus-within {
position: relative;
z-index: 1;
box-shadow: #4e367e 0px 0px 0px 2px
}
.vl-accordion-content div {
padding: 15px 20px;
}
.vl-accordion-content[data-state="closed"] {
animation: 300ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards running closeAccordion;
}
.vl-accordion-content[data-state="open"] {
animation: 300ms cubic-bezier(0.87, 0, 0.13, 1) 0s 1 normal forwards running openAccordion;
}
.vl-accordion-item h3 button svg {
transition: transform 300ms ease;
}
.vl-accordion-item[data-state="open"] h3 button svg {
transform: rotate(180deg);
}
.vl-accordion-item {
background-color: #161718;
height: 100%;
margin-top: 1px;
overflow: hidden;
}
.vl-accordion-item:first-child {
margin-top: 0px;
border-radius: 6px 6px 0 0;
}
.vl-accordion-item:last-child {
border-radius: 0 0 6px 6px;
}
.vl-accordion-header:hover {
background-color: #131415;
}
.vl-accordion-header button {
all: unset;
background-color: transparent;
height: 45px;
display: flex;
align-items: center;
justify-content: space-between;
line-height: 1;
padding: 0px 20px;
flex: 1 1 0%;
box-shadow: #1D1E1F 0px 1px 0px;
}
.vl-accordion-header {
all: unset;
display: flex;
}
@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>

View 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>

View File

@@ -0,0 +1,13 @@
<script setup>
const item = inject('accordionItem');
const { toggleAccordion } = inject('accordion');
</script>
<template>
<h3 class="vl-accordion-header">
<button :id="`vueless-${item.index}`" :aria-controls="`vueless-${item.index}`" :aria-expanded="item.isOpen.value"
@click="toggleAccordion(item.index)">
<slot />
</button>
</h3>
</template>

151
components/vlDropdown.vue Normal file
View File

@@ -0,0 +1,151 @@
<script setup>
const hex = ref((Math.random() * 0xfffff * 1000000).toString(16).slice(0, 6).toString());
const dropdownOpen = ref(false);
const dropdownButton = ref(null);
const dropdown = ref(null);
function dropdownButtonKeypress(event) {
if (event.key !== "Enter" && event.key !== " " && event.key !== "ArrowUp" && event.key !== "ArrowDown" || dropdownOpen.value === true) return;
event.preventDefault();
if (event.key === "ArrowUp") {
dropdownOpen.value = true;
setTimeout(() => {
dropdown.value.children[dropdown.value.children.length - 1].focus();
})
return;
}
if (event.key === "ArrowDown") {
dropdownOpen.value = true;
setTimeout(() => {
dropdown.value.children[1].focus();
})
return;
}
if (event.key === "Enter" || event.key === " ") {
dropdownOpen.value = !dropdownOpen.value;
setTimeout(() => {
dropdown.value.children[1].focus();
})
}
}
function dropdownKeypress(event) {
if (event.key === "ArrowUp") {
event.preventDefault();
const focusedElement = document.activeElement;
const elements = Array.from(dropdown.value.children).filter(
(el) => el.tabIndex === 0
);
const currentIndex = elements.indexOf(focusedElement);
const nextIndex = currentIndex > 0 ? currentIndex - 1 : elements.length - 1;
elements[nextIndex].focus();
}
if (event.key === "ArrowDown") {
event.preventDefault();
const focusedElement = document.activeElement;
const elements = Array.from(dropdown.value.children).filter(
(el) => el.tabIndex === 0
);
const currentIndex = elements.indexOf(focusedElement);
const nextIndex = currentIndex < elements.length - 1 ? currentIndex + 1 : 0;
elements[nextIndex].focus();
}
if (event.key === "Escape" || event.key === "Esc") {
event.preventDefault();
dropdownButton.value.focus();
dropdownOpen.value = false;
}
}
</script>
<template>
<div class="vl-dropdown-button">
<button :id="`menubutton-${hex}`" :aria-controls="`menu-${hex}`" class="vl-button"
@click.exact="dropdownOpen = !dropdownOpen;" @keydown="dropdownButtonKeypress($event)" ref="dropdownButton"
aria-haspopup="menu" :aria-expanded="dropdownOpen" :data-state="(dropdownOpen) ? 'open' : 'closed'">
Dropdown
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
</button>
<Transition name="vl-dropdown">
<div class="vl-dropdown" :id="`menu-${hex}`" tabindex="-1" :aria-labelledby="`menubutton-${hex}`"
:data-state="(dropdownOpen) ? 'open' : 'closed'" :key="dropdownOpen" :hidden="!dropdownOpen" role="menu"
@keydown="dropdownKeypress($event)" :aria-hidden="!dropdownOpen" ref="dropdown">
<span class="vl-diamond"><svg style="display:block" width="10" height="5" viewBox="0 0 30 10"
preserveAspectRatio="none">
<polygon points="0,0 30,0 15,10" style=""></polygon>
</svg></span>
<slot />
</div>
</Transition>
</div>
</template>
<style>
.vl-dropdown-button .vl-button[data-state="open"] svg {
transform: rotate(180deg);
}
.vl-dropdown-button .vl-button svg {
transition: transform 300ms ease;
}
.vl-dropdown-button {
position: relative;
}
.vl-diamond {
position: absolute;
top: 0px;
transform-origin: center 0px 0px;
transform: rotate(180deg) translateX(50%);
left: 50%;
}
.vl-diamond svg {
fill: #070809;
}
.vl-dropdown[data-state="open"] {
display: flex;
}
.vl-dropdown {
position: absolute;
padding-top: 4px;
padding-bottom: 4px;
padding-right: 4px;
padding-left: 4px;
margin-top: 8px;
left: -25%;
width: 150%;
background: #070809;
border-radius: 6px;
flex-direction: column;
gap: 2px 0;
}
.vl-dropdown button {
width: 100%;
background: unset;
border: unset;
justify-content: start;
border-radius: 6px;
}
.vl-dropdown button:hover {
background: #090a0b;
}
.vl-dropdown button:focus {
outline: 0;
background-color: hsl(226, 79%, 37.5%);
}
</style>

227
components/vlSlider.vue Normal file
View File

@@ -0,0 +1,227 @@
<script setup>
const props = defineProps({
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 1
},
step: {
type: Number,
default: 0.01
},
value: {
type: Number,
default: 0
},
id: {
type: String,
},
label: {
type: String,
}
})
const emit = defineEmits(['update:modelValue'])
const modelValue = ref(props.value);
watch(modelValue, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', props.value);
function roundValueToStep(number) {
const clampedNumber = Math.min(Math.max(number, props.min), props.max);
const quotient = Math.floor((clampedNumber - props.min) / props.step);
const lowerValue = props.min + quotient * props.step;
const upperValue = lowerValue + props.step;
if (clampedNumber - lowerValue < upperValue - clampedNumber) {
return lowerValue; // Round down
} else {
return upperValue; // Round up
}
}
const rangeSlider = ref(null);
const rangeThumb = ref(null);
const rangeValue = ref({
real: ref(roundValueToStep(props.value)),
stepped: ref(roundValueToStep(props.value))
});
const sliderPercentage = ref((rangeValue.value.real / props.max) * 100);
const steppedSliderPercentage = ref((rangeValue.value.stepped / props.max) * 100)
const steps = (props.max / props.step)
function snapToStep(percentage) {
const pixelsPerStep = rangeSlider.value.clientWidth / steps;
let step = roundValueToStep((((percentage / 100) * rangeSlider.value.clientWidth) / pixelsPerStep) * props.step);
const split = step.toString().split('.');
// fix float imprecisions by making sure the number only has at most 3 digits after the decimal point
if (split.length > 1) {
split[1] = split[1].slice(0,3);
step = parseFloat(split.join('.'));
}
rangeValue.value.stepped = step;
modelValue.value = step;
return (rangeValue.value.stepped / props.max) * 100;
}
function calculateSliderPosition(pointerEvent) {
if (!rangeSlider.value) return;
let pointerPosFromRangeStart = pointerEvent.clientX - rangeSlider.value.getClientRects()[0].x;
if (pointerPosFromRangeStart < 0) {
pointerPosFromRangeStart = 0;
}
if (pointerPosFromRangeStart > rangeSlider.value.clientWidth) {
pointerPosFromRangeStart = rangeSlider.value.clientWidth;
}
const value = (pointerPosFromRangeStart / rangeSlider.value.clientWidth);
const newSliderPercentage = value * 100;
if (newSliderPercentage === sliderPercentage.value) return;
updateSliderPosition(newSliderPercentage);
}
function updateSliderPosition(newSliderPercentage) {
sliderPercentage.value = newSliderPercentage;
steppedSliderPercentage.value = snapToStep(newSliderPercentage);
}
function pointerDown(pointerEvent) {
document.body.addEventListener('pointermove', calculateSliderPosition)
calculateSliderPosition(pointerEvent);
document.body.addEventListener('pointerup', () => {
rangeThumb.value.focus();
document.body.removeEventListener('pointermove', calculateSliderPosition)
}, { once: true })
}
function keydown(keyboardEvent) {
if (keyboardEvent.keyCode >= 37 && keyboardEvent.keyCode <= 40) {
keyboardEvent.preventDefault();
const direction = (keyboardEvent.key === "ArrowRight" || keyboardEvent.key === "ArrowUp") ? 'forward' : 'backward';
if (direction === 'forward') {
if (rangeValue.value.stepped + props.step > props.max) return;
rangeValue.value.stepped += props.step;
} else {
if (rangeValue.value.stepped - props.step < props.min) return;
rangeValue.value.stepped -= props.step;
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
if (keyboardEvent.keyCode === 36 || keyboardEvent.keyCode === 35) {
keyboardEvent.preventDefault();
if (keyboardEvent.key === "Home") {
rangeValue.value.stepped = props.min;
} else {
rangeValue.value.stepped = props.max;
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
if (keyboardEvent.keyCode === 33 || keyboardEvent.keyCode === 34) {
keyboardEvent.preventDefault();
const direction = (keyboardEvent.key === "PageUp") ? 'forward' : 'backward';
let multiplier = 2;
if (direction === 'forward') {
if (rangeValue.value.stepped + (props.step * multiplier) > props.max) multiplier = 1;
if (rangeValue.value.stepped + (props.step * multiplier) > props.max) return;
rangeValue.value.stepped += (props.step * multiplier);
} else {
if (rangeValue.value.stepped - (props.step * multiplier) > props.max) multiplier = 1;
if (rangeValue.value.stepped - (props.step * multiplier) > props.max) return;
rangeValue.value.stepped -= (props.step * multiplier);
}
return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
}
}
onMounted(() => {
if (props.id) {
document.body.querySelector(`[for="${props.id}"]`).addEventListener("click", () => {
rangeThumb.value.focus();
})
}
})
onUnmounted(() => {
if (props.id) {
document.body.querySelector(`[for="${props.id}"]`).removeEventListener("click", () => {
rangeThumb.value.focus();
})
}
})
</script>
<template>
<div class="vl-range-slider" ref="rangeSlider" @pointerdown="pointerDown($event)">
<span class="vl-range-track">
<span class="vl-range-track-filled" :style="'width: ' + steppedSliderPercentage + '%;'">
</span>
</span>
<span class="vl-range-thumb" :aria-label="props.label" :aria-labelledby="props.id" role="slider" ref="rangeThumb"
:aria-valuemin="props.min" :aria-valuemax="props.max" aria-orientation="horizontal" :aria-valuenow="rangeValue.stepped" @keydown="keydown($event)"
tabindex="0" :style="'left: ' + steppedSliderPercentage + '%;'"></span>
</div>
</template>
<style>
.vl-range-slider {
position: relative;
display: flex;
align-items: center;
width: 200px;
height: 20px;
margin: 4px 0;
}
.vl-range-track,
.vl-range-track-filled {
display: block;
height: 4px;
width: 100%;
background: #313234;
border-radius: 99999px;
}
.vl-range-track-filled {
background: hsl(226, 79%, 47.5%);
width: 0%;
}
.vl-range-thumb {
position: absolute;
display: block;
height: 20px;
width: 20px;
background: #f7f7f7;
border-radius: 99999px;
transform: translateX(-50%);
}
.vl-range-thumb:focus {
outline: 4px solid #b7b7b74a;
}
</style>

19
components/vlToggle.vue Normal file
View File

@@ -0,0 +1,19 @@
<script setup>
const emit = defineEmits(['update:modelValue'])
const checked = ref(false);
watch(checked, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', checked.value);
function toggleChecked() {
checked.value = !checked.value;
}
</script>
<template>
<button class="vl-toggle-button" @click="toggleChecked()" :aria-pressed="checked" :data-state="(checked) ? 'on' : 'off'">
<slot />
</button>
</template>

View File

@@ -0,0 +1,95 @@
<script setup>
const props = defineProps({
type: {
type: String,
default: "single"
},
defaultValue: {
type: String || Array,
}
})
const emit = defineEmits(['update:modelValue'])
const toggleGroup = ref(null);
const selected = ref((props.type === "single") ? props.defaultValue : [].concat(props.defaultValue));
watch(selected, (newValue) => {
emit('update:modelValue', newValue);
})
emit('update:modelValue', selected.value);
const items = ref([]);
function registerToggleItem(value) {
const item = { value, tabIndex: (props.defaultValue.includes(value)) ? ref('0') : ref('-1') }
const itemIndex = (items.value.push(item)) - 1
return { item, index: itemIndex };
}
function toggleItem(index) {
if (props.type === "single") {
selected.value = items.value[index].value;
return;
}
if (selected.value.indexOf(items.value[index].value) > -1) {
selected.value = selected.value.filter((e) => e !== items.value[index].value)
return;
}
selected.value.push(items.value[index].value);
}
function keydown(event) {
const elements = Array.from(toggleGroup.value.children).filter(
(el) => el.nodeName.toLowerCase() === 'button'
);
if (event.key === "Home") {
event.preventDefault();
items.value.forEach((el, i) => {
if (i === 0) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[0].focus();
}
if (event.key === "End") {
event.preventDefault();
items.value.forEach((el, i) => {
if (i === items.value.length - 1) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[items.value.length-1].focus();
}
const focusedElement = document.activeElement;
const currentIndex = elements.indexOf(focusedElement);
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
event.preventDefault();
const nextIndex = currentIndex < elements.length - 1 ? currentIndex + 1 : 0;
items.value.forEach((el, i) => {
if (i === nextIndex) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[nextIndex].focus();
}
if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
event.preventDefault();
const nextIndex = currentIndex > 0 ? currentIndex - 1 : elements.length - 1;
items.value.forEach((el, i) => {
if (i === nextIndex) return items.value[i].tabIndex = '0';
return items.value[i].tabIndex = '-1';
});
elements[nextIndex].focus();
}
}
provide('toggleGroup', { registerToggleItem, toggleItem, selected })
</script>
<template>
<div role="group" ref="toggleGroup" class="vl-toggle-group" @keydown="keydown($event)">
<slot />
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
const props = defineProps({
value: {
type: String,
default: ""
},
})
const toggleGroup = inject('toggleGroup');
const item = toggleGroup.registerToggleItem(props.value);
</script>
<template>
<button role="radio" type="button" @click="toggleGroup.toggleItem(item.index)" :aria-checked="toggleGroup.selected.value === props.value || toggleGroup.selected.value.indexOf(props.value) > -1" :tabindex="item.item.tabIndex.value" :data-state="(toggleGroup.selected.value === props.value || toggleGroup.selected.value.indexOf(props.value) > -1) ? 'checked' : 'unchecked'" class="vl-toggle-button">
<slot />
</button>
</template>

View File

@@ -0,0 +1,87 @@
<script setup>
const props = defineProps({
checked: {
type: Boolean,
default: false,
},
id: {
type: String
},
label: {
type: String
},
disabled: {
type: Boolean
}
})
const emit = defineEmits(['update:modelValue'])
const active = ref(props.checked);
watch(() => props.checked, (newValue) => {
active.value = newValue;
})
watch(active, (newValue) => {
emit('update:modelValue', newValue);
});
emit('update:modelValue', props.checked)
onMounted(() => {
document.querySelector(`[for='${props.id}']`).addEventListener('click', () => {
if (props.disabled) return;
active.value = !active.value
});
})
</script>
<template>
<button role="switch" class="vl-toggle-switch" :aria-disabled="(props.disabled === true) ? 'true' : 'false'"
:aria-label="label" :aria-labelledby="id" :tabindex="(disabled) ? '-1' : '0'"
@click="(!disabled) ? active = !active : ''" :aria-checked="active" :data-state="(active) ? 'checked' : 'unchecked'">
<div></div>
</button>
</template>
<style scoped> .vl-toggle-switch {
font-size: inherit;
border: 0;
cursor: pointer;
width: 2.5em;
height: 1.4em;
background: #1a1b1c;
border-radius: 100px;
padding: 0.125rem 0.25rem;
position: relative;
transition: background-color 0.3s ease;
}
.vl-toggle-switch[aria-disabled="true"] div {
background: #919191;
}
.vl-toggle-switch div {
position: relative;
left: 0;
width: 1em;
height: 1em;
background: #f7f7f7;
border-radius: 90px;
pointer-events: none;
transition: all 0.3s;
}
.vl-toggle-switch[data-state="checked"] {
background: hsl(226, 79%, 47.5%);
}
.vl-toggle-switch[data-state="checked"] div {
left: 100%;
transform: translateX(-100%);
}
.vl-toggle-switch:active div {
width: 1.3em;
}
</style>