feat: add dropdown

This commit is contained in:
Alexandr
2022-07-08 20:36:05 +03:00
parent 4fb027fcb4
commit a5ffeffb82
12 changed files with 228 additions and 29 deletions

View File

@@ -2,6 +2,7 @@ import type { Ref } from 'vue'
import { computed, useSlots } from 'vue'
import classNames from 'classnames'
import type { ButtonDuotoneGradient, ButtonGradient, ButtonMonochromeGradient, ButtonSize, ButtonVariant } from '../types'
import { useFlowbiteThemable } from '../../utils/FlowbiteThemable/composables/useFlowbiteThemable'
export type ButtonClassMap<T extends string> = { hover: Record<T, string>, default: Record<T, string> }
@@ -160,6 +161,8 @@ const alternativeColors = ['alternative', 'light']
export function useButtonClasses(props: UseButtonClassesProps): { wrapperClasses: Ref<string>, spanClasses: Ref<string> } {
const slots = useSlots()
const theme = useFlowbiteThemable()
const sizeClasses = computed(() => {
if (props.square.value) return buttonSquareSizeClasses[props.size.value]
return buttonSizeClasses[props.size.value]
@@ -170,6 +173,8 @@ export function useButtonClasses(props: UseButtonClassesProps): { wrapperClasses
const isColor = !!props.color.value
const isOutline = props.outline.value
const isActiveTheme = theme.isActive.value
let hoverClass = ''
let backgroundClass = ''
@@ -193,20 +198,24 @@ export function useButtonClasses(props: UseButtonClassesProps): { wrapperClasses
} else if (isColor && isOutline) { // COLOR AND OUTLINE
if (!alternativeColors.includes(props.color.value)) {
backgroundClass = buttonOutlineColorClasses.default[props.color.value as unknown as keyof typeof buttonOutlineColorClasses.default]
const color = isActiveTheme ? theme.color.value : props.color.value
backgroundClass = buttonOutlineColorClasses.default[color as unknown as keyof typeof buttonOutlineColorClasses.default]
if(!props.disabled.value)
hoverClass = buttonOutlineColorClasses.hover[props.color.value as unknown as keyof typeof buttonOutlineColorClasses.hover]
hoverClass = buttonOutlineColorClasses.hover[color as unknown as keyof typeof buttonOutlineColorClasses.hover]
} else {
console.warn(`cannot use outline prop with "${props.color.value}" color`) // TODO: prettify
}
} else { // JUST COLOR
backgroundClass = buttonColorClasses.default[props.color.value]
const color = isActiveTheme ? theme.color.value : props.color.value
backgroundClass = buttonColorClasses.default[color as unknown as keyof typeof buttonColorClasses.default]
if(!props.disabled.value)
hoverClass = buttonColorClasses.hover[props.color.value]
hoverClass = buttonColorClasses.hover[color as unknown as keyof typeof buttonColorClasses.hover]
}
let shadowClass = ''

View File

@@ -0,0 +1,54 @@
<template>
<div class="inline-flex relative" ref="wrapper">
<slot name="trigger" :show="onShow" :hide="onHide" :toggle="onToggle">
<Button @click="onToggle">
{{ text }}
<template #suffix>
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
</template>
</Button>
</slot>
<div ref="content" :style="contentStyles" :class="[{ hidden: !visible }, contentClasses]">
<slot />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, toRef } from 'vue'
import type { PropType } from 'vue'
import type { DropdownPlacement } from './types'
import { useDropdownClasses } from './composables/useDropdownClasses'
import Button from '../Button/Button.vue'
import { onClickOutside } from '@vueuse/core'
const visible = ref(false)
const onShow = () => visible.value = true
const onHide = () => visible.value = false
const onToggle = () => visible.value = !visible.value
const props = defineProps({
placement: {
type: String as PropType<DropdownPlacement>,
default: 'bottom',
},
text: {
type: String ,
default: '',
},
})
const content = ref<HTMLDivElement>()
const wrapper = ref<HTMLDivElement>()
const { contentClasses, contentStyles } = useDropdownClasses({
placement: toRef(props, 'placement'),
visible,
contentRef: content,
})
onClickOutside(wrapper, () => {
if(!visible.value) return
visible.value = false
})
</script>

View File

@@ -0,0 +1,65 @@
import { computed, nextTick, ref, watch } from 'vue'
import type { Ref } from 'vue'
import classNames from 'classnames'
import type { DropdownPlacement } from '../types'
const defaultDropdownClasses = 'absolute z-10 bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700'
const placementDropdownClasses: Record<DropdownPlacement, string> = {
bottom: '',
left: 'top-0',
right: 'top-0',
top: '',
}
export type UseDropdownClassesProps = {
placement: Ref<DropdownPlacement>
contentRef: Ref<HTMLDivElement | undefined>
visible: Ref<boolean>
}
const placementCalculators: Record<DropdownPlacement, (rect: DOMRect) => string> = {
bottom(rect: DOMRect): string {
return `bottom: -${rect.height}px;`
},
left(rect: DOMRect): string {
return `left: -${rect.width}px;`
},
right(rect: DOMRect): string {
return `right: -${rect.width}px;`
},
top(rect: DOMRect): string {
return `top: -${rect.height}px;`
},
}
export function useDropdownClasses(props: UseDropdownClassesProps): {
contentClasses: Ref<string>
contentStyles: Ref<string>
} {
watch(props.visible, (value: boolean) => {
if(value) nextTick(() => calculatePlacementClasses())
})
const placementStyles = ref('')
const calculatePlacementClasses = () => {
const boundingRect = props.contentRef.value?.getBoundingClientRect()
if(!boundingRect) return placementStyles.value = ''
placementStyles.value = placementCalculators[props.placement.value](boundingRect)
}
const contentClasses = computed(() => {
return classNames(
defaultDropdownClasses,
placementDropdownClasses[props.placement.value],
)
})
return {
contentClasses,
contentStyles: placementStyles,
}
}

1
src/components/Dropdown/types.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type DropdownPlacement = 'top' | 'bottom' | 'left' | 'right'

View File

@@ -1,7 +1,5 @@
<template>
<div>
<slot />
</div>
<slot />
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'

View File

@@ -1,4 +1,4 @@
import type { FlowbiteThemablePayload, FlowbiteTheme } from '../types'
import type { FlowbiteTheme } from '../types'
import type { Ref } from 'vue'
import { computed, inject } from 'vue'
import { FLOWBITE_THEMABLE_INJECTION_KEY } from '../injection/config'
@@ -10,6 +10,7 @@ type UseFlowbiteThemableReturns = {
disabledClasses: Ref<string>
borderClasses: Ref<string>
isActive: Ref<boolean>
color: Ref<FlowbiteTheme | undefined>
}
type FlowbiteThemeMap = { background: string, disabled: string, hover: string, text: string, border: string }
@@ -60,7 +61,8 @@ export function useFlowbiteThemable(): UseFlowbiteThemableReturns {
const theme = inject<Ref<FlowbiteTheme>>(FLOWBITE_THEMABLE_INJECTION_KEY)
const isActive = computed(() => !!theme)
const isActive = computed(() => !!theme?.value)
const color = computed(() => theme?.value)
const backgroundClasses = computed(() => {
if(!theme) return ''
@@ -94,5 +96,6 @@ export function useFlowbiteThemable(): UseFlowbiteThemableReturns {
textClasses,
borderClasses,
isActive,
color,
}
}