chore: update components

This commit is contained in:
Benjamin Canac
2021-11-18 18:53:07 +01:00
parent a5a78f9b94
commit 4605b8da32
4 changed files with 126 additions and 310 deletions

View File

@@ -1,179 +1,60 @@
<template>
<div
ref="container"
v-on-clickaway="close"
:class="typeof wrapperClass === 'string' ? wrapperClass : 'relative inline-block text-left'"
@keydown.escape="open = false"
@mouseover="mode === 'hover' ? mouseover() : () => {}"
@mouseleave="mode === 'hover' ? mouseleave() : () => {}"
>
<div class="flex items-center">
<slot name="trigger" :toggle="toggle" :open="open">
<TwButton
:variant="variant"
:size="size"
:label="label"
:icon="icon"
:rounded="rounded"
:square="square"
:loading="loading"
:disabled="disabled"
:custom-class="customClass"
:icon-class="iconClass"
trailing
@click.native="toggle"
>
<slot />
</TwButton>
</slot>
</div>
<Menu v-slot="{ open }" as="div" :class="wrapperClass">
<MenuButton ref="trigger" as="div">
<slot :open="open" />
</MenuButton>
<transition
appear
enter-class="transform scale-95 opacity-0"
enter-active-class="transition duration-100 ease-out"
enter-to-class="transform scale-100 opacity-100"
leave-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-to-class="transform scale-95 opacity-0"
>
<div
v-show="open && (links.length || $slots.header || $scopedSlots.header)"
ref="tooltip"
:class="[
dropdownClass,
mode === 'hover' && 'pt-2'
]"
class="z-30 rounded-md shadow-lg"
<div ref="container" :class="containerClass">
<transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0"
enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-out"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0"
>
<div
class="overflow-y-auto bg-white divide-y divide-gray-100 rounded-md ring-1 ring-gray-200 dark:ring-gray-700 dark:divide-gray-700"
:class="[
dropdownMenuClass,
darken ? 'dark:bg-gray-900' : 'dark:bg-gray-800'
]"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<slot name="header" />
<div v-if="links.length" class="divide-y divide-gray-100 dark:divide-gray-700">
<div v-for="(items, index) of links" :key="index" class="py-1">
<div
v-for="(link, i) of items"
:key="i"
role="menuitem"
@click="!link.disabled ? click(link) : (() => {})()"
>
<component
:is="(link.to && 'NuxtLink') || (link.href && 'a') || 'div'"
:to="link.to"
:href="link.href"
:target="link.target"
:class="[
link.click || (link.to || link.href) ? 'cursor-pointer hover:text-tw-gray-900 focus:text-tw-gray-900 hover:bg-tw-gray-100 focus:bg-tw-gray-100' : '',
link.active ? 'bg-tw-gray-100' : '',
!link.disabled ? 'text-tw-gray-600' : 'text-tw-gray-400 cursor-not-allowed',
size === 'md' ? 'px-4 py-2' : 'px-3 py-1.5',
customLinkClass,
link.customClass
]"
class="flex items-center justify-between space-x-3 text-sm group focus:outline-none"
>
<slot name="link" :link="link">
<div class="flex items-center flex-1 truncate" :class="{ 'flex-row-reverse justify-between': link.reverse, 'gap-3': size === 'md', 'gap-2': size !== 'md' }">
<Icon
v-if="link.icon"
:name="link.icon"
:class="[
!link.disabled ? 'group-hover:text-tw-gray-500 group-focus:text-tw-gray-500' : '',
size === 'md' ? 'h-5 w-5' : 'h-4 w-4',
link.customIconClass
]"
class="flex-shrink-0 text-tw-gray-400"
/>
<TwAvatar
v-if="link.avatar"
:src="link.avatar"
:alt="link.label"
:gradient="link.gradient"
size="xxs"
/>
<span class="truncate">{{ link.label }}</span>
</div>
<MenuItems :class="itemsClass">
<div v-for="(subItems, index) of items" :key="index" class="py-1">
<MenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled }">
<Component v-bind="item" :is="(item.to && 'NuxtLink') || (item.href && 'a') || 'button'" :class="resolveItemClass({ active, disabled })" @click="onItemClick(item)">
<slot :name="item.slot" :item="item">
<Icon v-if="item.icon" :name="item.icon" :class="itemIconClass" />
<div
v-if="link.shortcuts && link.shortcuts.length && $mq !== 'xs'"
class="flex items-center flex-shrink-0 ml-1 space-x-1 text-xs font-bold text-tw-gray-400"
>
<div v-for="(shortcut, j) of link.shortcuts" :key="j">
<span>
{{ shortcut }}
</span>
</div>
</div>
<span
v-if="link.unread"
class="flex-shrink-0 block w-2 h-2 p-1 mr-1 rounded-full opacity-75 bg-primary-600 dark:bg-white animate-pulse"
/>
</slot>
<slot v-if="link.slot" :name="link.slot" />
</component>
</div>
</div>
{{ item.label }}
</slot>
</Component>
</MenuItem>
</div>
</div>
</div>
</transition>
</div>
</MenuItems>
</transition>
</div>
</Menu>
</template>
<script>
import { createPopper } from '@popperjs/core'
import { directive as onClickaway } from 'vue-clickaway'
import {
Menu,
MenuButton,
MenuItems,
MenuItem
} from '@headlessui/vue'
import Icon from '../elements/Icon'
import { classNames, usePopper } from '../../utils'
export default {
components: {
Menu,
MenuButton,
MenuItems,
MenuItem,
Icon
},
directives: {
onClickaway
},
props: {
label: {
type: String,
default: ''
},
icon: {
type: String,
default: 'solid/chevron-down'
},
links: {
items: {
type: Array,
default: () => []
},
variant: {
type: String,
default: 'white'
},
size: {
type: String,
default: 'md',
validator (value) {
return ['', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'].includes(value)
}
},
mode: {
type: String,
default: 'click',
validator (value) {
return ['click', 'hover'].includes(value)
}
},
placement: {
type: String,
default: 'bottom-end'
@@ -184,146 +65,93 @@ export default {
},
wrapperClass: {
type: String,
default: null
default: 'relative inline-block text-left'
},
iconClass: {
containerClass: {
type: String,
default: null
default: 'w-48 z-20'
},
customClass: {
itemsClass: {
type: String,
default: null
default: 'bg-white divide-y divide-gray-100 dark:divide-gray-700 rounded-md ring-1 ring-black ring-opacity-5'
},
customLinkClass: {
itemClass: {
type: String,
default: null
default: 'group flex items-center px-4 py-2 text-sm w-full'
},
rounded: {
type: Boolean,
default: false
},
square: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
darken: {
type: Boolean,
default: false
},
dropdownClass: {
itemActiveClass: {
type: String,
default: 'w-48'
default: 'bg-tw-gray-100 text-tw-gray-900'
},
dropdownMenuClass: {
itemInactiveClass: {
type: String,
default: 'max-h-56'
default: 'text-tw-gray-700'
},
openDelay: {
type: Number,
default: 100
itemDisabledClass: {
type: String,
default: 'cursor-not-allowed opacity-50'
},
closeDelay: {
type: Number,
default: 0
},
preventOverflow: {
type: Number,
default: 8
itemIconClass: {
type: String,
default: 'mr-3 h-5 w-5 text-tw-gray-400 group-hover:text-tw-gray-500'
}
// openDelay: {
// type: Number,
// default: 100
// },
// closeDelay: {
// type: Number,
// default: 0
// },
},
data () {
return {
open: false,
instance: null,
openTimeout: null,
closeTimeout: null
setup (props) {
const [trigger, container] = usePopper({
placement: props.placement,
strategy: props.strategy,
modifiers: [{
name: 'offset',
options: {
offset: [0, 8]
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: 8
}
}]
})
function resolveItemClass ({ active, disabled }) {
return classNames(
props.itemClass,
active ? props.itemActiveClass : props.itemInactiveClass,
disabled && props.itemDisabledClass
)
}
},
watch: {
disabled (value) {
if (value && open) { this.close() }
},
open (value) {
if (!value) {
function onItemClick (item) {
if (item.disabled) {
return
}
if (this.instance) {
this.instance.destroy()
this.instance = null
if (item.click) {
item.click()
}
}
this.instance = createPopper(this.$refs.container, this.$refs.tooltip, {
strategy: this.strategy,
placement: this.placement,
modifiers: [
{
name: 'offset',
options: {
offset: this.mode === 'click' ? [0, 8] : 0
}
},
{
name: 'computeStyles',
options: {
gpuAcceleration: false,
adaptive: false
}
},
{
name: 'preventOverflow',
options: {
padding: this.preventOverflow
}
}
]
})
}
},
beforeDestroy () {
if (this.instance) {
this.instance.destroy()
this.instance = null
}
},
methods: {
click (link) {
if (link.click) {
link.click()
}
if (link.click || link.to) {
this.toggle()
}
},
mouseover () {
clearTimeout(this.closeTimeout)
this.closeTimeout = null
this.openTimeout = this.openTimeout || setTimeout(() => {
this.open = true
this.openTimeout = null
}, this.openDelay)
},
mouseleave () {
clearTimeout(this.openTimeout)
this.openTimeout = null
this.closeTimeout = this.closeTimeout || setTimeout(() => {
this.close()
this.closeTimeout = null
}, this.closeDelay)
},
toggle () {
this.open = !this.open
},
close () {
this.open = false
return {
trigger,
container,
onItemClick,
resolveItemClass
}
}
}

View File

@@ -12,15 +12,12 @@
</template>
<script>
import Vue from 'vue'
const RouterLink = Vue.component('RouterLink')
import { RouterLink } from 'vue-router'
export default {
name: 'Link',
props: {
// @ts-ignore
...RouterLink.options.props,
...RouterLink.props,
inactiveClass: {
type: String,
default: ''

View File

@@ -13,7 +13,6 @@
<input
:id="name"
ref="input"
v-focus="autofocus"
:name="name"
:value="modelValue"
:type="type"
@@ -43,7 +42,7 @@
</template>
<script>
import Focus from '../../directives/focus'
// import Focus from '../../directives/focus'
import Icon from '../elements/Icon'
@@ -51,9 +50,9 @@ export default {
components: {
Icon
},
directives: {
focus: Focus
},
// directives: {
// focus: Focus
// },
props: {
modelValue: {
type: [String, Number],

View File

@@ -1,8 +1,8 @@
<template>
<component
:is="$listeners && $listeners.submit ? 'form': 'div'"
:class="[padded && rounded && 'rounded-md', !padded && rounded && 'sm:rounded-md', wrapperClass, ringClass, backgroundClass]"
@submit.prevent="$listeners && $listeners.submit ? $listeners.submit() : null"
:is="$attrs && $attrs.submit ? 'form': 'div'"
:class="[padded && rounded && 'rounded-md', !padded && rounded && 'sm:rounded-md', wrapperClass, ringClass, shadowClass, backgroundClass]"
@submit.prevent="$attrs && $attrs.submit ? $attrs.submit() : null"
>
<div
v-if="$slots.header"
@@ -29,10 +29,22 @@ export default {
type: Boolean,
default: false
},
rounded: {
type: Boolean,
default: true
},
wrapperClass: {
type: String,
default: 'overflow-hidden'
},
backgroundClass: {
type: String,
default: 'bg-tw-white'
},
shadowClass: {
type: String,
default: ''
},
ringClass: {
type: String,
default: 'ring-1 ring-gray-200 dark:ring-gray-700'
@@ -64,26 +76,6 @@ export default {
borderColorClass: {
type: String,
default: 'border-gray-200 dark:border-gray-700'
},
rounded: {
type: Boolean,
default: true
},
variant: {
type: String,
default: 'gray',
validator (value) {
return ['gray', 'white', 'black'].includes(value)
}
}
},
computed: {
backgroundClass () {
return ({
white: 'bg-white dark:bg-gray-800',
gray: 'bg-tw-gray-50',
black: 'bg-white dark:bg-black'
})[this.variant]
}
}
}