Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 10 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 51 additions & 25 deletions docs/content/2.elements/5.accordion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- `disabled` - Determines whether the item is disabled or not.
- `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
#default
Expand Down Expand Up @@ -51,14 +52,14 @@ You can also pass any prop from the [Button](/elements/button) component directl
---
baseProps:
items:
- label: "1. What is NuxtLabs UI?"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "2. Getting Started"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "3. Theming"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "4. Components"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: '1. What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '3. Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '4. Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
color: 'primary'
variant: 'soft'
Expand Down Expand Up @@ -90,14 +91,14 @@ You can also set them to `null` to hide the icons.
---
baseProps:
items:
- label: "1. What is NuxtLabs UI?"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "2. Getting Started"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "3. Theming"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: "4. Components"
content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
- label: '1. What is NuxtLabs UI?'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '2. Getting Started'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '3. Theming'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
- label: '4. Components'
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit'
props:
openIcon: 'i-heroicons-plus'
closeIcon: 'i-heroicons-minus'
Expand All @@ -107,26 +108,51 @@ excludedProps:
---
::

### Multiple

Use the `multiple` prop to to allow multiple elements to be opened at the same time.

::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:
multiple: true
excludedProps:
- defaultOpen
---
::

### Open

Use the `default-open` prop to open all items by default.
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"
- 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
---
::

Expand Down
38 changes: 31 additions & 7 deletions src/runtime/components/elements/Accordion.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div :class="ui.wrapper">
<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">
<UButton v-bind="{ ...omit(ui.default, ['openIcon', 'closeIcon']), ...$attrs, ...omit(item, ['slot', 'disabled', 'content', 'defaultOpen']) }" class="w-full">
<template #trailing>
Expand All @@ -18,6 +18,8 @@
</slot>
</HDisclosureButton>

<StateEmitter :open="open" @open="closeOthers(index)" />

<Transition
v-bind="ui.transition"
@enter="onEnter"
Expand Down Expand Up @@ -45,6 +47,7 @@ import { defu } from 'defu'
import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue'
import UButton from '../elements/Button.vue'
import StateEmitter from '../../utils/StateEmitter'
import type { Button } from '../../types/button'
import { useAppConfig } from '#imports'
// TODO: Remove
Expand All @@ -57,12 +60,13 @@ export default defineComponent({
HDisclosureButton,
HDisclosurePanel,
UIcon,
UButton
UButton,
StateEmitter
},
inheritAttrs: false,
props: {
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: () => []
},
defaultOpen: {
Expand All @@ -77,6 +81,10 @@ export default defineComponent({
type: String,
default: () => appConfig.ui.accordion.default.closeIcon
},
multiple: {
type: Boolean,
default: false
},
ui: {
type: Object as PropType<Partial<typeof appConfig.ui.accordion>>,
default: () => appConfig.ui.accordion
Expand All @@ -90,6 +98,20 @@ export default defineComponent({

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) {
el.style.height = '0'
el.offsetHeight // Trigger a reflow, flushing the CSS changes
Expand All @@ -108,20 +130,22 @@ export default defineComponent({
}

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 {
// eslint-disable-next-line vue/no-dupe-keys
ui,
uiButton,
buttonRefs,
closeOthers,
omit,
onEnter,
onBeforeLeave,
onAfterEnter,
onLeave,
omit
onLeave
}
}
})
Expand Down
22 changes: 22 additions & 0 deletions src/runtime/utils/StateEmitter.ts
Original file line number Diff line number Diff line change
@@ -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 () => {}
}
})