Small button refactor, overflow and popout menus

This commit is contained in:
Prospector
2023-10-11 08:04:50 -07:00
parent c70ebb6cce
commit a708cf7f69
13 changed files with 556 additions and 58 deletions

View File

@@ -32,9 +32,15 @@ const props = defineProps({
type: Boolean,
default: false,
},
transparent: {
type: Boolean,
default: false,
},
})
const accentedButton = computed(() => ['danger', 'primary'].includes(props.color))
const accentedButton = computed(() =>
['danger', 'primary', 'red', 'orange', 'green', 'blue', 'purple', 'gray'].includes(props.color)
)
</script>
<template>
@@ -42,14 +48,20 @@ const accentedButton = computed(() => ['danger', 'primary'].includes(props.color
v-if="link"
class="btn"
:class="{
'icon-only': props.iconOnly,
'btn-large': props.large,
'btn-raised': color === 'raised',
'icon-only': iconOnly,
'btn-large': large,
'btn-danger': color === 'danger',
'btn-primary': color === 'primary',
'btn-secondary': color === 'secondary',
'btn-highlight': color === 'highlight',
'btn-outline': props.outline,
'btn-red': color === 'red',
'btn-orange': color === 'orange',
'btn-green': color === 'green',
'btn-blue': color === 'blue',
'btn-purple': color === 'purple',
'btn-gray': color === 'gray',
'btn-transparent': transparent,
'btn-outline': outline,
'color-accent-contrast': accentedButton,
}"
:to="link"
@@ -63,14 +75,20 @@ const accentedButton = computed(() => ['danger', 'primary'].includes(props.color
v-else
class="btn"
:class="{
'icon-only': props.iconOnly,
'btn-large': props.large,
'btn-raised': color === 'raised',
'icon-only': iconOnly,
'btn-large': large,
'btn-danger': color === 'danger',
'btn-primary': color === 'primary',
'btn-secondary': color === 'secondary',
'btn-highlight': color === 'highlight',
'btn-outline': props.outline,
'btn-red': color === 'red',
'btn-orange': color === 'orange',
'btn-green': color === 'green',
'btn-blue': color === 'blue',
'btn-purple': color === 'purple',
'btn-gray': color === 'gray',
'btn-transparent': transparent,
'btn-outline': outline,
'color-accent-contrast': accentedButton,
}"
@click="action"

View File

@@ -0,0 +1,78 @@
<template>
<PopoutMenu
ref="dropdown"
v-bind="$attrs"
:disabled="disabled"
:position="position"
:direction="direction"
>
<slot></slot>
<template #menu>
<Button
v-for="option in options"
:key="`option-${option.id}`"
:color="option.color ? option.color : 'default'"
transparent
:action="
() => {
option.action()
close()
}
"
>
<template v-if="!$slots[option.id]">{{ option.id }}</template>
<slot :name="option.id"></slot>
</Button>
</template>
</PopoutMenu>
</template>
<script setup>
import { ref } from 'vue'
import PopoutMenu from '@/components/base/PopoutMenu.vue'
import Button from '@/components/base/Button.vue'
defineProps({
options: {
type: Array,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
position: {
type: String,
default: 'bottom',
},
direction: {
type: String,
default: 'left',
},
})
defineOptions({
inheritAttrs: false,
})
const dropdown = ref(null)
const close = () => {
console.log('closing!')
dropdown.value.hide()
}
</script>
<style lang="scss" scoped>
.btn {
white-space: nowrap;
width: 100%;
box-shadow: none;
--text-color: var(--color-base);
--background-color: transparent;
justify-content: flex-start;
&:not(:last-child) {
margin-bottom: var(--gap-xs);
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div ref="dropdown" class="popup-container" tabindex="0" :aria-expanded="dropdownVisible">
<button v-bind="$attrs" ref="dropdownButton" @click="toggleDropdown">
<slot></slot>
</button>
<div
class="popup-menu"
:class="`position-${position}-${direction} ${dropdownVisible ? 'visible' : ''}`"
>
<slot name="menu"> </slot>
</div>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({
disabled: {
type: Boolean,
default: false,
},
position: {
type: String,
default: 'bottom',
},
direction: {
type: String,
default: 'left',
},
})
defineOptions({
inheritAttrs: false,
})
const dropdownVisible = ref(false)
const dropdown = ref(null)
const dropdownButton = ref(null)
const toggleDropdown = () => {
if (!props.disabled) {
dropdownVisible.value = !dropdownVisible.value
if (!dropdownVisible.value) {
dropdownButton.value.focus()
}
}
}
const hide = () => {
dropdownVisible.value = false
dropdownButton.value.focus()
}
const show = () => {
dropdownVisible.value = true
}
defineExpose({
show,
hide,
})
const handleClickOutside = (event) => {
const elements = document.elementsFromPoint(event.clientX, event.clientY)
if (
dropdown.value.$el !== event.target &&
!elements.includes(dropdown.value.$el) &&
!dropdown.value.contains(event.target)
) {
dropdownVisible.value = false
}
}
onMounted(() => {
window.addEventListener('click', handleClickOutside)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutside)
})
</script>
<style lang="scss" scoped>
.popup-container {
position: relative;
.popup-menu {
--_animation-offset: -1rem;
position: absolute;
scale: 0.75;
border: 1px solid var(--color-button-bg);
padding: var(--gap-md);
width: fit-content;
border-radius: var(--radius-md);
background-color: var(--color-raised-bg);
box-shadow: var(--shadow-floating);
z-index: 10;
opacity: 0;
pointer-events: none;
transition: bottom 0.125s ease-in-out, top 0.125s ease-in-out, left 0.125s ease-in-out,
right 0.125s ease-in-out, opacity 0.125s ease-in-out, scale 0.125s ease-in-out;
&.position-bottom-left {
top: calc(100% + var(--gap-sm) - 1rem);
right: -1rem;
}
&.position-bottom-right {
top: calc(100% + var(--gap-sm) - 1rem);
left: -1rem;
}
&.position-top-left {
bottom: calc(100% + var(--gap-sm) - 1rem);
right: -1rem;
}
&.position-top-right {
bottom: calc(100% + var(--gap-sm) - 1rem);
left: -1rem;
}
&.position-left-up {
bottom: -1rem;
right: calc(100% + var(--gap-sm) - 1rem);
}
&.position-left-down {
top: -1rem;
right: calc(100% + var(--gap-sm) - 1rem);
}
&.position-right-up {
bottom: -1rem;
left: calc(100% + var(--gap-sm) - 1rem);
}
&.position-right-down {
top: -1rem;
left: calc(100% + var(--gap-sm) - 1rem);
}
&.visible,
&:focus-within {
opacity: 1;
pointer-events: unset;
scale: 1;
&.position-bottom-left {
top: calc(100% + var(--gap-sm));
right: 0;
}
&.position-bottom-right {
top: calc(100% + var(--gap-sm));
left: 0;
}
&.position-top-left {
bottom: calc(100% + var(--gap-sm));
right: 0;
}
&.position-top-right {
bottom: calc(100% + var(--gap-sm));
left: 0;
}
&.position-left-up {
bottom: 0rem;
right: calc(100% + var(--gap-sm));
}
&.position-left-down {
top: 0rem;
right: calc(100% + var(--gap-sm));
}
&.position-right-up {
bottom: 0rem;
left: calc(100% + var(--gap-sm));
}
&.position-right-down {
top: 0rem;
left: calc(100% + var(--gap-sm));
}
}
.btn {
white-space: nowrap;
}
}
}
</style>

View File

@@ -36,12 +36,16 @@ export { default as NavItem } from './nav/NavItem.vue'
export { default as NavRow } from './nav/NavRow.vue'
export { default as NavStack } from './nav/NavStack.vue'
export { default as PopoutMenu } from './base/PopoutMenu.vue'
export { default as OverflowMenu } from './base/OverflowMenu.vue'
export { default as AlignLeftIcon } from '@/assets/icons/align-left.svg'
export { default as ArchiveIcon } from '@/assets/icons/archive.svg'
export { default as AsteriskIcon } from '@/assets/icons/asterisk.svg'
export { default as BellIcon } from '@/assets/icons/bell.svg'
export { default as BellRingIcon } from '@/assets/icons/bell-ring.svg'
export { default as BookIcon } from '@/assets/icons/book.svg'
export { default as BookmarkIcon } from '@/assets/icons/bookmark.svg'
export { default as BoxIcon } from '@/assets/icons/box.svg'
export { default as CalendarIcon } from '@/assets/icons/calendar.svg'
export { default as ChartIcon } from '@/assets/icons/chart.svg'
@@ -94,6 +98,8 @@ export { default as LockIcon } from '@/assets/icons/lock.svg'
export { default as LogInIcon } from '@/assets/icons/log-in.svg'
export { default as LogOutIcon } from '@/assets/icons/log-out.svg'
export { default as MoonIcon } from '@/assets/icons/moon.svg'
export { default as MoreHorizontalIcon } from '@/assets/icons/more-horizontal.svg'
export { default as MoreVerticalIcon } from '@/assets/icons/more-vertical.svg'
export { default as OmorphiaIcon } from '@/assets/icons/omorphia.svg'
export { default as PaintBrushIcon } from '@/assets/icons/paintbrush.svg'
export { default as PlayIcon } from '@/assets/icons/play.svg'