fix(primitives): eslint/tsconfig migration, asChild refactor, type fixes
- Migrate to eslint flat config + composite tsconfig. - Complete the asChild→as="template" refactor (remove asChild prop + :as-child bindings across components, matching Primitive's slot model). - Fix test type errors and source type-safety (useGraceArea hull/point math, FocusScope/util ref typing). Note: ~53 vue-tsc errors remain (HTML attr/event passthrough typing on transparent wrapper components + a couple of duplicate-export naming collisions) — not gated by CI (build/lint/test green); pending a component-attribute-typing design decision.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface AccordionContentProps extends PrimitiveProps {
|
||||
/** Keep content mounted even when closed. */
|
||||
forceMount?: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccordionContext, useAccordionItemContext } from './context';
|
||||
import { Presence } from '../presence';
|
||||
import { Primitive } from '../primitive';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
|
||||
const { as = 'div', forceMount = false } = defineProps<AccordionContentProps>();
|
||||
|
||||
const { forwardRef } = useForwardExpose();
|
||||
const ctx = useAccordionContext();
|
||||
const item = useAccordionItemContext();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Presence :present="forceMount || item.open.value">
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
role="region"
|
||||
:id="item.contentId.value"
|
||||
:aria-labelledby="item.triggerId.value"
|
||||
:data-state="item.open.value ? 'open' : 'closed'"
|
||||
:data-disabled="item.disabled.value ? '' : undefined"
|
||||
:data-orientation="ctx.orientation.value"
|
||||
:hidden="!item.open.value || undefined"
|
||||
>
|
||||
<slot :open="item.open.value" />
|
||||
</Primitive>
|
||||
</Presence>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface AccordionItemProps extends PrimitiveProps {
|
||||
/** Unique value for this item. */
|
||||
value: string;
|
||||
/** Disable this item. */
|
||||
disabled?: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provideAccordionItemContext, useAccordionContext } from './context';
|
||||
import { Primitive } from '../primitive';
|
||||
import { computed } from 'vue';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
import { useId } from '../config-provider';
|
||||
|
||||
const { value, disabled = false, as = 'div' } = defineProps<AccordionItemProps>();
|
||||
|
||||
const { forwardRef } = useForwardExpose();
|
||||
|
||||
const ctx = useAccordionContext();
|
||||
const isOpen = computed(() => ctx.isOpen(value));
|
||||
const isDisabled = computed(() => ctx.disabled.value || disabled);
|
||||
|
||||
const triggerId = useId(undefined, 'accordion-trigger');
|
||||
const contentId = useId(undefined, 'accordion-content');
|
||||
|
||||
provideAccordionItemContext({
|
||||
value,
|
||||
open: isOpen,
|
||||
disabled: isDisabled,
|
||||
triggerId,
|
||||
contentId,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:data-state="isOpen ? 'open' : 'closed'"
|
||||
:data-disabled="isDisabled ? '' : undefined"
|
||||
:data-orientation="ctx.orientation.value"
|
||||
>
|
||||
<slot :open="isOpen" />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
import type { RovingDirection } from '../utils/roving-focus';
|
||||
|
||||
export interface AccordionRootProps extends PrimitiveProps {
|
||||
/** Current open value(s) for controlled mode. */
|
||||
modelValue?: string | string[];
|
||||
|
||||
/** Initial value(s) for uncontrolled mode. */
|
||||
defaultValue?: string | string[];
|
||||
|
||||
/** 'single' allows one panel; 'multiple' allows many. @default 'single' */
|
||||
type?: 'single' | 'multiple';
|
||||
|
||||
/** Allow closing all panels in single mode. @default false */
|
||||
collapsible?: boolean;
|
||||
|
||||
/** Disable all items. */
|
||||
disabled?: boolean;
|
||||
|
||||
/** Orientation of the accordion. @default 'vertical' */
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
|
||||
/** Writing direction. @default 'ltr' */
|
||||
dir?: RovingDirection;
|
||||
|
||||
/** Wrap keyboard navigation. @default true */
|
||||
loop?: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef, toRef, watch } from 'vue';
|
||||
import { resolveNextIndex, rovingKeyToAction } from '../utils/roving-focus';
|
||||
import { Primitive } from '../primitive';
|
||||
import { provideAccordionContext } from './context';
|
||||
import { toArray } from '@robonen/stdlib';
|
||||
import { useCollectionProvider } from '../collection';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
|
||||
const {
|
||||
type = 'single',
|
||||
collapsible = false,
|
||||
disabled = false,
|
||||
orientation = 'vertical',
|
||||
dir = 'ltr',
|
||||
loop = true,
|
||||
modelValue,
|
||||
defaultValue,
|
||||
as = 'div',
|
||||
} = defineProps<AccordionRootProps>();
|
||||
|
||||
const { forwardRef } = useForwardExpose();
|
||||
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string | string[] | undefined] }>();
|
||||
|
||||
type RovingAction = NonNullable<ReturnType<typeof rovingKeyToAction>>;
|
||||
|
||||
const openSet = shallowRef<Set<string>>(
|
||||
new Set(toArray(modelValue ?? defaultValue)),
|
||||
);
|
||||
|
||||
function setEqualsArray(set: Set<string>, arr: string[]): boolean {
|
||||
if (arr.length !== set.size) return false;
|
||||
for (let i = 0; i < arr.length; i++) if (!set.has(arr[i]!)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
watch(() => modelValue, (v) => {
|
||||
if (v === undefined) return;
|
||||
const arr = toArray(v);
|
||||
if (setEqualsArray(openSet.value, arr)) return;
|
||||
openSet.value = new Set(arr);
|
||||
});
|
||||
|
||||
|
||||
function nextOpenSet(cur: Set<string>, value: string): Set<string> {
|
||||
const present = cur.has(value);
|
||||
|
||||
if (type === 'single') {
|
||||
if (!present) return new Set([value]);
|
||||
return collapsible ? new Set() : cur;
|
||||
}
|
||||
|
||||
const next = new Set(cur);
|
||||
if (present) next.delete(value);
|
||||
else next.add(value);
|
||||
return next;
|
||||
}
|
||||
|
||||
function toEmitValue(set: Set<string>): string | string[] | undefined {
|
||||
return type === 'single' ? set.values().next().value : [...set];
|
||||
}
|
||||
|
||||
function commit(next: Set<string>): void {
|
||||
openSet.value = next;
|
||||
emit('update:modelValue', toEmitValue(next));
|
||||
}
|
||||
|
||||
function isOpen(value: string): boolean {
|
||||
return openSet.value.has(value);
|
||||
}
|
||||
|
||||
function toggle(value: string): void {
|
||||
if (disabled) return;
|
||||
const cur = openSet.value;
|
||||
const next = nextOpenSet(cur, value);
|
||||
if (next !== cur) commit(next);
|
||||
}
|
||||
|
||||
const { getItems, CollectionSlot } = useCollectionProvider();
|
||||
const triggerElements = computed(() => getItems(true).map(i => i.ref));
|
||||
|
||||
function resolveFocusIndex(action: RovingAction, current: number, count: number): number {
|
||||
if (action.absolute === 'home') return 0;
|
||||
if (action.absolute === 'end') return count - 1;
|
||||
return resolveNextIndex(current === -1 ? 0 : current, action.delta, count, loop);
|
||||
}
|
||||
|
||||
function onTriggerKeyDown(event: KeyboardEvent, el: HTMLElement): void {
|
||||
const action = rovingKeyToAction(event, { orientation, dir, loop });
|
||||
if (!action) return;
|
||||
event.preventDefault();
|
||||
const enabled = triggerElements.value.filter(x => !x.hasAttribute('data-disabled'));
|
||||
if (enabled.length === 0) return;
|
||||
enabled[resolveFocusIndex(action, enabled.indexOf(el), enabled.length)]!.focus();
|
||||
}
|
||||
|
||||
provideAccordionContext({
|
||||
disabled: toRef(() => disabled),
|
||||
orientation: toRef(() => orientation),
|
||||
direction: toRef(() => dir),
|
||||
loop: toRef(() => loop),
|
||||
collapsible: toRef(() => collapsible),
|
||||
triggerElements,
|
||||
isOpen,
|
||||
toggle,
|
||||
onTriggerKeyDown,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollectionSlot>
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:data-orientation="orientation"
|
||||
:data-disabled="disabled ? '' : undefined"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</CollectionSlot>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface AccordionTriggerProps extends PrimitiveProps {
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccordionContext, useAccordionItemContext } from './context';
|
||||
import { Primitive } from '../primitive';
|
||||
import { useCollectionInjector } from '../collection';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
|
||||
const { as = 'button' } = defineProps<AccordionTriggerProps>();
|
||||
|
||||
const ctx = useAccordionContext();
|
||||
const item = useAccordionItemContext();
|
||||
const { forwardRef, currentElement } = useForwardExpose();
|
||||
const { CollectionItem } = useCollectionInjector();
|
||||
|
||||
function onClick(): void {
|
||||
if (item.disabled.value) return;
|
||||
ctx.toggle(item.value);
|
||||
}
|
||||
|
||||
function onKeyDown(event: KeyboardEvent): void {
|
||||
if (!currentElement.value) return;
|
||||
ctx.onTriggerKeyDown(event, currentElement.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollectionItem>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:ref="forwardRef"
|
||||
:type="as === 'button' ? 'button' : undefined"
|
||||
:id="item.triggerId.value"
|
||||
:aria-expanded="item.open.value"
|
||||
:aria-controls="item.contentId.value"
|
||||
:aria-disabled="item.disabled.value || undefined"
|
||||
:data-state="item.open.value ? 'open' : 'closed'"
|
||||
:data-disabled="item.disabled.value ? '' : undefined"
|
||||
:data-orientation="ctx.orientation.value"
|
||||
:disabled="item.disabled.value || undefined"
|
||||
@click="onClick"
|
||||
@keydown="onKeyDown"
|
||||
>
|
||||
<slot :open="item.open.value" />
|
||||
</Primitive>
|
||||
</CollectionItem>
|
||||
</template>
|
||||
@@ -0,0 +1,242 @@
|
||||
import { AccordionContent, AccordionItem, AccordionRoot, AccordionTrigger } from '../index';
|
||||
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
function createAccordion(rootProps: Record<string, unknown> = {}, itemCount = 3) {
|
||||
return mount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(AccordionRoot, { ...rootProps }, {
|
||||
default: () => Array.from({ length: itemCount }, (_, i) => {
|
||||
const val = String.fromCodePoint(97 + i); // 'a', 'b', 'c'
|
||||
return h(AccordionItem, { value: val, key: val, disabled: i === 2 ? true : undefined }, {
|
||||
default: () => [
|
||||
h(AccordionTrigger, null, { default: () => `Trigger ${val.toUpperCase()}` }),
|
||||
h(AccordionContent, null, { default: () => `Content ${val.toUpperCase()}` }),
|
||||
],
|
||||
});
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
{ attachTo: document.body },
|
||||
);
|
||||
}
|
||||
|
||||
function press(el: Element, key: string) {
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
describe('Accordion', () => {
|
||||
it('renders items with correct structure', () => {
|
||||
const w = createAccordion();
|
||||
const triggers = w.findAll('button');
|
||||
expect(triggers).toHaveLength(3);
|
||||
triggers.forEach((t) => {
|
||||
expect(t.attributes('aria-expanded')).toBeDefined();
|
||||
expect(t.attributes('aria-controls')).toBeDefined();
|
||||
});
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('all panels closed by default (single, non-collapsible)', () => {
|
||||
const w = createAccordion();
|
||||
const regions = w.findAll('[role="region"]');
|
||||
expect(regions).toHaveLength(0);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('defaultValue opens a panel', () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
const regions = w.findAll('[role="region"]');
|
||||
expect(regions).toHaveLength(1);
|
||||
expect(regions[0]!.text()).toBe('Content A');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('click toggles panel open/closed (single, collapsible)', async () => {
|
||||
const w = createAccordion({ collapsible: true });
|
||||
const triggers = w.findAll('button');
|
||||
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(1);
|
||||
expect(w.find('[role="region"]').text()).toBe('Content A');
|
||||
|
||||
// clicking again closes it (collapsible)
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(0);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('single mode: opening one closes previous', async () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
const triggers = w.findAll('button');
|
||||
|
||||
await triggers[1]!.trigger('click');
|
||||
await nextTick();
|
||||
const regions = w.findAll('[role="region"]');
|
||||
expect(regions).toHaveLength(1);
|
||||
expect(regions[0]!.text()).toBe('Content B');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('single mode: cannot close when not collapsible', async () => {
|
||||
const w = createAccordion({ defaultValue: 'a', collapsible: false });
|
||||
const triggers = w.findAll('button');
|
||||
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
// should stay open
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(1);
|
||||
expect(w.find('[role="region"]').text()).toBe('Content A');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('multiple mode: multiple panels open', async () => {
|
||||
const w = createAccordion({ type: 'multiple' });
|
||||
const triggers = w.findAll('button');
|
||||
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
await triggers[1]!.trigger('click');
|
||||
await nextTick();
|
||||
|
||||
const regions = w.findAll('[role="region"]');
|
||||
expect(regions).toHaveLength(2);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('multiple mode: toggle individual items', async () => {
|
||||
const w = createAccordion({ type: 'multiple', defaultValue: ['a', 'b'] });
|
||||
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(2);
|
||||
|
||||
const triggers = w.findAll('button');
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
// 'a' closed, 'b' still open
|
||||
const regions = w.findAll('[role="region"]');
|
||||
expect(regions).toHaveLength(1);
|
||||
expect(regions[0]!.text()).toBe('Content B');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('v-model works (single)', async () => {
|
||||
const value = ref<string | undefined>('a');
|
||||
const w = mount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(AccordionRoot, {
|
||||
modelValue: value.value,
|
||||
'onUpdate:modelValue': (v: string | string[] | undefined) => { value.value = v as string | undefined; },
|
||||
collapsible: true,
|
||||
}, {
|
||||
default: () => [
|
||||
h(AccordionItem, { value: 'a' }, {
|
||||
default: () => [
|
||||
h(AccordionTrigger, null, { default: () => 'A' }),
|
||||
h(AccordionContent, null, { default: () => 'PA' }),
|
||||
],
|
||||
}),
|
||||
h(AccordionItem, { value: 'b' }, {
|
||||
default: () => [
|
||||
h(AccordionTrigger, null, { default: () => 'B' }),
|
||||
h(AccordionContent, null, { default: () => 'PB' }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
}),
|
||||
{ attachTo: document.body },
|
||||
);
|
||||
|
||||
expect(w.find('[role="region"]').text()).toBe('PA');
|
||||
const triggers = w.findAll('button');
|
||||
await triggers[1]!.trigger('click');
|
||||
await nextTick();
|
||||
expect(value.value).toBe('b');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('keyboard navigation (vertical, ArrowDown/ArrowUp)', async () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
await nextTick();
|
||||
const triggers = w.findAll('button');
|
||||
const trigA = triggers[0]!.element as HTMLElement;
|
||||
trigA.focus();
|
||||
|
||||
press(trigA, 'ArrowDown');
|
||||
await nextTick();
|
||||
expect(document.activeElement).toBe(triggers[1]!.element);
|
||||
|
||||
press(triggers[1]!.element, 'ArrowUp');
|
||||
await nextTick();
|
||||
expect(document.activeElement).toBe(triggers[0]!.element);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('Home/End keys move focus', async () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
await nextTick();
|
||||
const triggers = w.findAll('button');
|
||||
const trigA = triggers[0]!.element as HTMLElement;
|
||||
trigA.focus();
|
||||
|
||||
press(trigA, 'End');
|
||||
await nextTick();
|
||||
// End goes to last enabled trigger (B, since C is disabled)
|
||||
expect(document.activeElement).toBe(triggers[1]!.element);
|
||||
|
||||
press(triggers[1]!.element, 'Home');
|
||||
await nextTick();
|
||||
expect(document.activeElement).toBe(triggers[0]!.element);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('disabled item cannot be toggled', async () => {
|
||||
const w = createAccordion({ type: 'multiple' });
|
||||
const triggers = w.findAll('button');
|
||||
await triggers[2]!.trigger('click');
|
||||
await nextTick();
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(0);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('disabled root blocks all interaction', async () => {
|
||||
const w = createAccordion({ disabled: true });
|
||||
const triggers = w.findAll('button');
|
||||
await triggers[0]!.trigger('click');
|
||||
await nextTick();
|
||||
expect(w.findAll('[role="region"]')).toHaveLength(0);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('data-state and aria-expanded reflect open state', async () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
const triggers = w.findAll('button');
|
||||
expect(triggers[0]!.attributes('aria-expanded')).toBe('true');
|
||||
expect(triggers[0]!.attributes('data-state')).toBe('open');
|
||||
expect(triggers[1]!.attributes('aria-expanded')).toBe('false');
|
||||
expect(triggers[1]!.attributes('data-state')).toBe('closed');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('content has role=region with aria-labelledby', () => {
|
||||
const w = createAccordion({ defaultValue: 'a' });
|
||||
const region = w.find('[role="region"]');
|
||||
expect(region.attributes('aria-labelledby')).toBeDefined();
|
||||
const trigger = w.findAll('button')[0]!;
|
||||
expect(region.attributes('aria-labelledby')).toBe(trigger.attributes('id'));
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('orientation reflects in data-orientation', () => {
|
||||
const w = createAccordion({ orientation: 'horizontal' });
|
||||
expect(w.find('[data-orientation="horizontal"]').exists()).toBe(true);
|
||||
w.unmount();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { useContextFactory } from '@robonen/vue';
|
||||
|
||||
export interface AccordionContext {
|
||||
disabled: Ref<boolean>;
|
||||
orientation: Ref<'horizontal' | 'vertical'>;
|
||||
direction: Ref<'ltr' | 'rtl'>;
|
||||
loop: Ref<boolean>;
|
||||
collapsible: Ref<boolean>;
|
||||
/** DOM-ordered trigger elements, sourced from the internal Collection. */
|
||||
triggerElements: ComputedRef<HTMLElement[]>;
|
||||
isOpen: (value: string) => boolean;
|
||||
toggle: (value: string) => void;
|
||||
onTriggerKeyDown: (event: KeyboardEvent, el: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export const {
|
||||
inject: useAccordionContext,
|
||||
provide: provideAccordionContext,
|
||||
} = useContextFactory<AccordionContext>('AccordionContext');
|
||||
|
||||
export interface AccordionItemContext {
|
||||
value: string;
|
||||
open: ComputedRef<boolean>;
|
||||
disabled: ComputedRef<boolean>;
|
||||
triggerId: ComputedRef<string>;
|
||||
contentId: ComputedRef<string>;
|
||||
}
|
||||
|
||||
export const {
|
||||
inject: useAccordionItemContext,
|
||||
provide: provideAccordionItemContext,
|
||||
} = useContextFactory<AccordionItemContext>('AccordionItemContext');
|
||||
@@ -0,0 +1,12 @@
|
||||
export { default as AccordionRoot } from './AccordionRoot.vue';
|
||||
export { default as AccordionItem } from './AccordionItem.vue';
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue';
|
||||
export { default as AccordionContent } from './AccordionContent.vue';
|
||||
|
||||
export { provideAccordionContext, useAccordionContext, provideAccordionItemContext, useAccordionItemContext } from './context';
|
||||
|
||||
export type { AccordionRootProps } from './AccordionRoot.vue';
|
||||
export type { AccordionItemProps } from './AccordionItem.vue';
|
||||
export type { AccordionTriggerProps } from './AccordionTrigger.vue';
|
||||
export type { AccordionContentProps } from './AccordionContent.vue';
|
||||
export type { AccordionContext, AccordionItemContext } from './context';
|
||||
Reference in New Issue
Block a user