fix(primitives): make v-model, native attributes and panel styling usable under strict TS
Publish to NPM / Check version changes and publish (push) Successful in 11m33s
Publish to NPM / Check version changes and publish (push) Successful in 11m33s
Consuming the package under `verbatimModuleSyntax` + `strictTemplates` surfaced four defects that forced workarounds downstream. - Drop the deprecated `SelectValue` string alias. It collided with the `SelectValue` component exported from the same barrel, so the component resolved to the type meaning and could not be imported (TS1484). - Narrow the select's model to `SelectModelValue<T, Multiple>` and make `TabsRoot` generic over its value, so a plain `v-model` on a `Ref<string>` type-checks. Both roots declare the value prop and emit explicitly instead of via `defineModel`, which would widen the payload with `| undefined` even though neither control ever clears its value. - Stop declaring `defineModel` keys in `defineEmits` as well. The duplicate erased the payload type from the generated declarations, shipping `(...args: unknown[]) => any` for eleven components' model events. - Let every part accept global DOM attributes (`id`, `role`, `aria-*`, `data-*`, ...) through `PrimitiveAttributes`. The heritage clause is marked `@vue-ignore`, so they stay out of the runtime props and keep falling through via `$attrs` exactly as before. - Give `SelectContent` and `SelectViewport` a single styleable root: the content forwards `$attrs` onto the panel, and the viewport's scrollbar CSS moves to a reference-counted `<head>` style tag. A forwarded `class` was previously dropped, leaving the panel unstyled. The tsconfig vue preset gains `htmlAttributes: ["aria-*", "data-*"]` so hyphenated data attributes are not camelized before they reach those types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -385,3 +385,74 @@ describe('Select — native form submission', () => {
|
||||
w.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select — attribute forwarding on the panel', () => {
|
||||
function mountStyled() {
|
||||
return track(mount(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(
|
||||
SelectRoot,
|
||||
{ defaultOpen: true },
|
||||
{
|
||||
default: () => [
|
||||
h(SelectTrigger, { id: 'styled-trigger', 'aria-label': 'Fruit' }, {
|
||||
default: () => h(SelectValue, { placeholder: 'Pick one' }),
|
||||
}),
|
||||
h(SelectPortal, null, {
|
||||
default: () => h(SelectContent, { class: 'panel', 'data-panel': 'yes' }, {
|
||||
default: () => h(SelectViewport, { class: 'viewport' }, {
|
||||
default: () => h(SelectItem, { value: 'apple' }, {
|
||||
default: () => h(SelectItemText, null, { default: () => 'Apple' }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
},
|
||||
}),
|
||||
{ attachTo: document.body },
|
||||
));
|
||||
}
|
||||
|
||||
it('forwards class and data attributes from SelectContent to the panel element', async () => {
|
||||
const w = mountStyled();
|
||||
await flush();
|
||||
const panel = document.querySelector('[data-primitives-select-content]') as HTMLElement | null;
|
||||
expect(panel).toBeTruthy();
|
||||
expect(panel!.classList.contains('panel')).toBe(true);
|
||||
expect(panel!.getAttribute('data-panel')).toBe('yes');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('forwards class from SelectViewport to the viewport element', async () => {
|
||||
const w = mountStyled();
|
||||
await flush();
|
||||
const viewport = document.querySelector('[data-primitives-select-viewport]') as HTMLElement | null;
|
||||
expect(viewport).toBeTruthy();
|
||||
expect(viewport!.classList.contains('viewport')).toBe(true);
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('keeps the trigger a single root that accepts native attributes', async () => {
|
||||
const w = mountStyled();
|
||||
await flush();
|
||||
const trigger = getTrigger();
|
||||
expect(trigger.id).toBe('styled-trigger');
|
||||
expect(trigger.getAttribute('aria-label')).toBe('Fruit');
|
||||
w.unmount();
|
||||
});
|
||||
|
||||
it('injects the scrollbar-hiding stylesheet into head instead of a sibling style node', async () => {
|
||||
const w = mountStyled();
|
||||
await flush();
|
||||
const injected = document.head.querySelector('#primitives-select-viewport');
|
||||
expect(injected).toBeTruthy();
|
||||
expect(injected!.textContent).toContain('[data-primitives-select-viewport]');
|
||||
const panel = document.querySelector('[data-primitives-select-content]') as HTMLElement;
|
||||
expect(panel.querySelector('style')).toBeNull();
|
||||
w.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user