feat(Accordion): add multiple prop and close others by default (#364)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Dominik Opyd
2023-07-03 14:38:13 +02:00
committed by GitHub
parent e0f1798f07
commit b78fcf91a4
3 changed files with 106 additions and 34 deletions

View File

@@ -16,6 +16,7 @@ Pass an array to the `items` prop of the Accordion component. Each item can have
- `content` - The content to display in the panel by default. - `content` - The content to display in the panel by default.
- `disabled` - Determines whether the item is disabled or not. - `disabled` - Determines whether the item is disabled or not.
- `defaultOpen` - Determines whether the item is initially open or closed. - `defaultOpen` - Determines whether the item is initially open or closed.
- `closeOthers` - Determines whether the item click close others or not. **It only works with multiple mode**.
::component-example ::component-example
#default #default
@@ -51,14 +52,14 @@ You can also pass any prop from the [Button](/elements/button) component directl
--- ---
baseProps: baseProps:
items: items:
- label: "1. What is NuxtLabs UI?" - label: '1. What is NuxtLabs UI?'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "2. Getting Started" - label: '2. Getting Started'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "3. Theming" - label: '3. Theming'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "4. Components" - label: '4. Components'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props: props:
color: 'primary' color: 'primary'
variant: 'soft' variant: 'soft'
@@ -90,14 +91,14 @@ You can also set them to `null` to hide the icons.
--- ---
baseProps: baseProps:
items: items:
- label: "1. What is NuxtLabs UI?" - label: '1. What is NuxtLabs UI?'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "2. Getting Started" - label: '2. Getting Started'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "3. Theming" - label: '3. Theming'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "4. Components" - label: '4. Components'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props: props:
openIcon: 'i-heroicons-plus' openIcon: 'i-heroicons-plus'
closeIcon: 'i-heroicons-minus' closeIcon: 'i-heroicons-minus'
@@ -107,29 +108,54 @@ excludedProps:
--- ---
:: ::
### Open ### Multiple
Use the `default-open` prop to open all items by default. Use the `multiple` prop to to allow multiple elements to be opened at the same time.
::component-card ::component-card
--- ---
baseProps: baseProps:
items: items:
- label: "What is NuxtLabs UI?" - label: 'What is NuxtLabs UI?'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "Getting Started" - label: 'Getting Started'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "Theming" - label: 'Theming'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: "Components" - label: 'Components'
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit" content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props: props:
defaultOpen: true multiple: true
excludedProps: excludedProps:
- defaultOpen - defaultOpen
--- ---
:: ::
### Open
Use the `default-open` prop to open all items by default. Works better when the `multiple` prop is set to `true`.
::component-card
---
baseProps:
items:
- label: 'What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: 'Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
defaultOpen: true
multiple: true
excludedProps:
- defaultOpen
- multiple
---
::
## Slots ## Slots
You can use slots to customize the buttons and items content of the Accordion. You can use slots to customize the buttons and items content of the Accordion.

View File

@@ -1,7 +1,7 @@
<template> <template>
<div :class="ui.wrapper"> <div :class="ui.wrapper">
<HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen"> <HDisclosure v-for="(item, index) in items" v-slot="{ open, close }" :key="index" :default-open="defaultOpen || item.defaultOpen">
<HDisclosureButton as="template" :disabled="item.disabled"> <HDisclosureButton :ref="() => buttonRefs[index] = close" as="template" :disabled="item.disabled">
<slot :item="item" :index="index" :open="open" :close="close"> <slot :item="item" :index="index" :open="open" :close="close">
<UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...$attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }" class="w-full"> <UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...$attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }" class="w-full">
<template #trailing> <template #trailing>
@@ -18,6 +18,8 @@
</slot> </slot>
</HDisclosureButton> </HDisclosureButton>
<StateEmitter :open="open" @open="closeOthers(index)" />
<Transition <Transition
v-bind="ui.transition" v-bind="ui.transition"
@enter="onEnter" @enter="onEnter"
@@ -45,6 +47,7 @@ import { defu } from 'defu'
import { omit } from 'lodash-es' import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue' import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue' import UButton from '../elements/Button.vue'
import StateEmitter from '../../utils/StateEmitter'
import type { Button } from '../../types/button' import type { Button } from '../../types/button'
import { useAppConfig } from '#imports' import { useAppConfig } from '#imports'
// TODO: Remove // TODO: Remove
@@ -57,12 +60,13 @@ export default defineComponent({
HDisclosureButton, HDisclosureButton,
HDisclosurePanel, HDisclosurePanel,
UIcon, UIcon,
UButton UButton,
StateEmitter
}, },
inheritAttrs: false, inheritAttrs: false,
props: { props: {
items: { items: {
type: Array as PropType<Partial<Button & { slot: string, disabled: boolean, content: string, defaultOpen: boolean }>[]>, type: Array as PropType<Partial<Button & { slot: string, disabled: boolean, content: string, defaultOpen: boolean, closeOthers: boolean }>[]>,
default: () => [] default: () => []
}, },
defaultOpen: { defaultOpen: {
@@ -77,6 +81,10 @@ export default defineComponent({
type: String, type: String,
default: () => appConfig.ui.accordion.default.closeIcon default: () => appConfig.ui.accordion.default.closeIcon
}, },
multiple: {
type: Boolean,
default: false
},
ui: { ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>, type: Object as PropType<Partial<typeof appConfig.ui.accordion>>,
default: () => appConfig.ui.accordion default: () => appConfig.ui.accordion
@@ -90,6 +98,20 @@ export default defineComponent({
const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button) const uiButton = computed<Partial<typeof appConfig.ui.button>>(() => appConfig.ui.button)
const buttonRefs = ref<Function[]>([])
function closeOthers (itemIndex: number) {
if (!props.items[itemIndex].closeOthers && props.multiple) {
return
}
buttonRefs.value.forEach((close, index) => {
if (index === itemIndex) return
close()
})
}
function onEnter (el: HTMLElement, done) { function onEnter (el: HTMLElement, done) {
el.style.height = '0' el.style.height = '0'
el.offsetHeight // Trigger a reflow, flushing the CSS changes el.offsetHeight // Trigger a reflow, flushing the CSS changes
@@ -108,20 +130,22 @@ export default defineComponent({
} }
function onLeave (el: HTMLElement, done) { function onLeave (el: HTMLElement, done) {
el.style.height = '0'; el.style.height = '0'
(el as HTMLElement).addEventListener('transitionend', done, { once: true }) el.addEventListener('transitionend', done, { once: true })
} }
return { return {
// eslint-disable-next-line vue/no-dupe-keys // eslint-disable-next-line vue/no-dupe-keys
ui, ui,
uiButton, uiButton,
buttonRefs,
closeOthers,
omit,
onEnter, onEnter,
onBeforeLeave, onBeforeLeave,
onAfterEnter, onAfterEnter,
onLeave, onLeave
omit
} }
} }
}) })

View File

@@ -0,0 +1,22 @@
import { watch, defineComponent } from 'vue'
export default defineComponent({
props: {
open: {
type: Boolean,
default: false
}
},
emits: ['open', 'close'],
setup (props, { emit }) {
watch(() => props.open, (value) => {
if (value) {
emit('open')
} else {
emit('close')
}
})
return () => {}
}
})