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:
2026-06-07 16:29:56 +07:00
parent c7644ade69
commit 626fbc70d8
408 changed files with 27367 additions and 154 deletions
@@ -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();
});
});
+11
View File
@@ -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';