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,17 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface AlertDialogActionProps extends PrimitiveProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogClose } from '../dialog';
|
||||
|
||||
const { as = 'button' } = defineProps<AlertDialogActionProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose :as="as" data-alert-dialog-action>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { PrimitiveProps } from '../primitive';
|
||||
|
||||
export interface AlertDialogCancelProps extends PrimitiveProps {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogClose } from '../dialog';
|
||||
|
||||
const { as = 'button' } = defineProps<AlertDialogCancelProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose :as="as" data-alert-dialog-cancel>
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import type { DialogContentEmits, DialogContentProps } from '../dialog';
|
||||
|
||||
export interface AlertDialogContentProps extends Omit<DialogContentProps, 'role'> {}
|
||||
export type AlertDialogContentEmits = DialogContentEmits;
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogContent } from '../dialog';
|
||||
|
||||
const props = defineProps<AlertDialogContentProps>();
|
||||
const emit = defineEmits<AlertDialogContentEmits>();
|
||||
|
||||
function onOpenAutoFocus(event: Event) {
|
||||
emit('openAutoFocus', event);
|
||||
if (event.defaultPrevented) return;
|
||||
queueMicrotask(() => {
|
||||
const content = document.querySelector<HTMLElement>('[data-alert-dialog-content]');
|
||||
const cancel = content?.querySelector<HTMLElement>('[data-alert-dialog-cancel]');
|
||||
if (cancel) {
|
||||
event.preventDefault();
|
||||
cancel.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogContent
|
||||
v-bind="props"
|
||||
role="alertdialog"
|
||||
data-alert-dialog-content
|
||||
@open-auto-focus="onOpenAutoFocus"
|
||||
@close-auto-focus="emit('closeAutoFocus', $event)"
|
||||
@escape-key-down="emit('escapeKeyDown', $event)"
|
||||
@pointer-down-outside="(e: PointerEvent | MouseEvent) => { e.preventDefault(); emit('pointerDownOutside', e); }"
|
||||
@focus-outside="(e: FocusEvent) => { e.preventDefault(); emit('focusOutside', e); }"
|
||||
@interact-outside="emit('interactOutside', $event)"
|
||||
@dismiss="emit('dismiss')"
|
||||
>
|
||||
<slot />
|
||||
</DialogContent>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { DialogRootProps } from '../dialog';
|
||||
|
||||
export interface AlertDialogRootProps extends Omit<DialogRootProps, 'modal'> {}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot } from '../dialog';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps<AlertDialogRootProps>();
|
||||
const openModel = defineModel<boolean | undefined>('open', { default: undefined });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot
|
||||
:default-open="props.defaultOpen"
|
||||
:modal="true"
|
||||
:open="openModel"
|
||||
@update:open="openModel = $event"
|
||||
>
|
||||
<slot :open="openModel" />
|
||||
</DialogRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,118 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import type { VueWrapper } from '@vue/test-utils';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { defineComponent, h, nextTick, ref } from 'vue';
|
||||
import {
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogRoot,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '../index';
|
||||
|
||||
const wrappers: Array<VueWrapper<any>> = [];
|
||||
|
||||
afterEach(() => {
|
||||
while (wrappers.length) wrappers.pop()!.unmount();
|
||||
document.body.innerHTML = '';
|
||||
document.body.removeAttribute('style');
|
||||
delete document.body.dataset['dismissableBlocking'];
|
||||
});
|
||||
|
||||
function track<T extends VueWrapper<any>>(w: T): T {
|
||||
wrappers.push(w);
|
||||
return w;
|
||||
}
|
||||
|
||||
function mountAlert(initialOpen = true) {
|
||||
const open = ref(initialOpen);
|
||||
const Harness = defineComponent({
|
||||
setup() {
|
||||
return () => h(
|
||||
AlertDialogRoot,
|
||||
{
|
||||
open: open.value,
|
||||
'onUpdate:open': (v: boolean | undefined) => { open.value = v!; },
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(AlertDialogTrigger, null, { default: () => 'Open' }),
|
||||
h(AlertDialogPortal, null, {
|
||||
default: () => [
|
||||
h(AlertDialogOverlay),
|
||||
h(AlertDialogContent, null, {
|
||||
default: () => [
|
||||
h(AlertDialogTitle, null, { default: () => 'Are you sure?' }),
|
||||
h(AlertDialogDescription, null, { default: () => 'This cannot be undone.' }),
|
||||
h(AlertDialogCancel, null, { default: () => 'Cancel' }),
|
||||
h(AlertDialogAction, null, { default: () => 'OK' }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
const w = track(mount(Harness, { attachTo: document.body }));
|
||||
return { wrapper: w, open };
|
||||
}
|
||||
|
||||
describe('AlertDialog', () => {
|
||||
it('renders content with role="alertdialog"', async () => {
|
||||
mountAlert(true);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const content = document.querySelector('[data-alert-dialog-content]');
|
||||
expect(content).toBeTruthy();
|
||||
expect(content!.getAttribute('role')).toBe('alertdialog');
|
||||
});
|
||||
|
||||
it('labels content via Title and describes via Description', async () => {
|
||||
mountAlert(true);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const content = document.querySelector<HTMLElement>('[data-alert-dialog-content]')!;
|
||||
const labelledby = content.getAttribute('aria-labelledby');
|
||||
const describedby = content.getAttribute('aria-describedby');
|
||||
expect(labelledby).toMatch(/dialog-title/);
|
||||
expect(describedby).toMatch(/dialog-description/);
|
||||
expect(document.getElementById(labelledby!)?.textContent).toBe('Are you sure?');
|
||||
expect(document.getElementById(describedby!)?.textContent).toBe('This cannot be undone.');
|
||||
});
|
||||
|
||||
it('Cancel button closes the dialog', async () => {
|
||||
const { open } = mountAlert(true);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const cancel = document.querySelector<HTMLButtonElement>('[data-alert-dialog-cancel]')!;
|
||||
cancel.click();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(open.value).toBe(false);
|
||||
});
|
||||
|
||||
it('Action button closes the dialog', async () => {
|
||||
const { open } = mountAlert(true);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
const action = document.querySelector<HTMLButtonElement>('[data-alert-dialog-action]')!;
|
||||
action.click();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(open.value).toBe(false);
|
||||
});
|
||||
|
||||
it('Cancel and Action carry data attributes', async () => {
|
||||
mountAlert(true);
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
expect(document.querySelector('[data-alert-dialog-cancel]')).toBeTruthy();
|
||||
expect(document.querySelector('[data-alert-dialog-action]')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export { DialogDescription as AlertDialogDescription, DialogOverlay as AlertDialogOverlay, DialogPortal as AlertDialogPortal, DialogTitle as AlertDialogTitle, DialogTrigger as AlertDialogTrigger } from '../dialog';
|
||||
export { default as AlertDialogAction } from './AlertDialogAction.vue';
|
||||
export { default as AlertDialogCancel } from './AlertDialogCancel.vue';
|
||||
export { default as AlertDialogContent } from './AlertDialogContent.vue';
|
||||
|
||||
export { default as AlertDialogRoot } from './AlertDialogRoot.vue';
|
||||
|
||||
export type { AlertDialogActionProps } from './AlertDialogAction.vue';
|
||||
export type { AlertDialogCancelProps } from './AlertDialogCancel.vue';
|
||||
export type { AlertDialogContentEmits, AlertDialogContentProps } from './AlertDialogContent.vue';
|
||||
export type { AlertDialogRootProps } from './AlertDialogRoot.vue';
|
||||
Reference in New Issue
Block a user