diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b52fef6..0dbea64 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -34,6 +34,7 @@ function buildSidebar() { function getComponents() { return [ + { text: 'Accordion', link: 'components/accordion/accordion.md' }, { text: 'Alert', link: '/components/alert/alert.md' }, { text: 'Avatar', link: 'components/avatar/avatar.md' }, { text: 'Breadcrumb', link: 'components/breadcrumb/breadcrumb.md' }, @@ -51,7 +52,6 @@ function getComponents() { { text: 'Modal', link: 'components/modal/modal.md' }, { text: 'Navbar', link: 'components/navbar/navbar.md' }, - { text: '- Accordion', link: 'components/accordion/accordion.md' }, { text: '- Carousel', link: 'components/carousel/carousel.md' }, { text: '- Footer', link: 'components/footer/footer.md' }, { text: '- Pagination', link: 'components/pagination/pagination.md' }, diff --git a/docs/components/accordion/accordion.md b/docs/components/accordion/accordion.md index 3ed9fa6..2e2209c 100644 --- a/docs/components/accordion/accordion.md +++ b/docs/components/accordion/accordion.md @@ -1,15 +1,186 @@ # Vue Accordion Component - Flowbite +#### Use Tailwind CSS accordion component to show expanding content +--- + +:::tip +Original reference: [https://flowbite.com/docs/components/accordion/](https://flowbite.com/docs/components/accordion/) +::: + +## Default accordion +Use this example to basic accordion. ```vue + + ``` + +## Always open accordion +Always open prop to makes accordion able to open multiple elements. +```vue + + + + +``` + + + +## Flush accordion +Flush prop removes side borders, and rounded corners +```vue + + + + +``` + + + +## Styling accordion +You can style accordion content and headers by passing tailwind classes into them. +```vue + + + + +``` + + diff --git a/docs/components/accordion/examples/AccordionAlwaysOpenExample.vue b/docs/components/accordion/examples/AccordionAlwaysOpenExample.vue new file mode 100644 index 0000000..10f8f2e --- /dev/null +++ b/docs/components/accordion/examples/AccordionAlwaysOpenExample.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/components/accordion/examples/AccordionExample.vue b/docs/components/accordion/examples/AccordionExample.vue index 4d214c1..96ce3d8 100644 --- a/docs/components/accordion/examples/AccordionExample.vue +++ b/docs/components/accordion/examples/AccordionExample.vue @@ -1,8 +1,35 @@ - + + diff --git a/docs/components/accordion/examples/AccordionFlushExample.vue b/docs/components/accordion/examples/AccordionFlushExample.vue new file mode 100644 index 0000000..363e44a --- /dev/null +++ b/docs/components/accordion/examples/AccordionFlushExample.vue @@ -0,0 +1,35 @@ + + + diff --git a/docs/components/accordion/examples/AccordionStyledHeadersExample.vue b/docs/components/accordion/examples/AccordionStyledHeadersExample.vue new file mode 100644 index 0000000..63f3474 --- /dev/null +++ b/docs/components/accordion/examples/AccordionStyledHeadersExample.vue @@ -0,0 +1,35 @@ + + + diff --git a/package-lock.json b/package-lock.json index 1098230..8832eaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "floating-vue": "^2.0.0-beta.20", "flowbite": "1.5.4", "lodash-es": "4.17.21", - "tailwindcss": "3.2.4" + "nanoid": "4.0.0", + "tailwindcss": "^3" }, "devDependencies": { "@types/lodash-es": "4.17.6", @@ -3502,14 +3503,14 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^14 || ^16 || >=18" } }, "node_modules/natural-compare": { @@ -3859,6 +3860,17 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/preact": { "version": "10.11.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", @@ -7901,9 +7913,9 @@ "dev": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==" }, "natural-compare": { "version": "1.4.0", @@ -8074,6 +8086,13 @@ "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" + }, + "dependencies": { + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + } } }, "postcss-import": { diff --git a/package.json b/package.json index 78f48ac..4f738b5 100644 --- a/package.json +++ b/package.json @@ -64,12 +64,13 @@ "vue-tsc": "0.30.0" }, "dependencies": { - "floating-vue": "^2.0.0-beta.20", "@vueuse/core": "9.3.0", "classnames": "2.3.2", + "floating-vue": "^2.0.0-beta.20", "flowbite": "1.5.4", "lodash-es": "4.17.21", - "tailwindcss": "3.2.4" + "nanoid": "4.0.0", + "tailwindcss": "^3" }, "engines": { "node": "14.x", diff --git a/src/components/Accordion/Accordion.vue b/src/components/Accordion/Accordion.vue index b6a2780..cdf0f17 100644 --- a/src/components/Accordion/Accordion.vue +++ b/src/components/Accordion/Accordion.vue @@ -1,71 +1,25 @@ diff --git a/src/components/Accordion/AccordionContent.vue b/src/components/Accordion/AccordionContent.vue new file mode 100644 index 0000000..b55e754 --- /dev/null +++ b/src/components/Accordion/AccordionContent.vue @@ -0,0 +1,13 @@ + + + diff --git a/src/components/Accordion/AccordionHeader.vue b/src/components/Accordion/AccordionHeader.vue new file mode 100644 index 0000000..60d4442 --- /dev/null +++ b/src/components/Accordion/AccordionHeader.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/Accordion/AccordionPanel.vue b/src/components/Accordion/AccordionPanel.vue new file mode 100644 index 0000000..8687627 --- /dev/null +++ b/src/components/Accordion/AccordionPanel.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/Accordion/composables/useAccordionContentClasses.ts b/src/components/Accordion/composables/useAccordionContentClasses.ts new file mode 100644 index 0000000..4ed7b56 --- /dev/null +++ b/src/components/Accordion/composables/useAccordionContentClasses.ts @@ -0,0 +1,29 @@ +import { computed, inject } from 'vue' +import { useAccordionState } from '@/components/Accordion/composables/useAccordionState' +import classNames from 'classnames' + + +const baseContentClasses = 'p-5 border border-gray-200 dark:border-gray-700 dark:bg-gray-900' +export function useAccordionContentClasses() { + const accordionId: string = inject('accordionId') ?? '' + const panelId: string = inject('panelId') ?? '' + + + const { accordionsStates } = useAccordionState() + + const accordionState = computed(() => accordionsStates[accordionId]) + const panelState = computed(() => accordionsStates[accordionId].panels[panelId]) + const panelsCount = computed(() => Object.keys(accordionsStates[accordionId].panels[panelId]).length) + + const contentClasses = computed(() => classNames(baseContentClasses, { + hidden: !panelState.value.isVisible, + 'border-b-0': panelState.value.order !== panelsCount.value - 1 || accordionState.value.flush, + 'border-t-0': panelState.value.order === panelsCount.value - 1, + 'border-x-0': accordionState.value.flush, + })) + return { + contentClasses, + } +} + + diff --git a/src/components/Accordion/composables/useAccordionHeaderClasses.ts b/src/components/Accordion/composables/useAccordionHeaderClasses.ts new file mode 100644 index 0000000..75e60f0 --- /dev/null +++ b/src/components/Accordion/composables/useAccordionHeaderClasses.ts @@ -0,0 +1,35 @@ +import { computed, inject } from 'vue' +import { useAccordionState } from '@/components/Accordion/composables/useAccordionState' +import classNames from 'classnames' + + +const baseHeaderClasses = 'flex items-center p-5 w-full font-medium text-left text-gray-500 border border-gray-200 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-800 dark:border-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800' +const baseArrowClasses = 'w-6 h-6 shrink-0' +export function useAccordionHeaderClasses() { + const accordionId: string = inject('accordionId') ?? '' + const panelId: string = inject('panelId') ?? '' + + const { accordionsStates } = useAccordionState() + const accordionState = computed(() => accordionsStates[accordionId]) + const panelState = computed(() => accordionState.value.panels[panelId]) + const panelsCount = computed(() => Object.keys(panelState.value).length) + const isPanelLast = computed(() => panelState.value.order !== panelsCount.value - 1) + const isBottomBorderVisibleForFlush = computed(() => + isPanelLast.value || + (accordionState.value.flush && panelState.value.order === panelsCount.value - 1 && !panelState.value.isVisible)) + + const headerClasses = computed(() => classNames(baseHeaderClasses, { + 'bg-gray-100 dark:bg-gray-800': panelState.value.isVisible, + 'rounded-t-xl': panelState.value.order === 0 && !accordionState.value.flush, + 'border-t-0': panelState.value.order === 0 && accordionState.value.flush, + 'border-b-0': isBottomBorderVisibleForFlush.value, + 'border-x-0': accordionState.value.flush, + })) + const arrowClasses = computed(() => classNames(baseArrowClasses,{ 'rotate-180': panelState.value.isVisible })) + return { + headerClasses, + arrowClasses, + } +} + + diff --git a/src/components/Accordion/composables/useAccordionState.ts b/src/components/Accordion/composables/useAccordionState.ts new file mode 100644 index 0000000..29f5e71 --- /dev/null +++ b/src/components/Accordion/composables/useAccordionState.ts @@ -0,0 +1,28 @@ +import { onBeforeMount, onBeforeUnmount, reactive } from 'vue' +import type { tState } from '@/components/Accordion/types' + +const accordionsStates = reactive({}) +export function useAccordionState(id?: string, options?: {flush: boolean, alwaysOpen: boolean}): { + accordionsStates: tState +} { + + onBeforeMount(() => { + if (!id) return + accordionsStates[id] = { + id: id, + flush: options?.flush ?? false, + alwaysOpen: options?.alwaysOpen ?? false, + panels: {}, + } + }) + onBeforeUnmount(() => { + if (!id) return + delete accordionsStates[id] + }) + + return { + accordionsStates, + } +} + + diff --git a/src/components/Accordion/types.ts b/src/components/Accordion/types.ts new file mode 100644 index 0000000..409f1e9 --- /dev/null +++ b/src/components/Accordion/types.ts @@ -0,0 +1,18 @@ +export type tAccordionMode = 'flush' | 'alwaysOpen' | 'default' +export type tAccordionPanel = { + order: number + id: string + isVisible: boolean +} +type tAccordionPanels = { + [key: string]: tAccordionPanel +} +type tStateElement = { + id: string, + flush: boolean, + alwaysOpen: boolean, + panels: tAccordionPanels +} +export type tState = { + [key: string]: tStateElement +} diff --git a/src/index.ts b/src/index.ts index 627797b..9de0f19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,7 @@ +export { default as Accordion } from './components/Accordion/Accordion.vue' +export { default as AccordionPanel } from './components/Accordion/AccordionPanel.vue' +export { default as AccordionHeader } from './components/Accordion/AccordionHeader.vue' +export { default as AccordionContent } from './components/Accordion/AccordionContent.vue' export { default as Button } from './components/Button/Button.vue' export { default as Spinner } from './components/Spinner/Spinner.vue' export { default as ButtonGroup } from './components/ButtonGroup/ButtonGroup.vue' @@ -12,7 +16,6 @@ export { default as BreadcrumbItem } from './components/Breadcrumb/BreadcrumbIte export { default as Avatar } from './components/Avatar/Avatar.vue' export { default as StackedAvatars } from './components/Avatar/StackedAvatars.vue' export { default as StackedAvatarsCounter } from './components/Avatar/StackedAvatarsCounter.vue' -export { default as Accordion } from './components/Accordion/Accordion.vue' export { default as Badge } from './components/Badge/Badge.vue' export { default as TheCard } from './components/Card/TheCard.vue' export { default as Carousel } from './components/Carousel/Carousel.vue'