Prettier
Didn't run this on every single file because it messes up the formatting of the some of the TestRenderer constructions
This commit is contained in:
28
README.md
28
README.md
@@ -6,8 +6,8 @@ This is an unofficial, complete Svelte port of the Headless UI component library
|
|||||||
|
|
||||||
This library is for you if you fall into one of two categories:
|
This library is for you if you fall into one of two categories:
|
||||||
|
|
||||||
* You want unstyled yet sophisticated customizable UI components that fully follow the WAI-ARIA specs. You want a component library to handle all the messy details (keyboard navigation, focus management, aria-* attributes, and many many more), but you want to style your components yourself and not be constrained by existing design systems like Material UI.
|
- You want unstyled yet sophisticated customizable UI components that fully follow the WAI-ARIA specs. You want a component library to handle all the messy details (keyboard navigation, focus management, aria-\* attributes, and many many more), but you want to style your components yourself and not be constrained by existing design systems like Material UI.
|
||||||
* You want to use the commercial Tailwind UI component library (https://tailwindui.com/) in your Svelte project, and want a drop-in replacement for the React components which power Tailwind UI.
|
- You want to use the commercial Tailwind UI component library (https://tailwindui.com/) in your Svelte project, and want a drop-in replacement for the React components which power Tailwind UI.
|
||||||
|
|
||||||
This project is intended to keep an API as close as possible to the React API for the base Headless UI project, with only a few small differences. While one of the primary goals is to enable using Tailwind UI in a Svelte project with as little effort as possible, **neither Tailwind UI nor Tailwind CSS is required** to use these components.
|
This project is intended to keep an API as close as possible to the React API for the base Headless UI project, with only a few small differences. While one of the primary goals is to enable using Tailwind UI in a Svelte project with as little effort as possible, **neither Tailwind UI nor Tailwind CSS is required** to use these components.
|
||||||
|
|
||||||
@@ -22,8 +22,10 @@ npm install @rgossiaux/svelte-headlessui
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
For now, until I write separate documentation, you can refer to the [Headless UI React documentation](https://headlessui.dev/). The API is nearly identical to the React API there, with the following differences:
|
For now, until I write separate documentation, you can refer to the [Headless UI React documentation](https://headlessui.dev/). The API is nearly identical to the React API there, with the following differences:
|
||||||
* Components do not have . in their names; use `ListboxButton` instead of `Listbox.Button`
|
|
||||||
* Event handlers are done Svelte-style with custom events:
|
- Components do not have . in their names; use `ListboxButton` instead of `Listbox.Button`
|
||||||
|
- Event handlers are done Svelte-style with custom events:
|
||||||
|
|
||||||
```
|
```
|
||||||
// React version
|
// React version
|
||||||
<Listbox onChange={(value) => console.log(value)}>
|
<Listbox onChange={(value) => console.log(value)}>
|
||||||
@@ -38,7 +40,8 @@ For now, until I write separate documentation, you can refer to the [Headless UI
|
|||||||
|
|
||||||
Note the `.detail` that is needed to get the event value in the Svelte version.
|
Note the `.detail` that is needed to get the event value in the Svelte version.
|
||||||
|
|
||||||
* Instead of render props, we use Svelte's [slot props](https://svelte.dev/tutorial/slot-props):
|
- Instead of render props, we use Svelte's [slot props](https://svelte.dev/tutorial/slot-props):
|
||||||
|
|
||||||
```
|
```
|
||||||
// React version
|
// React version
|
||||||
<Listbox.Button>
|
<Listbox.Button>
|
||||||
@@ -50,8 +53,10 @@ Note the `.detail` that is needed to get the event value in the Svelte version.
|
|||||||
<!--- Something using open and disabled --->
|
<!--- Something using open and disabled --->
|
||||||
</ListboxButton>
|
</ListboxButton>
|
||||||
```
|
```
|
||||||
* When porting React code, HTML attributes use their real names instead of the camelCased React versions. In particular, use `class=` instead of `className=`.
|
|
||||||
* When porting React code, use `{#each}` instead of `.map` (don't forget to add a key!):
|
- When porting React code, HTML attributes use their real names instead of the camelCased React versions. In particular, use `class=` instead of `className=`.
|
||||||
|
- When porting React code, use `{#each}` instead of `.map` (don't forget to add a key!):
|
||||||
|
|
||||||
```
|
```
|
||||||
// React version
|
// React version
|
||||||
{people.map((person) => (
|
{people.map((person) => (
|
||||||
@@ -64,12 +69,15 @@ Note the `.detail` that is needed to get the event value in the Svelte version.
|
|||||||
<ListboxOption
|
<ListboxOption
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Similarly, React's `{value && (<Component />)}` style syntax becomes `{#if value} <Component /> {/if}` in Svelte, of course.
|
Similarly, React's `{value && (<Component />)}` style syntax becomes `{#if value} <Component /> {/if}` in Svelte, of course.
|
||||||
* While the `as` prop is supported, `as={Fragment}` support is not possible due to limitations in Svelte itself. You'll have to settle for rendering a `div` or similar. Usually this won't cause any problems, especially if you are writing your own code, but if you are porting React Headless UI code that both uses `as={Fragment}` and `z-index`, your div could possibly create a new [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context), causing the `z-index` to not work correctly. If this happens, you can fix by copying the `z-index` (and a relevant `position`) to the component that used to render as a `Fragment`.
|
|
||||||
|
- While the `as` prop is supported, `as={Fragment}` support is not possible due to limitations in Svelte itself. You'll have to settle for rendering a `div` or similar. Usually this won't cause any problems, especially if you are writing your own code, but if you are porting React Headless UI code that both uses `as={Fragment}` and `z-index`, your div could possibly create a new [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context), causing the `z-index` to not work correctly. If this happens, you can fix by copying the `z-index` (and a relevant `position`) to the component that used to render as a `Fragment`.
|
||||||
|
|
||||||
Furthermore, specific to Svelte, you may
|
Furthermore, specific to Svelte, you may
|
||||||
* Pass [actions](https://svelte.dev/tutorial/actions) to any component in this library using the `use` prop, with the syntax `use={[[action1, action1options], [action2], [action3, action3options], ...]}`, and they will be forwarded to the underlying DOM element.
|
|
||||||
* Add your own event listeners with modifiers, which will be forwarded to the underyling DOM element. Modifiers are separated with the `!` character instead of the normal `|`: `on:click!capture={(e) => ...}`
|
- Pass [actions](https://svelte.dev/tutorial/actions) to any component in this library using the `use` prop, with the syntax `use={[[action1, action1options], [action2], [action3, action3options], ...]}`, and they will be forwarded to the underlying DOM element.
|
||||||
|
- Add your own event listeners with modifiers, which will be forwarded to the underyling DOM element. Modifiers are separated with the `!` character instead of the normal `|`: `on:click!capture={(e) => ...}`
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
|
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.svelte$': ['svelte-jester', { preprocess: true }],
|
"^.+\\.svelte$": ["svelte-jester", { preprocess: true }],
|
||||||
'^.+\\.js$': 'babel-jest',
|
"^.+\\.js$": "babel-jest",
|
||||||
'^.+\\.ts$': 'ts-jest',
|
"^.+\\.ts$": "ts-jest",
|
||||||
},
|
},
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
|
||||||
'@testing-library/jest-dom/extend-expect',
|
|
||||||
],
|
|
||||||
testEnvironment: "jsdom",
|
testEnvironment: "jsdom",
|
||||||
moduleFileExtensions: ['js', 'ts', 'svelte'],
|
moduleFileExtensions: ["js", "ts", "svelte"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"\\$lib/(.+)$": "<rootDir>/src/lib/$1",
|
"\\$lib/(.+)$": "<rootDir>/src/lib/$1",
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Dialog, DialogDescription, DialogOverlay, DialogTitle } from "."
|
import { Dialog, DialogDescription, DialogOverlay, DialogTitle } from ".";
|
||||||
import TestTabSentinel from "./_TestTabSentinel.svelte";
|
import TestTabSentinel from "./_TestTabSentinel.svelte";
|
||||||
import ManagedDialog from "./_ManagedDialog.svelte";
|
import ManagedDialog from "./_ManagedDialog.svelte";
|
||||||
import NestedTestComponent from "./_NestedTestComponent.svelte";
|
import NestedTestComponent from "./_NestedTestComponent.svelte";
|
||||||
@@ -10,95 +10,100 @@ import Div from "$lib/internal/elements/Div.svelte";
|
|||||||
import Form from "$lib/internal/elements/Form.svelte";
|
import Form from "$lib/internal/elements/Form.svelte";
|
||||||
import P from "$lib/internal/elements/P.svelte";
|
import P from "$lib/internal/elements/P.svelte";
|
||||||
import Input from "$lib/internal/elements/Input.svelte";
|
import Input from "$lib/internal/elements/Input.svelte";
|
||||||
import { assertActiveElement, assertDialog, assertDialogDescription, DialogState, getByText, getDialog, getDialogOverlay, getDialogOverlays, getDialogs } from "$lib/test-utils/accessibility-assertions";
|
import {
|
||||||
|
assertActiveElement,
|
||||||
|
assertDialog,
|
||||||
|
assertDialogDescription,
|
||||||
|
DialogState,
|
||||||
|
getByText,
|
||||||
|
getDialog,
|
||||||
|
getDialogOverlay,
|
||||||
|
getDialogOverlays,
|
||||||
|
getDialogs,
|
||||||
|
} from "$lib/test-utils/accessibility-assertions";
|
||||||
import { click, Keys, press } from "$lib/test-utils/interactions";
|
import { click, Keys, press } from "$lib/test-utils/interactions";
|
||||||
import Transition from "$lib/components/transitions/TransitionRoot.svelte";
|
import Transition from "$lib/components/transitions/TransitionRoot.svelte";
|
||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
jest.mock('../../hooks/use-id', () => {
|
jest.mock("../../hooks/use-id", () => {
|
||||||
return {
|
return {
|
||||||
useId: jest.fn(() => ++id),
|
useId: jest.fn(() => ++id),
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
global.IntersectionObserver = class FakeIntersectionObserver {
|
global.IntersectionObserver = class FakeIntersectionObserver {
|
||||||
observe() {}
|
observe() {}
|
||||||
disconnect() {}
|
disconnect() {}
|
||||||
}
|
};
|
||||||
|
|
||||||
beforeEach(() => id = 0)
|
beforeEach(() => (id = 0));
|
||||||
afterAll(() => jest.restoreAllMocks())
|
afterAll(() => jest.restoreAllMocks());
|
||||||
|
|
||||||
describe('Safe guards', () => {
|
describe("Safe guards", () => {
|
||||||
it.each([
|
it.each([
|
||||||
['DialogOverlay', DialogOverlay],
|
["DialogOverlay", DialogOverlay],
|
||||||
['DialogTitle', DialogTitle],
|
["DialogTitle", DialogTitle],
|
||||||
])(
|
])(
|
||||||
'should error when we are using a <%s /> without a parent <Dialog />',
|
"should error when we are using a <%s /> without a parent <Dialog />",
|
||||||
suppressConsoleLogs((name, Component) => {
|
suppressConsoleLogs((name, Component) => {
|
||||||
expect(() => render(Component)).toThrowError(
|
expect(() => render(Component)).toThrowError(
|
||||||
`<${name} /> is missing a parent <Dialog /> component.`
|
`<${name} /> is missing a parent <Dialog /> component.`
|
||||||
)
|
);
|
||||||
expect.hasAssertions()
|
expect.hasAssertions();
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
it(
|
it(
|
||||||
'should be possible to render a Dialog without crashing',
|
"should be possible to render a Dialog without crashing",
|
||||||
suppressConsoleLogs(async () => {
|
suppressConsoleLogs(async () => {
|
||||||
render(
|
render(TestRenderer, {
|
||||||
TestRenderer, {
|
|
||||||
allProps: [
|
allProps: [
|
||||||
Dialog,
|
Dialog,
|
||||||
{ open: false, onClose: console.log },
|
{ open: false, onClose: console.log },
|
||||||
[
|
[
|
||||||
[Button,
|
[Button, {}, "Trigger"],
|
||||||
{},
|
|
||||||
"Trigger"],
|
|
||||||
[DialogOverlay],
|
[DialogOverlay],
|
||||||
[DialogTitle],
|
[DialogTitle],
|
||||||
[P, {}, "Contents"],
|
[P, {}, "Contents"],
|
||||||
[DialogDescription]
|
[DialogDescription],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
assertDialog({ state: DialogState.InvisibleUnmounted })
|
assertDialog({ state: DialogState.InvisibleUnmounted });
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe("Rendering", () => {
|
||||||
describe('Dialog', () => {
|
describe("Dialog", () => {
|
||||||
it(
|
it(
|
||||||
'should complain when the `open` and `onClose` prop are missing',
|
"should complain when the `open` and `onClose` prop are missing",
|
||||||
suppressConsoleLogs(async () => {
|
|
||||||
expect(() => render(Dialog, { as: "div" })).toThrowErrorMatchingInlineSnapshot(
|
|
||||||
`"You forgot to provide an \`open\` prop to the \`Dialog\` component."`
|
|
||||||
)
|
|
||||||
expect.hasAssertions()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
it(
|
|
||||||
'should complain when an `open` prop is not a boolean',
|
|
||||||
suppressConsoleLogs(async () => {
|
suppressConsoleLogs(async () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
render(
|
render(Dialog, { as: "div" })
|
||||||
TestRenderer, {
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
allProps: [
|
`"You forgot to provide an \`open\` prop to the \`Dialog\` component."`
|
||||||
Dialog,
|
);
|
||||||
{ open: null, onClose: console.log, as: "div" },
|
expect.hasAssertions();
|
||||||
]
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"should complain when an `open` prop is not a boolean",
|
||||||
|
suppressConsoleLogs(async () => {
|
||||||
|
expect(() =>
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [Dialog, { open: null, onClose: console.log, as: "div" }],
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: null"`
|
`"You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: null"`
|
||||||
)
|
);
|
||||||
expect.hasAssertions()
|
expect.hasAssertions();
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
// TODO: render prop tests!
|
// TODO: render prop tests!
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ export { default as Dialog } from "./Dialog.svelte";
|
|||||||
export { default as DialogTitle } from "./DialogTitle.svelte";
|
export { default as DialogTitle } from "./DialogTitle.svelte";
|
||||||
export { default as DialogOverlay } from "./DialogOverlay.svelte";
|
export { default as DialogOverlay } from "./DialogOverlay.svelte";
|
||||||
export { default as DialogDescription } from "./../description/Description.svelte";
|
export { default as DialogDescription } from "./../description/Description.svelte";
|
||||||
|
|
||||||
|
|||||||
@@ -2,70 +2,76 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from ".";
|
|||||||
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
||||||
import { render } from "@testing-library/svelte";
|
import { render } from "@testing-library/svelte";
|
||||||
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
|
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
|
||||||
import { assertActiveElement, assertDisclosureButton, assertDisclosurePanel, DisclosureState, getByText, getDisclosureButton, getDisclosurePanel } from "$lib/test-utils/accessibility-assertions";
|
import {
|
||||||
|
assertActiveElement,
|
||||||
|
assertDisclosureButton,
|
||||||
|
assertDisclosurePanel,
|
||||||
|
DisclosureState,
|
||||||
|
getByText,
|
||||||
|
getDisclosureButton,
|
||||||
|
getDisclosurePanel,
|
||||||
|
} from "$lib/test-utils/accessibility-assertions";
|
||||||
import { click, Keys, MouseButton, press } from "$lib/test-utils/interactions";
|
import { click, Keys, MouseButton, press } from "$lib/test-utils/interactions";
|
||||||
import { Transition, TransitionChild } from "../transitions";
|
import { Transition, TransitionChild } from "../transitions";
|
||||||
import TransitionDebug from "./_TransitionDebug.svelte";
|
import TransitionDebug from "./_TransitionDebug.svelte";
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
jest.mock('../../hooks/use-id', () => {
|
jest.mock("../../hooks/use-id", () => {
|
||||||
return {
|
return {
|
||||||
useId: jest.fn(() => ++id),
|
useId: jest.fn(() => ++id),
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
||||||
|
beforeEach(() => (id = 0));
|
||||||
beforeEach(() => id = 0)
|
afterAll(() => jest.restoreAllMocks());
|
||||||
afterAll(() => jest.restoreAllMocks())
|
|
||||||
|
|
||||||
function nextFrame() {
|
function nextFrame() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>((resolve) => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Safe guards', () => {
|
describe("Safe guards", () => {
|
||||||
it.each([
|
it.each([
|
||||||
['DisclosureButton', DisclosureButton],
|
["DisclosureButton", DisclosureButton],
|
||||||
['DisclosurePanel', DisclosurePanel],
|
["DisclosurePanel", DisclosurePanel],
|
||||||
])(
|
])(
|
||||||
'should error when we are using a <%s /> without a parent <Disclosure />',
|
"should error when we are using a <%s /> without a parent <Disclosure />",
|
||||||
suppressConsoleLogs((name, Component) => {
|
suppressConsoleLogs((name, Component) => {
|
||||||
expect(() => render(Component)).toThrowError(
|
expect(() => render(Component)).toThrowError(
|
||||||
`<${name} /> is missing a parent <Disclosure /> component.`
|
`<${name} /> is missing a parent <Disclosure /> component.`
|
||||||
)
|
);
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
it(
|
it(
|
||||||
'should be possible to render a Disclosure without crashing',
|
"should be possible to render a Disclosure without crashing",
|
||||||
suppressConsoleLogs(async () => {
|
suppressConsoleLogs(async () => {
|
||||||
render(
|
render(TestRenderer, {
|
||||||
TestRenderer, {
|
|
||||||
allProps: [
|
allProps: [
|
||||||
Disclosure,
|
Disclosure,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[DisclosureButton, {}, "Trigger"],
|
[DisclosureButton, {}, "Trigger"],
|
||||||
[DisclosurePanel, {}, "Contents"],
|
[DisclosurePanel, {}, "Contents"],
|
||||||
]
|
],
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
assertDisclosureButton({
|
assertDisclosureButton({
|
||||||
state: DisclosureState.InvisibleUnmounted,
|
state: DisclosureState.InvisibleUnmounted,
|
||||||
attributes: { id: 'headlessui-disclosure-button-1' },
|
attributes: { id: "headlessui-disclosure-button-1" },
|
||||||
})
|
});
|
||||||
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
|
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted });
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe("Rendering", () => {
|
||||||
// describe('Disclosure', () => {
|
// describe('Disclosure', () => {
|
||||||
// it(
|
// it(
|
||||||
// 'should be possible to render a Disclosure using a render prop',
|
// 'should be possible to render a Disclosure using a render prop',
|
||||||
@@ -242,7 +248,7 @@ describe('Rendering', () => {
|
|||||||
// )
|
// )
|
||||||
// })
|
// })
|
||||||
|
|
||||||
describe('DisclosureButton', () => {
|
describe("DisclosureButton", () => {
|
||||||
// it(
|
// it(
|
||||||
// 'should be possible to render a DisclosureButton using a render prop',
|
// 'should be possible to render a DisclosureButton using a render prop',
|
||||||
// suppressConsoleLogs(async () => {
|
// suppressConsoleLogs(async () => {
|
||||||
|
|||||||
@@ -1,9 +1,44 @@
|
|||||||
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from ".";
|
import {
|
||||||
|
Listbox,
|
||||||
|
ListboxButton,
|
||||||
|
ListboxLabel,
|
||||||
|
ListboxOption,
|
||||||
|
ListboxOptions,
|
||||||
|
} from ".";
|
||||||
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
||||||
import { render } from "@testing-library/svelte";
|
import { render } from "@testing-library/svelte";
|
||||||
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
|
import TestRenderer from "$lib/test-utils/TestRenderer.svelte";
|
||||||
import { assertActiveElement, assertActiveListboxOption, assertListbox, assertListboxButton, assertListboxButtonLinkedWithListbox, assertListboxButtonLinkedWithListboxLabel, assertListboxOption, assertNoActiveListboxOption, assertNoSelectedListboxOption, getByText, getListbox, getListboxButton, getListboxButtons, getListboxes, getListboxLabel, getListboxOptions, ListboxState } from "$lib/test-utils/accessibility-assertions";
|
import {
|
||||||
import { click, focus, Keys, MouseButton, mouseLeave, mouseMove, press, shift, type, word } from "$lib/test-utils/interactions";
|
assertActiveElement,
|
||||||
|
assertActiveListboxOption,
|
||||||
|
assertListbox,
|
||||||
|
assertListboxButton,
|
||||||
|
assertListboxButtonLinkedWithListbox,
|
||||||
|
assertListboxButtonLinkedWithListboxLabel,
|
||||||
|
assertListboxOption,
|
||||||
|
assertNoActiveListboxOption,
|
||||||
|
assertNoSelectedListboxOption,
|
||||||
|
getByText,
|
||||||
|
getListbox,
|
||||||
|
getListboxButton,
|
||||||
|
getListboxButtons,
|
||||||
|
getListboxes,
|
||||||
|
getListboxLabel,
|
||||||
|
getListboxOptions,
|
||||||
|
ListboxState,
|
||||||
|
} from "$lib/test-utils/accessibility-assertions";
|
||||||
|
import {
|
||||||
|
click,
|
||||||
|
focus,
|
||||||
|
Keys,
|
||||||
|
MouseButton,
|
||||||
|
mouseLeave,
|
||||||
|
mouseMove,
|
||||||
|
press,
|
||||||
|
shift,
|
||||||
|
type,
|
||||||
|
word,
|
||||||
|
} from "$lib/test-utils/interactions";
|
||||||
import { Transition } from "../transitions";
|
import { Transition } from "../transitions";
|
||||||
import TransitionDebug from "$lib/components/disclosure/_TransitionDebug.svelte";
|
import TransitionDebug from "$lib/components/disclosure/_TransitionDebug.svelte";
|
||||||
import ManagedListbox from "./_ManagedListbox.svelte";
|
import ManagedListbox from "./_ManagedListbox.svelte";
|
||||||
@@ -215,9 +250,9 @@ describe('Rendering', () => {
|
|||||||
// assertListbox({ state: ListboxState.Visible })
|
// assertListbox({ state: ListboxState.Visible })
|
||||||
// })
|
// })
|
||||||
// )
|
// )
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('ListboxButton', () => {
|
describe("ListboxButton", () => {
|
||||||
// it(
|
// it(
|
||||||
// 'should be possible to render a ListboxButton using a render prop',
|
// 'should be possible to render a ListboxButton using a render prop',
|
||||||
// suppressConsoleLogs(async () => {
|
// suppressConsoleLogs(async () => {
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ export { default as RadioGroup } from "./RadioGroup.svelte";
|
|||||||
export { default as RadioGroupOption } from "./RadioGroupOption.svelte";
|
export { default as RadioGroupOption } from "./RadioGroupOption.svelte";
|
||||||
export { default as RadioGroupLabel } from "../label/Label.svelte";
|
export { default as RadioGroupLabel } from "../label/Label.svelte";
|
||||||
export { default as RadioGroupDescription } from "../description/Description.svelte";
|
export { default as RadioGroupDescription } from "../description/Description.svelte";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { assertActiveElement, assertFocusable, assertNotFocusable, assertRadioGroupLabel, getByText, getRadioGroupOptions } from "$lib/test-utils/accessibility-assertions";
|
import {
|
||||||
|
assertActiveElement,
|
||||||
|
assertFocusable,
|
||||||
|
assertNotFocusable,
|
||||||
|
assertRadioGroupLabel,
|
||||||
|
getByText,
|
||||||
|
getRadioGroupOptions,
|
||||||
|
} from "$lib/test-utils/accessibility-assertions";
|
||||||
import { render } from "@testing-library/svelte";
|
import { render } from "@testing-library/svelte";
|
||||||
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from ".";
|
import { RadioGroup, RadioGroupLabel, RadioGroupOption } from ".";
|
||||||
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { render } from "@testing-library/svelte";
|
import { render } from "@testing-library/svelte";
|
||||||
import TestRenderer from "../../test-utils/TestRenderer.svelte";
|
import TestRenderer from "../../test-utils/TestRenderer.svelte";
|
||||||
import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from ".";
|
import { Switch, SwitchDescription, SwitchGroup, SwitchLabel } from ".";
|
||||||
import { assertActiveElement, assertSwitch, getSwitch, getSwitchLabel, SwitchState } from "$lib/test-utils/accessibility-assertions";
|
import {
|
||||||
|
assertActiveElement,
|
||||||
|
assertSwitch,
|
||||||
|
getSwitch,
|
||||||
|
getSwitchLabel,
|
||||||
|
SwitchState,
|
||||||
|
} from "$lib/test-utils/accessibility-assertions";
|
||||||
import Button from "$lib/internal/elements/Button.svelte";
|
import Button from "$lib/internal/elements/Button.svelte";
|
||||||
import Div from "$lib/internal/elements/Div.svelte";
|
import Div from "$lib/internal/elements/Div.svelte";
|
||||||
import Span from "$lib/internal/elements/Span.svelte";
|
import Span from "$lib/internal/elements/Span.svelte";
|
||||||
import ManagedSwitch from "./_ManagedSwitch.svelte";
|
import ManagedSwitch from "./_ManagedSwitch.svelte";
|
||||||
import { click, Keys, press } from "$lib/test-utils/interactions";
|
import { click, Keys, press } from "$lib/test-utils/interactions";
|
||||||
jest.mock('../../hooks/use-id')
|
jest.mock("../../hooks/use-id");
|
||||||
|
|
||||||
describe('Safe guards', () => {
|
describe("Safe guards", () => {
|
||||||
it('should be possible to render a Switch without crashing', () => {
|
it("should be possible to render a Switch without crashing", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [Switch, { checked: false, onChange: console.log }],
|
||||||
Switch,
|
});
|
||||||
{ checked: false, onChange: console.log }
|
});
|
||||||
]
|
|
||||||
});
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Rendering', () => {
|
describe("Rendering", () => {
|
||||||
// TODO: handle these render prop (slot prop) tests
|
// TODO: handle these render prop (slot prop) tests
|
||||||
|
|
||||||
// it('should be possible to render an (on) Switch using a render prop', () => {
|
// it('should be possible to render an (on) Switch using a render prop', () => {
|
||||||
@@ -43,383 +46,334 @@ describe('Rendering', () => {
|
|||||||
// assertSwitch({ state: SwitchState.Off, textContent: 'Off' })
|
// assertSwitch({ state: SwitchState.Off, textContent: 'Off' })
|
||||||
// })
|
// })
|
||||||
|
|
||||||
it('should be possible to render an (on) Switch using an `as` prop', () => {
|
it("should be possible to render an (on) Switch using an `as` prop", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [Switch, { as: "span", checked: true, onChange: console.log }],
|
||||||
Switch,
|
});
|
||||||
{ as: "span", checked: true, onChange: console.log },
|
assertSwitch({ state: SwitchState.On, tag: "span" });
|
||||||
]
|
|
||||||
});
|
});
|
||||||
assertSwitch({ state: SwitchState.On, tag: 'span' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be possible to render an (off) Switch using an `as` prop', () => {
|
it("should be possible to render an (off) Switch using an `as` prop", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [Switch, { as: "span", checked: false, onChange: console.log }],
|
||||||
Switch,
|
});
|
||||||
{ as: "span", checked: false, onChange: console.log },
|
assertSwitch({ state: SwitchState.Off, tag: "span" });
|
||||||
]
|
|
||||||
});
|
});
|
||||||
assertSwitch({ state: SwitchState.Off, tag: 'span' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be possible to use the switch contents as the label', () => {
|
it("should be possible to use the switch contents as the label", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
Switch,
|
Switch,
|
||||||
{ checked: false, onChange: console.log },
|
{ checked: false, onChange: console.log },
|
||||||
[
|
[Span, {}, "Enable notifications"],
|
||||||
Span,
|
],
|
||||||
{},
|
});
|
||||||
"Enable notifications"
|
assertSwitch({ state: SwitchState.Off, label: "Enable notifications" });
|
||||||
]
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('`type` attribute', () => {
|
describe("`type` attribute", () => {
|
||||||
it('should set the `type` to "button" by default', async () => {
|
it('should set the `type` to "button" by default', async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
Switch,
|
Switch,
|
||||||
{ checked: false, onChange: console.log },
|
{ checked: false, onChange: console.log },
|
||||||
"Trigger"
|
"Trigger",
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getSwitch()).toHaveAttribute('type', 'button')
|
expect(getSwitch()).toHaveAttribute("type", "button");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should not set the `type` to "button" if it already contains a `type`', async () => {
|
it('should not set the `type` to "button" if it already contains a `type`', async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
Switch,
|
Switch,
|
||||||
{ checked: false, onChange: console.log, type: "submit" },
|
{ checked: false, onChange: console.log, type: "submit" },
|
||||||
"Trigger"
|
"Trigger",
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getSwitch()).toHaveAttribute('type', 'submit')
|
expect(getSwitch()).toHaveAttribute("type", "submit");
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should not set the type if the "as" prop is not a "button"', async () => {
|
it('should not set the type if the "as" prop is not a "button"', async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
Switch,
|
Switch,
|
||||||
{ checked: false, onChange: console.log, as: "div" },
|
{ checked: false, onChange: console.log, as: "div" },
|
||||||
"Trigger"
|
"Trigger",
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getSwitch()).not.toHaveAttribute('type')
|
expect(getSwitch()).not.toHaveAttribute("type");
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('Render composition', () => {
|
describe("Render composition", () => {
|
||||||
it('should be possible to render a Switch.Group, Switch and Switch.Label', () => {
|
it("should be possible to render a Switch.Group, Switch and Switch.Label", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[Switch,
|
[Switch, { checked: false, onChange: console.log }],
|
||||||
{ checked: false, onChange: console.log }
|
[SwitchLabel, {}, "Enable notifications"],
|
||||||
],
|
],
|
||||||
[SwitchLabel,
|
],
|
||||||
{},
|
});
|
||||||
"Enable notifications"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
assertSwitch({ state: SwitchState.Off, label: 'Enable notifications' })
|
assertSwitch({ state: SwitchState.Off, label: "Enable notifications" });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should be possible to render a Switch.Group, Switch and Switch.Label (before the Switch)', () => {
|
it("should be possible to render a Switch.Group, Switch and Switch.Label (before the Switch)", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[SwitchLabel,
|
[SwitchLabel, {}, "Label B"],
|
||||||
{},
|
[Switch, { checked: false, onChange: console.log }, "Label A"],
|
||||||
"Label B"],
|
],
|
||||||
[Switch,
|
],
|
||||||
{ checked: false, onChange: console.log },
|
});
|
||||||
"Label A"]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||||
// technologies.
|
// technologies.
|
||||||
//
|
//
|
||||||
// Thus: Label A should not be part of the "label" in this case
|
// Thus: Label A should not be part of the "label" in this case
|
||||||
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
|
assertSwitch({ state: SwitchState.Off, label: "Label B" });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should be possible to render a Switch.Group, Switch and Switch.Label (after the Switch)', () => {
|
it("should be possible to render a Switch.Group, Switch and Switch.Label (after the Switch)", () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[Switch,
|
[Switch, { checked: false, onChange: console.log }, "Label A"],
|
||||||
{ checked: false, onChange: console.log },
|
[SwitchLabel, {}, "Label B"],
|
||||||
"Label A"],
|
],
|
||||||
[SwitchLabel,
|
],
|
||||||
{},
|
});
|
||||||
"Label B"]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||||
// technologies.
|
// technologies.
|
||||||
//
|
//
|
||||||
// Thus: Label A should not be part of the "label" in this case
|
// Thus: Label A should not be part of the "label" in this case
|
||||||
assertSwitch({ state: SwitchState.Off, label: 'Label B' })
|
assertSwitch({ state: SwitchState.Off, label: "Label B" });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)', async () => {
|
it("should be possible to render a Switch.Group, Switch and Switch.Description (before the Switch)", async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[SwitchDescription,
|
[SwitchDescription, {}, "This is an important feature"],
|
||||||
{},
|
[Switch, { checked: false, onChange: console.log }],
|
||||||
"This is an important feature"],
|
],
|
||||||
[Switch,
|
],
|
||||||
{ checked: false, onChange: console.log }],
|
});
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)', () => {
|
|
||||||
render(TestRenderer, {
|
|
||||||
allProps: [
|
|
||||||
SwitchGroup,
|
|
||||||
{},
|
|
||||||
[
|
|
||||||
[Switch,
|
|
||||||
{ checked: false, onChange: console.log }],
|
|
||||||
[SwitchDescription,
|
|
||||||
{},
|
|
||||||
"This is an important feature"],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
assertSwitch({ state: SwitchState.Off, description: 'This is an important feature' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be possible to render a Switch.Group, Switch, Switch.Label and Switch.Description', () => {
|
|
||||||
render(TestRenderer, {
|
|
||||||
allProps: [
|
|
||||||
SwitchGroup,
|
|
||||||
{},
|
|
||||||
[
|
|
||||||
[SwitchLabel,
|
|
||||||
{},
|
|
||||||
"Label A"],
|
|
||||||
[Switch,
|
|
||||||
{ checked: false, onChange: console.log }],
|
|
||||||
[SwitchDescription,
|
|
||||||
{},
|
|
||||||
"This is an important feature"],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
assertSwitch({
|
assertSwitch({
|
||||||
state: SwitchState.Off,
|
state: SwitchState.Off,
|
||||||
label: 'Label A',
|
description: "This is an important feature",
|
||||||
description: 'This is an important feature',
|
});
|
||||||
})
|
});
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Keyboard interactions', () => {
|
it("should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)", () => {
|
||||||
describe('`Space` key', () => {
|
|
||||||
it('should be possible to toggle the Switch with Space', async () => {
|
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
ManagedSwitch,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
]
|
[
|
||||||
})
|
[Switch, { checked: false, onChange: console.log }],
|
||||||
|
[SwitchDescription, {}, "This is an important feature"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSwitch({
|
||||||
|
state: SwitchState.Off,
|
||||||
|
description: "This is an important feature",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be possible to render a Switch.Group, Switch, Switch.Label and Switch.Description", () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [
|
||||||
|
SwitchGroup,
|
||||||
|
{},
|
||||||
|
[
|
||||||
|
[SwitchLabel, {}, "Label A"],
|
||||||
|
[Switch, { checked: false, onChange: console.log }],
|
||||||
|
[SwitchDescription, {}, "This is an important feature"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSwitch({
|
||||||
|
state: SwitchState.Off,
|
||||||
|
label: "Label A",
|
||||||
|
description: "This is an important feature",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Keyboard interactions", () => {
|
||||||
|
describe("`Space` key", () => {
|
||||||
|
it("should be possible to toggle the Switch with Space", async () => {
|
||||||
|
render(TestRenderer, {
|
||||||
|
allProps: [ManagedSwitch, {}],
|
||||||
|
});
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Focus the switch
|
// Focus the switch
|
||||||
getSwitch()?.focus()
|
getSwitch()?.focus();
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await press(Keys.Space)
|
await press(Keys.Space);
|
||||||
|
|
||||||
// Ensure state is on
|
// Ensure state is on
|
||||||
assertSwitch({ state: SwitchState.On })
|
assertSwitch({ state: SwitchState.On });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await press(Keys.Space)
|
await press(Keys.Space);
|
||||||
|
|
||||||
// Ensure state is off
|
// Ensure state is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('`Enter` key', () => {
|
describe("`Enter` key", () => {
|
||||||
it('should not be possible to use Enter to toggle the Switch', async () => {
|
it("should not be possible to use Enter to toggle the Switch", async () => {
|
||||||
let handleChange = jest.fn()
|
let handleChange = jest.fn();
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [ManagedSwitch, { onChange: handleChange }],
|
||||||
ManagedSwitch,
|
});
|
||||||
{ onChange: handleChange },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Focus the switch
|
// Focus the switch
|
||||||
getSwitch()?.focus()
|
getSwitch()?.focus();
|
||||||
|
|
||||||
// Try to toggle
|
// Try to toggle
|
||||||
await press(Keys.Enter)
|
await press(Keys.Enter);
|
||||||
|
|
||||||
expect(handleChange).not.toHaveBeenCalled()
|
expect(handleChange).not.toHaveBeenCalled();
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('`Tab` key', () => {
|
describe("`Tab` key", () => {
|
||||||
it('should be possible to tab away from the Switch', async () => {
|
it("should be possible to tab away from the Switch", async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
Div,
|
Div,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[Switch,
|
[Switch, { checked: false, onChange: console.log }],
|
||||||
{ checked: false, onChange: console.log }],
|
[Button, { id: "btn" }, "Other element"],
|
||||||
[Button, { id: "btn" }, "Other element"]
|
],
|
||||||
]
|
],
|
||||||
]
|
});
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Focus the switch
|
// Focus the switch
|
||||||
getSwitch()?.focus()
|
getSwitch()?.focus();
|
||||||
|
|
||||||
// Expect the switch to be active
|
// Expect the switch to be active
|
||||||
assertActiveElement(getSwitch())
|
assertActiveElement(getSwitch());
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await press(Keys.Tab)
|
await press(Keys.Tab);
|
||||||
|
|
||||||
// Expect the button to be active
|
// Expect the button to be active
|
||||||
assertActiveElement(document.getElementById('btn'))
|
assertActiveElement(document.getElementById("btn"));
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
describe('Mouse interactions', () => {
|
describe("Mouse interactions", () => {
|
||||||
it('should be possible to toggle the Switch with a click', async () => {
|
it("should be possible to toggle the Switch with a click", async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [ManagedSwitch, {}],
|
||||||
ManagedSwitch,
|
});
|
||||||
{},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await click(getSwitch())
|
await click(getSwitch());
|
||||||
|
|
||||||
// Ensure state is on
|
// Ensure state is on
|
||||||
assertSwitch({ state: SwitchState.On })
|
assertSwitch({ state: SwitchState.On });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await click(getSwitch())
|
await click(getSwitch());
|
||||||
|
|
||||||
// Ensure state is off
|
// Ensure state is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should be possible to toggle the Switch with a click on the Label', async () => {
|
it("should be possible to toggle the Switch with a click on the Label", async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[ManagedSwitch,
|
[ManagedSwitch, {}],
|
||||||
{},
|
[SwitchLabel, {}, "The label"],
|
||||||
],
|
],
|
||||||
[SwitchLabel,
|
],
|
||||||
{},
|
});
|
||||||
"The label"]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await click(getSwitchLabel())
|
await click(getSwitchLabel());
|
||||||
|
|
||||||
// Ensure the switch is focused
|
// Ensure the switch is focused
|
||||||
assertActiveElement(getSwitch())
|
assertActiveElement(getSwitch());
|
||||||
|
|
||||||
// Ensure state is on
|
// Ensure state is on
|
||||||
assertSwitch({ state: SwitchState.On })
|
assertSwitch({ state: SwitchState.On });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await click(getSwitchLabel())
|
await click(getSwitchLabel());
|
||||||
|
|
||||||
// Ensure the switch is focused
|
// Ensure the switch is focused
|
||||||
assertActiveElement(getSwitch())
|
assertActiveElement(getSwitch());
|
||||||
|
|
||||||
// Ensure state is off
|
// Ensure state is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
})
|
});
|
||||||
|
|
||||||
it('should not be possible to toggle the Switch with a click on the Label (passive)', async () => {
|
it("should not be possible to toggle the Switch with a click on the Label (passive)", async () => {
|
||||||
render(TestRenderer, {
|
render(TestRenderer, {
|
||||||
allProps: [
|
allProps: [
|
||||||
SwitchGroup,
|
SwitchGroup,
|
||||||
{},
|
{},
|
||||||
[
|
[
|
||||||
[ManagedSwitch,
|
[ManagedSwitch, {}],
|
||||||
{},
|
[SwitchLabel, { passive: true }, "The label"],
|
||||||
],
|
],
|
||||||
[SwitchLabel,
|
],
|
||||||
{ passive: true },
|
});
|
||||||
"The label"]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure checkbox is off
|
// Ensure checkbox is off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
|
|
||||||
// Toggle
|
// Toggle
|
||||||
await click(getSwitchLabel())
|
await click(getSwitchLabel());
|
||||||
|
|
||||||
// Ensure state is still off
|
// Ensure state is still off
|
||||||
assertSwitch({ state: SwitchState.Off })
|
assertSwitch({ state: SwitchState.Off });
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function useActions(
|
|||||||
return {
|
return {
|
||||||
update(actions: ActionArray) {
|
update(actions: ActionArray) {
|
||||||
if (((actions && actions.length) || 0) != actionReturns.length) {
|
if (((actions && actions.length) || 0) != actionReturns.length) {
|
||||||
throw new Error('You must not change the length of an actions array.');
|
throw new Error("You must not change the length of an actions array.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actions) {
|
if (actions) {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export function portal(element: HTMLElement, target: HTMLElement | null | undefined) {
|
export function portal(
|
||||||
|
element: HTMLElement,
|
||||||
|
target: HTMLElement | null | undefined
|
||||||
|
) {
|
||||||
if (target) {
|
if (target) {
|
||||||
target.append(element);
|
target.append(element);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,46 +40,46 @@ import Strong from "./Strong.svelte";
|
|||||||
import Ul from "./Ul.svelte";
|
import Ul from "./Ul.svelte";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
"a": A,
|
a: A,
|
||||||
"address": Address,
|
address: Address,
|
||||||
"article": Article,
|
article: Article,
|
||||||
"aside": Aside,
|
aside: Aside,
|
||||||
"b": B,
|
b: B,
|
||||||
"bdi": Bdi,
|
bdi: Bdi,
|
||||||
"bdo": Bdo,
|
bdo: Bdo,
|
||||||
"blockquote": Blockquote,
|
blockquote: Blockquote,
|
||||||
"button": Button,
|
button: Button,
|
||||||
"cite": Cite,
|
cite: Cite,
|
||||||
"code": Code,
|
code: Code,
|
||||||
"data": Data,
|
data: Data,
|
||||||
"datalist": Datalist,
|
datalist: Datalist,
|
||||||
"dd": Dd,
|
dd: Dd,
|
||||||
"dl": Dl,
|
dl: Dl,
|
||||||
"dt": Dt,
|
dt: Dt,
|
||||||
"div": Div,
|
div: Div,
|
||||||
"em": Em,
|
em: Em,
|
||||||
"footer": Footer,
|
footer: Footer,
|
||||||
"form": Form,
|
form: Form,
|
||||||
"h1": H1,
|
h1: H1,
|
||||||
"h2": H2,
|
h2: H2,
|
||||||
"h3": H3,
|
h3: H3,
|
||||||
"h4": H4,
|
h4: H4,
|
||||||
"h5": H5,
|
h5: H5,
|
||||||
"h6": H6,
|
h6: H6,
|
||||||
"header": Header,
|
header: Header,
|
||||||
"i": I,
|
i: I,
|
||||||
"input": Input,
|
input: Input,
|
||||||
"label": Label,
|
label: Label,
|
||||||
"li": Li,
|
li: Li,
|
||||||
"main": Main,
|
main: Main,
|
||||||
"nav": Nav,
|
nav: Nav,
|
||||||
"ol": Ol,
|
ol: Ol,
|
||||||
"p": P,
|
p: P,
|
||||||
"section": Section,
|
section: Section,
|
||||||
"span": Span,
|
span: Span,
|
||||||
"strong": Strong,
|
strong: Strong,
|
||||||
"ul": Ul,
|
ul: Ul,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SupportedElement = keyof typeof components;
|
export type SupportedElement = keyof typeof components;
|
||||||
export type SupportedAs = SupportedElement | SvelteComponent;
|
export type SupportedAs = SupportedElement | SvelteComponent;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,62 @@
|
|||||||
import { tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { fireEvent } from '@testing-library/svelte'
|
import { fireEvent } from "@testing-library/svelte";
|
||||||
|
|
||||||
export let Keys: Record<string, Partial<KeyboardEvent>> = {
|
export let Keys: Record<string, Partial<KeyboardEvent>> = {
|
||||||
Space: { key: ' ', keyCode: 32, charCode: 32 },
|
Space: { key: " ", keyCode: 32, charCode: 32 },
|
||||||
Enter: { key: 'Enter', keyCode: 13, charCode: 13 },
|
Enter: { key: "Enter", keyCode: 13, charCode: 13 },
|
||||||
Escape: { key: 'Escape', keyCode: 27, charCode: 27 },
|
Escape: { key: "Escape", keyCode: 27, charCode: 27 },
|
||||||
Backspace: { key: 'Backspace', keyCode: 8 },
|
Backspace: { key: "Backspace", keyCode: 8 },
|
||||||
|
|
||||||
ArrowLeft: { key: 'ArrowLeft', keyCode: 37 },
|
ArrowLeft: { key: "ArrowLeft", keyCode: 37 },
|
||||||
ArrowUp: { key: 'ArrowUp', keyCode: 38 },
|
ArrowUp: { key: "ArrowUp", keyCode: 38 },
|
||||||
ArrowRight: { key: 'ArrowRight', keyCode: 39 },
|
ArrowRight: { key: "ArrowRight", keyCode: 39 },
|
||||||
ArrowDown: { key: 'ArrowDown', keyCode: 40 },
|
ArrowDown: { key: "ArrowDown", keyCode: 40 },
|
||||||
|
|
||||||
Home: { key: 'Home', keyCode: 36 },
|
Home: { key: "Home", keyCode: 36 },
|
||||||
End: { key: 'End', keyCode: 35 },
|
End: { key: "End", keyCode: 35 },
|
||||||
|
|
||||||
PageUp: { key: 'PageUp', keyCode: 33 },
|
PageUp: { key: "PageUp", keyCode: 33 },
|
||||||
PageDown: { key: 'PageDown', keyCode: 34 },
|
PageDown: { key: "PageDown", keyCode: 34 },
|
||||||
|
|
||||||
Tab: { key: 'Tab', keyCode: 9, charCode: 9 },
|
Tab: { key: "Tab", keyCode: 9, charCode: 9 },
|
||||||
}
|
};
|
||||||
|
|
||||||
export function shift(event: Partial<KeyboardEvent>) {
|
export function shift(event: Partial<KeyboardEvent>) {
|
||||||
return { ...event, shiftKey: true }
|
return { ...event, shiftKey: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function word(input: string): Partial<KeyboardEvent>[] {
|
export function word(input: string): Partial<KeyboardEvent>[] {
|
||||||
return input.split('').map(key => ({ key }))
|
return input.split("").map((key) => ({ key }));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Default = Symbol()
|
let Default = Symbol();
|
||||||
let Ignore = Symbol()
|
let Ignore = Symbol();
|
||||||
|
|
||||||
let cancellations: Record<string | typeof Default, Record<string, Set<string>>> = {
|
let cancellations: Record<
|
||||||
|
string | typeof Default,
|
||||||
|
Record<string, Set<string>>
|
||||||
|
> = {
|
||||||
[Default]: {
|
[Default]: {
|
||||||
keydown: new Set(['keypress']),
|
keydown: new Set(["keypress"]),
|
||||||
keypress: new Set([]),
|
keypress: new Set([]),
|
||||||
keyup: new Set([]),
|
keyup: new Set([]),
|
||||||
},
|
},
|
||||||
[Keys.Enter.key!]: {
|
[Keys.Enter.key!]: {
|
||||||
keydown: new Set(['keypress', 'click']),
|
keydown: new Set(["keypress", "click"]),
|
||||||
keypress: new Set(['click']),
|
keypress: new Set(["click"]),
|
||||||
keyup: new Set([]),
|
keyup: new Set([]),
|
||||||
},
|
},
|
||||||
[Keys.Space.key!]: {
|
[Keys.Space.key!]: {
|
||||||
keydown: new Set(['keypress', 'click']),
|
keydown: new Set(["keypress", "click"]),
|
||||||
keypress: new Set([]),
|
keypress: new Set([]),
|
||||||
keyup: new Set(['click']),
|
keyup: new Set(["click"]),
|
||||||
},
|
},
|
||||||
[Keys.Tab.key!]: {
|
[Keys.Tab.key!]: {
|
||||||
keydown: new Set(['keypress', 'blur', 'focus']),
|
keydown: new Set(["keypress", "blur", "focus"]),
|
||||||
keypress: new Set([]),
|
keypress: new Set([]),
|
||||||
keyup: new Set([]),
|
keyup: new Set([]),
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
let order: Record<
|
let order: Record<
|
||||||
string | typeof Default,
|
string | typeof Default,
|
||||||
@@ -64,105 +67,115 @@ let order: Record<
|
|||||||
> = {
|
> = {
|
||||||
[Default]: [
|
[Default]: [
|
||||||
async function keydown(element, event) {
|
async function keydown(element, event) {
|
||||||
return await fireEvent.keyDown(element, event)
|
return await fireEvent.keyDown(element, event);
|
||||||
},
|
},
|
||||||
async function keypress(element, event) {
|
async function keypress(element, event) {
|
||||||
return await fireEvent.keyPress(element, event)
|
return await fireEvent.keyPress(element, event);
|
||||||
},
|
},
|
||||||
async function keyup(element, event) {
|
async function keyup(element, event) {
|
||||||
return await fireEvent.keyUp(element, event)
|
return await fireEvent.keyUp(element, event);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[Keys.Enter.key!]: [
|
[Keys.Enter.key!]: [
|
||||||
async function keydown(element, event) {
|
async function keydown(element, event) {
|
||||||
return await fireEvent.keyDown(element, event)
|
return await fireEvent.keyDown(element, event);
|
||||||
},
|
},
|
||||||
async function keypress(element, event) {
|
async function keypress(element, event) {
|
||||||
return await fireEvent.keyPress(element, event)
|
return await fireEvent.keyPress(element, event);
|
||||||
},
|
},
|
||||||
async function click(element, event) {
|
async function click(element, event) {
|
||||||
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
if (element instanceof HTMLButtonElement)
|
||||||
return Ignore
|
return await fireEvent.click(element, event);
|
||||||
|
return Ignore;
|
||||||
},
|
},
|
||||||
async function keyup(element, event) {
|
async function keyup(element, event) {
|
||||||
return await fireEvent.keyUp(element, event)
|
return await fireEvent.keyUp(element, event);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[Keys.Space.key!]: [
|
[Keys.Space.key!]: [
|
||||||
async function keydown(element, event) {
|
async function keydown(element, event) {
|
||||||
return await fireEvent.keyDown(element, event)
|
return await fireEvent.keyDown(element, event);
|
||||||
},
|
},
|
||||||
async function keypress(element, event) {
|
async function keypress(element, event) {
|
||||||
return await fireEvent.keyPress(element, event)
|
return await fireEvent.keyPress(element, event);
|
||||||
},
|
},
|
||||||
async function keyup(element, event) {
|
async function keyup(element, event) {
|
||||||
return await fireEvent.keyUp(element, event)
|
return await fireEvent.keyUp(element, event);
|
||||||
},
|
},
|
||||||
async function click(element, event) {
|
async function click(element, event) {
|
||||||
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
if (element instanceof HTMLButtonElement)
|
||||||
return Ignore
|
return await fireEvent.click(element, event);
|
||||||
|
return Ignore;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[Keys.Tab.key!]: [
|
[Keys.Tab.key!]: [
|
||||||
async function keydown(element, event) {
|
async function keydown(element, event) {
|
||||||
return await fireEvent.keyDown(element, event)
|
return await fireEvent.keyDown(element, event);
|
||||||
},
|
},
|
||||||
async function blurAndfocus(_element, event) {
|
async function blurAndfocus(_element, event) {
|
||||||
return focusNext(event)
|
return focusNext(event);
|
||||||
},
|
},
|
||||||
async function keyup(element, event) {
|
async function keyup(element, event) {
|
||||||
return await fireEvent.keyUp(element, event)
|
return await fireEvent.keyUp(element, event);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function type(events: Partial<KeyboardEvent>[], element = document.activeElement) {
|
export async function type(
|
||||||
jest.useFakeTimers()
|
events: Partial<KeyboardEvent>[],
|
||||||
|
element = document.activeElement
|
||||||
|
) {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
for (let event of events) {
|
for (let event of events) {
|
||||||
let skip = new Set()
|
let skip = new Set();
|
||||||
let actions = order[event.key!] ?? order[Default as any]
|
let actions = order[event.key!] ?? order[Default as any];
|
||||||
for (let action of actions) {
|
for (let action of actions) {
|
||||||
let checks = action.name.split('And')
|
let checks = action.name.split("And");
|
||||||
if (checks.some(check => skip.has(check))) continue
|
if (checks.some((check) => skip.has(check))) continue;
|
||||||
|
|
||||||
let result: boolean | typeof Ignore | Element = await action(element, {
|
let result: boolean | typeof Ignore | Element = await action(element, {
|
||||||
type: action.name,
|
type: action.name,
|
||||||
charCode: event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
|
charCode:
|
||||||
|
event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
|
||||||
...event,
|
...event,
|
||||||
})
|
});
|
||||||
if (result === Ignore) continue
|
if (result === Ignore) continue;
|
||||||
if (result instanceof Element) {
|
if (result instanceof Element) {
|
||||||
element = result
|
element = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = !result
|
let cancelled = !result;
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
let skippablesForKey = cancellations[event.key!] ?? cancellations[Default as any]
|
let skippablesForKey =
|
||||||
let skippables = skippablesForKey?.[action.name] ?? new Set()
|
cancellations[event.key!] ?? cancellations[Default as any];
|
||||||
|
let skippables = skippablesForKey?.[action.name] ?? new Set();
|
||||||
|
|
||||||
for (let skippable of skippables) skip.add(skippable)
|
for (let skippable of skippables) skip.add(skippable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to actually wait in our tests, so let's advance
|
// We don't want to actually wait in our tests, so let's advance
|
||||||
jest.runAllTimers()
|
jest.runAllTimers();
|
||||||
|
|
||||||
await tick()
|
await tick();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, type)
|
Error.captureStackTrace(err, type);
|
||||||
throw err
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
jest.useRealTimers()
|
jest.useRealTimers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function press(event: Partial<KeyboardEvent>, element = document.activeElement) {
|
export async function press(
|
||||||
return type([event], element)
|
event: Partial<KeyboardEvent>,
|
||||||
|
element = document.activeElement
|
||||||
|
) {
|
||||||
|
return type([event], element);
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MouseButton {
|
export enum MouseButton {
|
||||||
@@ -175,153 +188,158 @@ export async function click(
|
|||||||
button = MouseButton.Left
|
button = MouseButton.Left
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
let options = { button }
|
let options = { button };
|
||||||
|
|
||||||
if (button === MouseButton.Left) {
|
if (button === MouseButton.Left) {
|
||||||
// Cancel in pointerDown cancels mouseDown, mouseUp
|
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||||
let cancelled = !(await fireEvent.pointerDown(element, options))
|
let cancelled = !(await fireEvent.pointerDown(element, options));
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
await fireEvent.mouseDown(element, options)
|
await fireEvent.mouseDown(element, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure to trigger a `focus` event if the element is focusable, or within a focusable element
|
// Ensure to trigger a `focus` event if the element is focusable, or within a focusable element
|
||||||
let next: HTMLElement | null = element as HTMLElement | null
|
let next: HTMLElement | null = element as HTMLElement | null;
|
||||||
while (next !== null) {
|
while (next !== null) {
|
||||||
if (next.matches(focusableSelector)) {
|
if (next.matches(focusableSelector)) {
|
||||||
next.focus()
|
next.focus();
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
next = next.parentElement
|
next = next.parentElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fireEvent.pointerUp(element, options)
|
await fireEvent.pointerUp(element, options);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
await fireEvent.mouseUp(element, options)
|
await fireEvent.mouseUp(element, options);
|
||||||
}
|
}
|
||||||
await fireEvent.click(element, options)
|
await fireEvent.click(element, options);
|
||||||
} else if (button === MouseButton.Right) {
|
} else if (button === MouseButton.Right) {
|
||||||
// Cancel in pointerDown cancels mouseDown, mouseUp
|
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||||
let cancelled = !(await fireEvent.pointerDown(element, options))
|
let cancelled = !(await fireEvent.pointerDown(element, options));
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
await fireEvent.mouseDown(element, options)
|
await fireEvent.mouseDown(element, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only in Firefox:
|
// Only in Firefox:
|
||||||
await fireEvent.pointerUp(element, options)
|
await fireEvent.pointerUp(element, options);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
await fireEvent.mouseUp(element, options)
|
await fireEvent.mouseUp(element, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, click)
|
Error.captureStackTrace(err, click);
|
||||||
throw err
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function focus(element: Document | Element | Window | null) {
|
export async function focus(element: Document | Element | Window | null) {
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
await fireEvent.focus(element)
|
await fireEvent.focus(element);
|
||||||
|
|
||||||
await tick()
|
await tick();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, focus)
|
Error.captureStackTrace(err, focus);
|
||||||
throw err
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function mouseEnter(element: Document | Element | Window | null) {
|
export async function mouseEnter(element: Document | Element | Window | null) {
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
await fireEvent.pointerOver(element)
|
await fireEvent.pointerOver(element);
|
||||||
await fireEvent.pointerEnter(element)
|
await fireEvent.pointerEnter(element);
|
||||||
await fireEvent.mouseOver(element)
|
await fireEvent.mouseOver(element);
|
||||||
|
|
||||||
await tick()
|
await tick();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, mouseEnter)
|
Error.captureStackTrace(err, mouseEnter);
|
||||||
throw err
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mouseMove(element: Document | Element | Window | null) {
|
export async function mouseMove(element: Document | Element | Window | null) {
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
await fireEvent.pointerMove(element)
|
await fireEvent.pointerMove(element);
|
||||||
await fireEvent.mouseMove(element)
|
await fireEvent.mouseMove(element);
|
||||||
|
|
||||||
await tick()
|
await tick();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, mouseMove)
|
Error.captureStackTrace(err, mouseMove);
|
||||||
throw err
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mouseLeave(element: Document | Element | Window | null) {
|
export async function mouseLeave(element: Document | Element | Window | null) {
|
||||||
try {
|
try {
|
||||||
if (element === null) return expect(element).not.toBe(null)
|
if (element === null) return expect(element).not.toBe(null);
|
||||||
|
|
||||||
await fireEvent.pointerOut(element)
|
await fireEvent.pointerOut(element);
|
||||||
await fireEvent.pointerLeave(element)
|
await fireEvent.pointerLeave(element);
|
||||||
await fireEvent.mouseOut(element)
|
await fireEvent.mouseOut(element);
|
||||||
await fireEvent.mouseLeave(element)
|
await fireEvent.mouseLeave(element);
|
||||||
|
|
||||||
await tick()
|
await tick();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Error.captureStackTrace(err, mouseLeave)
|
Error.captureStackTrace(err, mouseLeave);
|
||||||
throw err
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
function focusNext(event: Partial<KeyboardEvent>) {
|
function focusNext(event: Partial<KeyboardEvent>) {
|
||||||
let direction = event.shiftKey ? -1 : +1
|
let direction = event.shiftKey ? -1 : +1;
|
||||||
let focusableElements = getFocusableElements()
|
let focusableElements = getFocusableElements();
|
||||||
let total = focusableElements.length
|
let total = focusableElements.length;
|
||||||
|
|
||||||
function innerFocusNext(offset = 0): Element {
|
function innerFocusNext(offset = 0): Element {
|
||||||
let currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
|
let currentIdx = focusableElements.indexOf(
|
||||||
let next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
|
document.activeElement as HTMLElement
|
||||||
|
);
|
||||||
|
let next = focusableElements[
|
||||||
|
(currentIdx + total + direction + offset) % total
|
||||||
|
] as HTMLElement;
|
||||||
|
|
||||||
if (next) next?.focus({ preventScroll: true })
|
if (next) next?.focus({ preventScroll: true });
|
||||||
|
|
||||||
if (next !== document.activeElement) return innerFocusNext(offset + direction)
|
if (next !== document.activeElement)
|
||||||
return next
|
return innerFocusNext(offset + direction);
|
||||||
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
return innerFocusNext()
|
return innerFocusNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credit:
|
// Credit:
|
||||||
// - https://stackoverflow.com/a/30753870
|
// - https://stackoverflow.com/a/30753870
|
||||||
let focusableSelector = [
|
let focusableSelector = [
|
||||||
'[contentEditable=true]',
|
"[contentEditable=true]",
|
||||||
'[tabindex]',
|
"[tabindex]",
|
||||||
'a[href]',
|
"a[href]",
|
||||||
'area[href]',
|
"area[href]",
|
||||||
'button:not([disabled])',
|
"button:not([disabled])",
|
||||||
'iframe',
|
"iframe",
|
||||||
'input:not([disabled])',
|
"input:not([disabled])",
|
||||||
'select:not([disabled])',
|
"select:not([disabled])",
|
||||||
'textarea:not([disabled])',
|
"textarea:not([disabled])",
|
||||||
]
|
]
|
||||||
.map(
|
.map(
|
||||||
process.env.NODE_ENV === 'test'
|
process.env.NODE_ENV === "test"
|
||||||
? // TODO: Remove this once JSDOM fixes the issue where an element that is
|
? // TODO: Remove this once JSDOM fixes the issue where an element that is
|
||||||
// "hidden" can be the document.activeElement, because this is not possible
|
// "hidden" can be the document.activeElement, because this is not possible
|
||||||
// in real browsers.
|
// in real browsers.
|
||||||
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
|
(selector) =>
|
||||||
: selector => `${selector}:not([tabindex='-1'])`
|
`${selector}:not([tabindex='-1']):not([style*='display: none'])`
|
||||||
|
: (selector) => `${selector}:not([tabindex='-1'])`
|
||||||
)
|
)
|
||||||
.join(',')
|
.join(",");
|
||||||
|
|
||||||
function getFocusableElements(container = document.body) {
|
function getFocusableElements(container = document.body) {
|
||||||
if (!container) return []
|
if (!container) return [];
|
||||||
return Array.from(container.querySelectorAll(focusableSelector))
|
return Array.from(container.querySelectorAll(focusableSelector));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
type FunctionPropertyNames<T> = {
|
type FunctionPropertyNames<T> = {
|
||||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
|
||||||
}[keyof T] &
|
}[keyof T] &
|
||||||
string
|
string;
|
||||||
|
|
||||||
export function suppressConsoleLogs<T extends unknown[]>(
|
export function suppressConsoleLogs<T extends unknown[]>(
|
||||||
cb: (...args: T) => unknown,
|
cb: (...args: T) => unknown,
|
||||||
type: FunctionPropertyNames<typeof global.console> = 'error'
|
type: FunctionPropertyNames<typeof global.console> = "error"
|
||||||
) {
|
) {
|
||||||
return (...args: T) => {
|
return (...args: T) => {
|
||||||
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
|
let spy = jest.spyOn(global.console, type).mockImplementation(jest.fn());
|
||||||
|
|
||||||
return new Promise<unknown>((resolve, reject) => {
|
return new Promise<unknown>((resolve, reject) => {
|
||||||
Promise.resolve(cb(...args)).then(resolve, reject)
|
Promise.resolve(cb(...args)).then(resolve, reject);
|
||||||
}).finally(() => spy.mockRestore())
|
}).finally(() => spy.mockRestore());
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import type { SupportedAs } from "$lib/internal/elements";
|
import type { SupportedAs } from "$lib/internal/elements";
|
||||||
export function resolveButtonType(props: { type?: string, as?: SupportedAs }, ref: HTMLElement | null | undefined): string | undefined {
|
export function resolveButtonType(
|
||||||
|
props: { type?: string; as?: SupportedAs },
|
||||||
|
ref: HTMLElement | null | undefined
|
||||||
|
): string | undefined {
|
||||||
if (props.type) return props.type;
|
if (props.type) return props.type;
|
||||||
let tag = props.as ?? "button";
|
let tag = props.as ?? "button";
|
||||||
if (typeof tag === "string" && tag.toLowerCase() === "button") return "button";
|
if (typeof tag === "string" && tag.toLowerCase() === "button")
|
||||||
|
return "button";
|
||||||
if (ref instanceof HTMLButtonElement) return "button";
|
if (ref instanceof HTMLButtonElement) return "button";
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,3 @@
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
files: (filepath) => {
|
files: (filepath) => {
|
||||||
return !filepath.endsWith(".test.ts");
|
return !filepath.endsWith(".test.ts");
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// hydrate the <div id="svelte"> element in src/app.html
|
// hydrate the <div id="svelte"> element in src/app.html
|
||||||
|
|||||||
Reference in New Issue
Block a user