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,28 @@
|
||||
<script lang="ts">
|
||||
import type { PopperAnchorProps } from '../popper';
|
||||
|
||||
export interface PopoverAnchorProps extends PopperAnchorProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, onUnmounted } from 'vue';
|
||||
import { PopperAnchor } from '../popper';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const props = defineProps<PopoverAnchorProps>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
|
||||
onBeforeMount(() => {
|
||||
ctx.hasCustomAnchor.value = true;
|
||||
});
|
||||
onUnmounted(() => {
|
||||
ctx.hasCustomAnchor.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopperAnchor v-bind="props">
|
||||
<slot />
|
||||
</PopperAnchor>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { PopperArrowProps } from '../popper';
|
||||
|
||||
export interface PopoverArrowProps extends PopperArrowProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PopperArrow } from '../popper';
|
||||
|
||||
const { width = 10, height = 5 } = defineProps<PopoverArrowProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopperArrow :width="width" :height="height">
|
||||
<slot />
|
||||
</PopperArrow>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface PopoverCloseProps extends PrimitiveProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Primitive } from '../primitive';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const { as = 'button' } = defineProps<PopoverCloseProps>();
|
||||
const { forwardRef } = useForwardExpose();
|
||||
const ctx = usePopoverContext();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:type="as === 'button' ? 'button' : undefined"
|
||||
@click="ctx.onOpenChange(false)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import type { PopoverContentImplEmits, PopoverContentImplProps } from './PopoverContentImpl.vue';
|
||||
|
||||
export interface PopoverContentProps extends PopoverContentImplProps {
|
||||
/** Keep mounted for CSS exit animations. */
|
||||
forceMount?: boolean;
|
||||
}
|
||||
|
||||
export type PopoverContentEmits = PopoverContentImplEmits;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PopoverContentModal from './PopoverContentModal.vue';
|
||||
import PopoverContentNonModal from './PopoverContentNonModal.vue';
|
||||
import { Presence } from '../presence';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const { forceMount = false, ...contentProps } = defineProps<PopoverContentProps>();
|
||||
const emit = defineEmits<PopoverContentEmits>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Presence :present="ctx.open.value" :force-mount="forceMount">
|
||||
<PopoverContentModal
|
||||
v-if="ctx.modal.value"
|
||||
v-bind="contentProps"
|
||||
@open-auto-focus="emit('openAutoFocus', $event)"
|
||||
@close-auto-focus="emit('closeAutoFocus', $event)"
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
||||
@focus-outside="emit('focusOutside', $event)"
|
||||
@interact-outside="emit('interactOutside', $event)"
|
||||
@dismiss="emit('dismiss')"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContentModal>
|
||||
<PopoverContentNonModal
|
||||
v-else
|
||||
v-bind="contentProps"
|
||||
@open-auto-focus="emit('openAutoFocus', $event)"
|
||||
@close-auto-focus="emit('closeAutoFocus', $event)"
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
||||
@focus-outside="emit('focusOutside', $event)"
|
||||
@interact-outside="emit('interactOutside', $event)"
|
||||
@dismiss="emit('dismiss')"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContentNonModal>
|
||||
</Presence>
|
||||
</template>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import type { DismissableLayerEmits } from '../dismissable-layer';
|
||||
import type { FocusScopeEmits } from '../focus-scope';
|
||||
import type { PopperContentProps } from '../popper';
|
||||
|
||||
export interface PopoverContentImplProps extends PopperContentProps {
|
||||
/** Trap focus inside the content (modal popovers). */
|
||||
trapFocus?: boolean;
|
||||
/** Block outside pointer events (modal popovers). */
|
||||
disableOutsidePointerEvents?: boolean;
|
||||
}
|
||||
|
||||
export interface PopoverContentImplEmits {
|
||||
openAutoFocus: FocusScopeEmits['mountAutoFocus'];
|
||||
closeAutoFocus: FocusScopeEmits['unmountAutoFocus'];
|
||||
escapeKeyDown: DismissableLayerEmits['escapeKeyDown'];
|
||||
pointerDownOutside: DismissableLayerEmits['pointerDownOutside'];
|
||||
focusOutside: DismissableLayerEmits['focusOutside'];
|
||||
interactOutside: DismissableLayerEmits['interactOutside'];
|
||||
dismiss: [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DismissableLayer } from '../dismissable-layer';
|
||||
import { FocusScope } from '../focus-scope';
|
||||
import { PopperContent } from '../popper';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const {
|
||||
trapFocus = false,
|
||||
disableOutsidePointerEvents = false,
|
||||
as = 'div',
|
||||
...popperProps
|
||||
} = defineProps<PopoverContentImplProps>();
|
||||
|
||||
const emit = defineEmits<PopoverContentImplEmits>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FocusScope
|
||||
as="template"
|
||||
:loop="true"
|
||||
:trapped="trapFocus"
|
||||
@mount-auto-focus="emit('openAutoFocus', $event)"
|
||||
@unmount-auto-focus="emit('closeAutoFocus', $event)"
|
||||
>
|
||||
<DismissableLayer
|
||||
as="template"
|
||||
:disable-outside-pointer-events="disableOutsidePointerEvents"
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
||||
@focus-outside="emit('focusOutside', $event)"
|
||||
@interact-outside="emit('interactOutside', $event)"
|
||||
@dismiss="ctx.onOpenChange(false)"
|
||||
>
|
||||
<PopperContent
|
||||
:id="ctx.contentId.value"
|
||||
:as="as"
|
||||
v-bind="popperProps"
|
||||
:data-state="ctx.open.value ? 'open' : 'closed'"
|
||||
:aria-labelledby="ctx.triggerId.value"
|
||||
role="dialog"
|
||||
:style="{
|
||||
'--popover-content-transform-origin': 'var(--popper-transform-origin)',
|
||||
'--popover-content-available-width': 'var(--popper-available-width)',
|
||||
'--popover-content-available-height': 'var(--popper-available-height)',
|
||||
'--popover-trigger-width': 'var(--popper-anchor-width)',
|
||||
'--popover-trigger-height': 'var(--popper-anchor-height)',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</PopperContent>
|
||||
</DismissableLayer>
|
||||
</FocusScope>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentImplEmits, PopoverContentImplProps } from './PopoverContentImpl.vue';
|
||||
import PopoverContentImpl from './PopoverContentImpl.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useBodyScrollLock } from '@robonen/vue';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const props = defineProps<PopoverContentImplProps>();
|
||||
const emit = defineEmits<PopoverContentImplEmits>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
const isRightClickOutsideRef = ref(false);
|
||||
|
||||
useBodyScrollLock();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverContentImpl
|
||||
v-bind="props"
|
||||
:trap-focus="ctx.open.value"
|
||||
disable-outside-pointer-events
|
||||
@close-auto-focus.prevent="(event: Event) => {
|
||||
emit('closeAutoFocus', event);
|
||||
if (!isRightClickOutsideRef) ctx.triggerElement.value?.focus();
|
||||
}"
|
||||
@pointer-down-outside="(event: PointerEvent | MouseEvent) => {
|
||||
emit('pointerDownOutside', event);
|
||||
const ctrlLeftClick = event.button === 0 && event.ctrlKey === true;
|
||||
isRightClickOutsideRef = event.button === 2 || ctrlLeftClick;
|
||||
}"
|
||||
@focus-outside.prevent
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@interact-outside="emit('interactOutside', $event)"
|
||||
@dismiss="emit('dismiss')"
|
||||
@open-auto-focus="emit('openAutoFocus', $event)"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContentImpl>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { PopoverContentImplEmits, PopoverContentImplProps } from './PopoverContentImpl.vue';
|
||||
import PopoverContentImpl from './PopoverContentImpl.vue';
|
||||
import { ref } from 'vue';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const props = defineProps<PopoverContentImplProps>();
|
||||
const emit = defineEmits<PopoverContentImplEmits>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
const hasInteractedOutsideRef = ref(false);
|
||||
const hasPointerDownOutsideRef = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopoverContentImpl
|
||||
v-bind="props"
|
||||
:trap-focus="false"
|
||||
:disable-outside-pointer-events="false"
|
||||
@close-auto-focus="(event: Event) => {
|
||||
emit('closeAutoFocus', event);
|
||||
if (!event.defaultPrevented) {
|
||||
if (!hasInteractedOutsideRef) ctx.triggerElement.value?.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
hasInteractedOutsideRef = false;
|
||||
hasPointerDownOutsideRef = false;
|
||||
}"
|
||||
@interact-outside="(event: PointerEvent | MouseEvent | FocusEvent) => {
|
||||
emit('interactOutside', event);
|
||||
if (!event.defaultPrevented) {
|
||||
hasInteractedOutsideRef = true;
|
||||
if (event.type === 'pointerdown') hasPointerDownOutsideRef = true;
|
||||
}
|
||||
const target = event.target as HTMLElement;
|
||||
if (ctx.triggerElement.value?.contains(target)) event.preventDefault();
|
||||
if (event.type === 'focusin' && hasPointerDownOutsideRef) event.preventDefault();
|
||||
}"
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@pointer-down-outside="emit('pointerDownOutside', $event)"
|
||||
@focus-outside="emit('focusOutside', $event)"
|
||||
@dismiss="emit('dismiss')"
|
||||
@open-auto-focus="emit('openAutoFocus', $event)"
|
||||
>
|
||||
<slot />
|
||||
</PopoverContentImpl>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { TeleportPrimitiveProps } from '../teleport';
|
||||
|
||||
export interface PopoverPortalProps extends TeleportPrimitiveProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PortalPrimitive from '../teleport/Teleport.vue';
|
||||
|
||||
const props = defineProps<PopoverPortalProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PortalPrimitive v-bind="props">
|
||||
<slot />
|
||||
</PortalPrimitive>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
export interface PopoverRootProps {
|
||||
/** Uncontrolled initial open state. Ignored once `v-model:open` is bound. */
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* Modal mode traps focus, locks scroll, and disables outside pointer events.
|
||||
* @default false
|
||||
*/
|
||||
modal?: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRef } from 'vue';
|
||||
import { PopperRoot } from '../popper';
|
||||
import { providePopoverContext } from './context';
|
||||
import { useId } from '../config-provider';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const { defaultOpen = false, modal = false } = defineProps<PopoverRootProps>();
|
||||
|
||||
const localOpen = ref<boolean>(defaultOpen);
|
||||
|
||||
const open = defineModel<boolean>('open', {
|
||||
default: undefined,
|
||||
get: v => v ?? localOpen.value,
|
||||
set: (v) => {
|
||||
localOpen.value = v;
|
||||
return v;
|
||||
},
|
||||
});
|
||||
|
||||
const triggerId = useId(undefined, 'popover-trigger');
|
||||
const contentId = useId(undefined, 'popover-content');
|
||||
const triggerElement = ref<HTMLElement>();
|
||||
const hasCustomAnchor = ref(false);
|
||||
|
||||
providePopoverContext({
|
||||
open,
|
||||
// Identity passthrough via `toRef` — reactive without `computed`'s effect/cache.
|
||||
modal: toRef(() => modal),
|
||||
triggerId,
|
||||
contentId,
|
||||
triggerElement,
|
||||
hasCustomAnchor,
|
||||
onOpenChange: (value) => { open.value = value; },
|
||||
onOpenToggle: () => { open.value = !open.value; },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PopperRoot>
|
||||
<slot :open="open" :close="() => open = false" />
|
||||
</PopperRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface PopoverTriggerProps extends PrimitiveProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PopperAnchor } from '../popper';
|
||||
import { Primitive } from '../primitive';
|
||||
import { onMounted } from 'vue';
|
||||
import { useForwardExpose } from '@robonen/vue';
|
||||
import { usePopoverContext } from './context';
|
||||
|
||||
const { as = 'button' } = defineProps<PopoverTriggerProps>();
|
||||
|
||||
const ctx = usePopoverContext();
|
||||
const { forwardRef, currentElement: triggerElement } = useForwardExpose();
|
||||
|
||||
onMounted(() => {
|
||||
ctx.triggerElement.value = triggerElement.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="ctx.hasCustomAnchor.value ? Primitive : PopperAnchor" as="template">
|
||||
<Primitive
|
||||
:id="ctx.triggerId.value"
|
||||
:ref="forwardRef"
|
||||
:as="as"
|
||||
:type="as === 'button' ? 'button' : undefined"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="ctx.open.value"
|
||||
:aria-controls="ctx.contentId.value"
|
||||
:data-state="ctx.open.value ? 'open' : 'closed'"
|
||||
@click="ctx.onOpenToggle"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</component>
|
||||
</template>
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
PopoverClose,
|
||||
PopoverContent,
|
||||
PopoverRoot,
|
||||
PopoverTrigger,
|
||||
} from '../index';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||
import type { VueWrapper } from '@vue/test-utils';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { userEvent } from 'vitest/browser';
|
||||
|
||||
const wrappers: Array<VueWrapper<any>> = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (wrappers.length) wrappers.pop()!.unmount();
|
||||
document.body.innerHTML = '';
|
||||
document.body.removeAttribute('style');
|
||||
});
|
||||
|
||||
function track<T extends VueWrapper<any>>(w: T): T {
|
||||
wrappers.push(w);
|
||||
return w;
|
||||
}
|
||||
|
||||
function mountPopover(options: {
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
modal?: boolean;
|
||||
onUpdateOpen?: (v: boolean) => void;
|
||||
} = {}) {
|
||||
const Wrapper = defineComponent({
|
||||
setup() {
|
||||
return () => h(
|
||||
PopoverRoot,
|
||||
{
|
||||
open: options.open,
|
||||
defaultOpen: options.defaultOpen,
|
||||
modal: options.modal,
|
||||
'onUpdate:open': options.onUpdateOpen,
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(PopoverTrigger, null, { default: () => 'Toggle' }),
|
||||
h(PopoverContent, { forceMount: true }, {
|
||||
default: () => [
|
||||
h('p', 'Popover body'),
|
||||
h(PopoverClose, null, { default: () => 'Close' }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return track(mount(Wrapper, { attachTo: document.body }));
|
||||
}
|
||||
|
||||
describe('Popover', () => {
|
||||
it('renders trigger', () => {
|
||||
const wrapper = mountPopover();
|
||||
const trigger = wrapper.find('button');
|
||||
expect(trigger.text()).toBe('Toggle');
|
||||
expect(trigger.attributes('aria-haspopup')).toBe('dialog');
|
||||
expect(trigger.attributes('data-state')).toBe('closed');
|
||||
});
|
||||
|
||||
it('opens on trigger click', async () => {
|
||||
const wrapper = mountPopover();
|
||||
const trigger = wrapper.find('button');
|
||||
|
||||
await trigger.trigger('click');
|
||||
await nextTick();
|
||||
|
||||
expect(trigger.attributes('aria-expanded')).toBe('true');
|
||||
expect(trigger.attributes('data-state')).toBe('open');
|
||||
});
|
||||
|
||||
it('toggles with v-model:open', async () => {
|
||||
const onUpdate = vi.fn();
|
||||
const wrapper = mountPopover({ onUpdateOpen: onUpdate });
|
||||
const trigger = wrapper.find('button');
|
||||
|
||||
await trigger.trigger('click');
|
||||
await nextTick();
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('opens with defaultOpen', async () => {
|
||||
const wrapper = mountPopover({ defaultOpen: true });
|
||||
await nextTick();
|
||||
|
||||
const trigger = wrapper.find('button');
|
||||
expect(trigger.attributes('data-state')).toBe('open');
|
||||
});
|
||||
|
||||
it('close button closes the popover', async () => {
|
||||
const onUpdate = vi.fn();
|
||||
const wrapper = mountPopover({ defaultOpen: true, onUpdateOpen: onUpdate });
|
||||
await nextTick();
|
||||
|
||||
const closeBtn = wrapper.findAll('button').find(b => b.text() === 'Close');
|
||||
expect(closeBtn).toBeDefined();
|
||||
|
||||
await closeBtn!.trigger('click');
|
||||
await nextTick();
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('content has role="dialog"', async () => {
|
||||
const wrapper = mountPopover({ defaultOpen: true });
|
||||
await nextTick();
|
||||
|
||||
const content = wrapper.find('[role="dialog"]');
|
||||
expect(content.exists()).toBe(true);
|
||||
expect(content.attributes('data-state')).toBe('open');
|
||||
});
|
||||
|
||||
it('closes on Escape key', async () => {
|
||||
const onUpdate = vi.fn();
|
||||
mountPopover({ defaultOpen: true, onUpdateOpen: onUpdate });
|
||||
await nextTick();
|
||||
|
||||
await userEvent.keyboard('{Escape}');
|
||||
await nextTick();
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('supports controlled open', async () => {
|
||||
const Wrapper = defineComponent({
|
||||
setup() {
|
||||
const open = ref(false);
|
||||
return () => h(
|
||||
PopoverRoot,
|
||||
{ open: open.value, 'onUpdate:open': (v: boolean) => { open.value = v; } },
|
||||
{
|
||||
default: () => [
|
||||
h(PopoverTrigger, null, { default: () => 'Toggle' }),
|
||||
h(PopoverContent, { forceMount: true }, { default: () => 'Body' }),
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = track(mount(Wrapper, { attachTo: document.body }));
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('button').attributes('data-state')).toBe('closed');
|
||||
|
||||
await wrapper.find('button').trigger('click');
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find('button').attributes('data-state')).toBe('open');
|
||||
});
|
||||
|
||||
it('trigger has aria-controls pointing to content id', async () => {
|
||||
const wrapper = mountPopover({ defaultOpen: true });
|
||||
await nextTick();
|
||||
|
||||
const trigger = wrapper.find('button');
|
||||
const contentId = wrapper.find('[role="dialog"]').attributes('id');
|
||||
expect(trigger.attributes('aria-controls')).toBe(contentId);
|
||||
});
|
||||
});
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,18 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import { useContextFactory } from '@robonen/vue';
|
||||
|
||||
export interface PopoverContext {
|
||||
open: Ref<boolean>;
|
||||
modal: Ref<boolean>;
|
||||
triggerId: ComputedRef<string>;
|
||||
contentId: ComputedRef<string>;
|
||||
triggerElement: Ref<HTMLElement | undefined>;
|
||||
hasCustomAnchor: Ref<boolean>;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
onOpenToggle: () => void;
|
||||
}
|
||||
|
||||
const ctx = useContextFactory<PopoverContext>('PopoverContext');
|
||||
|
||||
export const providePopoverContext = ctx.provide;
|
||||
export const usePopoverContext = ctx.inject;
|
||||
@@ -0,0 +1,18 @@
|
||||
export { default as PopoverRoot } from './PopoverRoot.vue';
|
||||
export { default as PopoverTrigger } from './PopoverTrigger.vue';
|
||||
export { default as PopoverAnchor } from './PopoverAnchor.vue';
|
||||
export { default as PopoverContent } from './PopoverContent.vue';
|
||||
export { default as PopoverPortal } from './PopoverPortal.vue';
|
||||
export { default as PopoverArrow } from './PopoverArrow.vue';
|
||||
export { default as PopoverClose } from './PopoverClose.vue';
|
||||
|
||||
export { usePopoverContext } from './context';
|
||||
|
||||
export type { PopoverContext } from './context';
|
||||
export type { PopoverRootProps } from './PopoverRoot.vue';
|
||||
export type { PopoverTriggerProps } from './PopoverTrigger.vue';
|
||||
export type { PopoverAnchorProps } from './PopoverAnchor.vue';
|
||||
export type { PopoverContentEmits, PopoverContentProps } from './PopoverContent.vue';
|
||||
export type { PopoverPortalProps } from './PopoverPortal.vue';
|
||||
export type { PopoverArrowProps } from './PopoverArrow.vue';
|
||||
export type { PopoverCloseProps } from './PopoverClose.vue';
|
||||
Reference in New Issue
Block a user