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

@@ -27,6 +27,7 @@ function getComponents() {
{ text: 'Alert', link: '/guide/alert/alert.md' },
{ text: 'Button', link: '/guide/button/button.md' },
{ text: 'Button Group', link: '/guide/buttonGroup/buttonGroup.md' },
{ text: 'Dropdown', link: '/guide/dropdown/dropdown.md' },
{ text: 'Spinner', link: '/guide/spinner/spinner.md' },
{ text: 'Tabs', link: '/guide/tabs/tabs.md' },
]

View File

@@ -0,0 +1,40 @@
<script setup>
import DropdownPlacementExample from './examples/DropdownPlacementExample.vue';
</script>
# Dropdown
## Props - placement
```vue
<script setup>
import { Dropdown } from 'flowbite-vue'
</script>
<template>
<dropdown placement="bottom" text="Bottom">
Any content here
</dropdown>
<dropdown placement="top">
<template #trigger="{ toggle }">
<Button @click="toggle">
Top
<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>
</template>
<div class="p-2">
Padding content
</div>
</dropdown>
<dropdown placement="right" text="Right">
<Spinner size="6" class="m-4" />
</dropdown>
<dropdown placement="left" text="Left">
hello world
</dropdown>
</template>
```
<DropdownPlacementExample />

View File

@@ -0,0 +1,29 @@
<template>
<div class="vp-raw inline-flex align-center gap-2 flex-wrap">
<dropdown placement="bottom" text="Bottom">
Any content here
</dropdown>
<dropdown placement="top">
<template #trigger="{ toggle }">
<Button @click="toggle">
Top
<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>
</template>
<div class="p-2">
Padding content
</div>
</dropdown>
<dropdown placement="right" text="Right">
<Spinner size="6" class="m-4" />
</dropdown>
<dropdown placement="left" text="Left">
hello world
</dropdown>
</div>
</template>
<script setup>
import { Dropdown, Spinner, Button } from '../../../../src/index'
</script>

35
package-lock.json generated
View File

@@ -323,8 +323,7 @@
"@types/web-bluetooth": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz",
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A==",
"dev": true
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A=="
},
"@typescript-eslint/eslint-plugin": {
"version": "5.30.0",
@@ -596,28 +595,25 @@
"dev": true
},
"@vueuse/core": {
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.7.5.tgz",
"integrity": "sha512-tqgzeZGoZcXzoit4kOGLWJibDMLp0vdm6ZO41SSUQhkhtrPhAg6dbIEPiahhUu6sZAmSYvVrZgEr5aKD51nrLA==",
"dev": true,
"version": "8.9.1",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.1.tgz",
"integrity": "sha512-a7goYb/gJxjXRBw4Fr/jEACiN33ghwM1ohJVu+Zwsr3lNL4qCQ1nU+ogta98lNg5hXJxWj7mYEmQDjjyWOu5nA==",
"requires": {
"@types/web-bluetooth": "^0.0.14",
"@vueuse/metadata": "8.7.5",
"@vueuse/shared": "8.7.5",
"@vueuse/metadata": "8.9.1",
"@vueuse/shared": "8.9.1",
"vue-demi": "*"
}
},
"@vueuse/metadata": {
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.7.5.tgz",
"integrity": "sha512-emJZKRQSaEnVqmlu39NpNp8iaW+bPC2kWykWoWOZMSlO/0QVEmO/rt8A5VhOEJTKLX3vwTevqbiRy9WJRwVOQg==",
"dev": true
"version": "8.9.1",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.1.tgz",
"integrity": "sha512-6LADOlyl3oENHa9dsoY7LXjU1Mh14DnpM6ztETI3hpm5ZffOMIG5CB2Q6aEZfIvYr1lkJVmG2L82wFKk7VRfIA=="
},
"@vueuse/shared": {
"version": "8.7.5",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.7.5.tgz",
"integrity": "sha512-THXPvMBFmg6Gf6AwRn/EdTh2mhqwjGsB2Yfp374LNQSQVKRHtnJ0I42bsZTn7nuEliBxqUrGQm/lN6qUHmhJLw==",
"dev": true,
"version": "8.9.1",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.1.tgz",
"integrity": "sha512-klZfn7ijI3juqVgpfQVrrlBh4uTFajwSCWm8Cdt45Kg26b1LZ9jn9n7J6GhmkFay5016GnjjivQoekQSMeJNUg==",
"requires": {
"vue-demi": "*"
}
@@ -2886,10 +2882,9 @@
}
},
"vue-demi": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.1.tgz",
"integrity": "sha512-xmkJ56koG3ptpLnpgmIzk9/4nFf4CqduSJbUM0OdPoU87NwRuZ6x49OLhjSa/fC15fV+5CbEnrxU4oyE022svg==",
"dev": true
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.2.tgz",
"integrity": "sha512-41ukrclEbMddAyP7PvxMSYqnOSzPV6r7GNnyTSKSCNTaz19GehxmTiXyP9kwHSUv2+Dr6hHqiUiF7L1VAw2KdQ=="
},
"vue-eslint-parser": {
"version": "9.0.3",

View File

@@ -54,5 +54,8 @@
"vitest": "^0.16.0",
"vue-eslint-parser": "^9.0.3",
"vue-tsc": "^0.38.2"
},
"dependencies": {
"@vueuse/core": "^8.9.1"
}
}

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,
}
}

View File

@@ -4,4 +4,5 @@ export { default as ButtonGroup } from './components/ButtonGroup/ButtonGroup.vue
export { default as Alert } from './components/Alert/Alert.vue'
export { default as Tabs } from './components/Tabs/Tabs.vue'
export { default as Tab } from './components/Tabs/components/Tab/Tab.vue'
export { default as Dropdown } from './components/Dropdown/Dropdown.vue'
export { default as FlowbiteThemable } from './components/utils/FlowbiteThemable/FlowbiteThemable.vue'