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:
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -2,9 +2,9 @@ name: Jest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
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:
|
||||
|
||||
* 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 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.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
<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.
|
||||
|
||||
* 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
|
||||
<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 --->
|
||||
</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
|
||||
{people.map((person) => (
|
||||
@@ -64,12 +69,15 @@ Note the `.detail` that is needed to get the event value in the Svelte version.
|
||||
<ListboxOption
|
||||
...
|
||||
```
|
||||
|
||||
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
|
||||
* 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
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
|
||||
}
|
||||
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
'^.+\\.svelte$': ['svelte-jester', { preprocess: true }],
|
||||
'^.+\\.js$': 'babel-jest',
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
"^.+\\.svelte$": ["svelte-jester", { preprocess: true }],
|
||||
"^.+\\.js$": "babel-jest",
|
||||
"^.+\\.ts$": "ts-jest",
|
||||
},
|
||||
setupFilesAfterEnv: [
|
||||
'@testing-library/jest-dom/extend-expect',
|
||||
],
|
||||
setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
|
||||
testEnvironment: "jsdom",
|
||||
moduleFileExtensions: ['js', 'ts', 'svelte'],
|
||||
moduleFileExtensions: ["js", "ts", "svelte"],
|
||||
moduleNameMapper: {
|
||||
"\\$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 ManagedDialog from "./_ManagedDialog.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 P from "$lib/internal/elements/P.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 Transition from "$lib/components/transitions/TransitionRoot.svelte";
|
||||
import { tick } from "svelte";
|
||||
|
||||
let id = 0;
|
||||
jest.mock('../../hooks/use-id', () => {
|
||||
jest.mock("../../hooks/use-id", () => {
|
||||
return {
|
||||
useId: jest.fn(() => ++id),
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
// @ts-expect-error
|
||||
global.IntersectionObserver = class FakeIntersectionObserver {
|
||||
observe() { }
|
||||
disconnect() { }
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
|
||||
beforeEach(() => id = 0)
|
||||
afterAll(() => jest.restoreAllMocks())
|
||||
beforeEach(() => (id = 0));
|
||||
afterAll(() => jest.restoreAllMocks());
|
||||
|
||||
describe('Safe guards', () => {
|
||||
describe("Safe guards", () => {
|
||||
it.each([
|
||||
['DialogOverlay', DialogOverlay],
|
||||
['DialogTitle', DialogTitle],
|
||||
["DialogOverlay", DialogOverlay],
|
||||
["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) => {
|
||||
expect(() => render(Component)).toThrowError(
|
||||
`<${name} /> is missing a parent <Dialog /> component.`
|
||||
)
|
||||
expect.hasAssertions()
|
||||
);
|
||||
expect.hasAssertions();
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
it(
|
||||
'should be possible to render a Dialog without crashing',
|
||||
"should be possible to render a Dialog without crashing",
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Dialog,
|
||||
{ open: false, onClose: console.log },
|
||||
[
|
||||
[Button,
|
||||
{},
|
||||
"Trigger"],
|
||||
[Button, {}, "Trigger"],
|
||||
[DialogOverlay],
|
||||
[DialogTitle],
|
||||
[P, {}, "Contents"],
|
||||
[DialogDescription]
|
||||
]
|
||||
]
|
||||
})
|
||||
[DialogDescription],
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
assertDialog({ state: DialogState.InvisibleUnmounted })
|
||||
assertDialog({ state: DialogState.InvisibleUnmounted });
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
describe('Dialog', () => {
|
||||
describe("Rendering", () => {
|
||||
describe("Dialog", () => {
|
||||
it(
|
||||
'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',
|
||||
"should complain when the `open` and `onClose` prop are missing",
|
||||
suppressConsoleLogs(async () => {
|
||||
expect(() =>
|
||||
render(
|
||||
TestRenderer, {
|
||||
allProps: [
|
||||
Dialog,
|
||||
{ open: null, onClose: console.log, as: "div" },
|
||||
]
|
||||
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 () => {
|
||||
expect(() =>
|
||||
render(TestRenderer, {
|
||||
allProps: [Dialog, { open: null, onClose: console.log, as: "div" }],
|
||||
})
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"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!
|
||||
|
||||
|
||||
@@ -2,4 +2,3 @@ export { default as Dialog } from "./Dialog.svelte";
|
||||
export { default as DialogTitle } from "./DialogTitle.svelte";
|
||||
export { default as DialogOverlay } from "./DialogOverlay.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 { render } from "@testing-library/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 { Transition, TransitionChild } from "../transitions";
|
||||
import TransitionDebug from "./_TransitionDebug.svelte";
|
||||
|
||||
let id = 0;
|
||||
jest.mock('../../hooks/use-id', () => {
|
||||
jest.mock("../../hooks/use-id", () => {
|
||||
return {
|
||||
useId: jest.fn(() => ++id),
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
beforeEach(() => id = 0)
|
||||
afterAll(() => jest.restoreAllMocks())
|
||||
beforeEach(() => (id = 0));
|
||||
afterAll(() => jest.restoreAllMocks());
|
||||
|
||||
function nextFrame() {
|
||||
return new Promise<void>(resolve => {
|
||||
return new Promise<void>((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('Safe guards', () => {
|
||||
describe("Safe guards", () => {
|
||||
it.each([
|
||||
['DisclosureButton', DisclosureButton],
|
||||
['DisclosurePanel', DisclosurePanel],
|
||||
["DisclosureButton", DisclosureButton],
|
||||
["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) => {
|
||||
expect(() => render(Component)).toThrowError(
|
||||
`<${name} /> is missing a parent <Disclosure /> component.`
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
it(
|
||||
'should be possible to render a Disclosure without crashing',
|
||||
"should be possible to render a Disclosure without crashing",
|
||||
suppressConsoleLogs(async () => {
|
||||
render(
|
||||
TestRenderer, {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Disclosure,
|
||||
{},
|
||||
[
|
||||
[DisclosureButton, {}, "Trigger"],
|
||||
[DisclosurePanel, {}, "Contents"],
|
||||
]
|
||||
]
|
||||
})
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
assertDisclosureButton({
|
||||
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', () => {
|
||||
// it(
|
||||
// 'should be possible to render a Disclosure using a render prop',
|
||||
@@ -242,7 +248,7 @@ describe('Rendering', () => {
|
||||
// )
|
||||
// })
|
||||
|
||||
describe('DisclosureButton', () => {
|
||||
describe("DisclosureButton", () => {
|
||||
// it(
|
||||
// 'should be possible to render a DisclosureButton using a render prop',
|
||||
// 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 { render } from "@testing-library/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 { click, focus, Keys, MouseButton, mouseLeave, mouseMove, press, shift, type, word } from "$lib/test-utils/interactions";
|
||||
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 {
|
||||
click,
|
||||
focus,
|
||||
Keys,
|
||||
MouseButton,
|
||||
mouseLeave,
|
||||
mouseMove,
|
||||
press,
|
||||
shift,
|
||||
type,
|
||||
word,
|
||||
} from "$lib/test-utils/interactions";
|
||||
import { Transition } from "../transitions";
|
||||
import TransitionDebug from "$lib/components/disclosure/_TransitionDebug.svelte";
|
||||
import ManagedListbox from "./_ManagedListbox.svelte";
|
||||
@@ -215,9 +250,9 @@ describe('Rendering', () => {
|
||||
// assertListbox({ state: ListboxState.Visible })
|
||||
// })
|
||||
// )
|
||||
})
|
||||
});
|
||||
|
||||
describe('ListboxButton', () => {
|
||||
describe("ListboxButton", () => {
|
||||
// it(
|
||||
// 'should be possible to render a ListboxButton using a render prop',
|
||||
// suppressConsoleLogs(async () => {
|
||||
|
||||
@@ -2,4 +2,3 @@ export { default as RadioGroup } from "./RadioGroup.svelte";
|
||||
export { default as RadioGroupOption } from "./RadioGroupOption.svelte";
|
||||
export { default as RadioGroupLabel } from "../label/Label.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 { RadioGroup, RadioGroupLabel, RadioGroupOption } from ".";
|
||||
import { suppressConsoleLogs } from "$lib/test-utils/suppress-console-logs";
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { render } from "@testing-library/svelte";
|
||||
import TestRenderer from "../../test-utils/TestRenderer.svelte";
|
||||
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 Div from "$lib/internal/elements/Div.svelte";
|
||||
import Span from "$lib/internal/elements/Span.svelte";
|
||||
import ManagedSwitch from "./_ManagedSwitch.svelte";
|
||||
import { click, Keys, press } from "$lib/test-utils/interactions";
|
||||
jest.mock('../../hooks/use-id')
|
||||
jest.mock("../../hooks/use-id");
|
||||
|
||||
describe('Safe guards', () => {
|
||||
it('should be possible to render a Switch without crashing', () => {
|
||||
describe("Safe guards", () => {
|
||||
it("should be possible to render a Switch without crashing", () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ checked: false, onChange: console.log }
|
||||
]
|
||||
allProps: [Switch, { checked: false, onChange: console.log }],
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
describe("Rendering", () => {
|
||||
// TODO: handle these render prop (slot prop) tests
|
||||
|
||||
// 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' })
|
||||
// })
|
||||
|
||||
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, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ as: "span", checked: true, onChange: console.log },
|
||||
]
|
||||
allProps: [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, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ as: "span", checked: false, onChange: console.log },
|
||||
]
|
||||
allProps: [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, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ 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 () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ 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 () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ 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 () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Switch,
|
||||
{ checked: false, onChange: console.log, as: "div" },
|
||||
"Trigger"
|
||||
]
|
||||
"Trigger",
|
||||
],
|
||||
});
|
||||
|
||||
expect(getSwitch()).not.toHaveAttribute('type')
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(getSwitch()).not.toHaveAttribute("type");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Render composition', () => {
|
||||
it('should be possible to render a Switch.Group, Switch and Switch.Label', () => {
|
||||
describe("Render composition", () => {
|
||||
it("should be possible to render a Switch.Group, Switch and Switch.Label", () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[Switch,
|
||||
{ checked: false, onChange: console.log }
|
||||
[Switch, { 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, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[SwitchLabel,
|
||||
{},
|
||||
"Label B"],
|
||||
[Switch,
|
||||
{ checked: false, onChange: console.log },
|
||||
"Label A"]
|
||||
]
|
||||
]
|
||||
})
|
||||
[SwitchLabel, {}, "Label B"],
|
||||
[Switch, { checked: false, onChange: console.log }, "Label A"],
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||
// technologies.
|
||||
//
|
||||
// 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, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[Switch,
|
||||
{ checked: false, onChange: console.log },
|
||||
"Label A"],
|
||||
[SwitchLabel,
|
||||
{},
|
||||
"Label B"]
|
||||
]
|
||||
]
|
||||
})
|
||||
[Switch, { checked: false, onChange: console.log }, "Label A"],
|
||||
[SwitchLabel, {}, "Label B"],
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Warning! Using aria-label or aria-labelledby will hide any descendant content from assistive
|
||||
// technologies.
|
||||
//
|
||||
// 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, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[SwitchDescription,
|
||||
{},
|
||||
"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"],
|
||||
]
|
||||
]
|
||||
})
|
||||
[SwitchDescription, {}, "This is an important feature"],
|
||||
[Switch, { checked: false, onChange: console.log }],
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
assertSwitch({
|
||||
state: SwitchState.Off,
|
||||
label: 'Label A',
|
||||
description: 'This is an important feature',
|
||||
})
|
||||
})
|
||||
})
|
||||
description: "This is an important feature",
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyboard interactions', () => {
|
||||
describe('`Space` key', () => {
|
||||
it('should be possible to toggle the Switch with Space', async () => {
|
||||
it("should be possible to render a Switch.Group, Switch and Switch.Description (after the Switch)", () => {
|
||||
render(TestRenderer, {
|
||||
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
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Focus the switch
|
||||
getSwitch()?.focus()
|
||||
getSwitch()?.focus();
|
||||
|
||||
// Toggle
|
||||
await press(Keys.Space)
|
||||
await press(Keys.Space);
|
||||
|
||||
// Ensure state is on
|
||||
assertSwitch({ state: SwitchState.On })
|
||||
assertSwitch({ state: SwitchState.On });
|
||||
|
||||
// Toggle
|
||||
await press(Keys.Space)
|
||||
await press(Keys.Space);
|
||||
|
||||
// Ensure state is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
})
|
||||
})
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
});
|
||||
});
|
||||
|
||||
describe('`Enter` key', () => {
|
||||
it('should not be possible to use Enter to toggle the Switch', async () => {
|
||||
let handleChange = jest.fn()
|
||||
describe("`Enter` key", () => {
|
||||
it("should not be possible to use Enter to toggle the Switch", async () => {
|
||||
let handleChange = jest.fn();
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
ManagedSwitch,
|
||||
{ onChange: handleChange },
|
||||
]
|
||||
})
|
||||
|
||||
allProps: [ManagedSwitch, { onChange: handleChange }],
|
||||
});
|
||||
|
||||
// Ensure checkbox is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Focus the switch
|
||||
getSwitch()?.focus()
|
||||
getSwitch()?.focus();
|
||||
|
||||
// Try to toggle
|
||||
await press(Keys.Enter)
|
||||
await press(Keys.Enter);
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(handleChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('`Tab` key', () => {
|
||||
it('should be possible to tab away from the Switch', async () => {
|
||||
describe("`Tab` key", () => {
|
||||
it("should be possible to tab away from the Switch", async () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
Div,
|
||||
{},
|
||||
[
|
||||
[Switch,
|
||||
{ checked: false, onChange: console.log }],
|
||||
[Button, { id: "btn" }, "Other element"]
|
||||
]
|
||||
]
|
||||
})
|
||||
[Switch, { checked: false, onChange: console.log }],
|
||||
[Button, { id: "btn" }, "Other element"],
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Ensure checkbox is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Focus the switch
|
||||
getSwitch()?.focus()
|
||||
getSwitch()?.focus();
|
||||
|
||||
// Expect the switch to be active
|
||||
assertActiveElement(getSwitch())
|
||||
assertActiveElement(getSwitch());
|
||||
|
||||
// Toggle
|
||||
await press(Keys.Tab)
|
||||
await press(Keys.Tab);
|
||||
|
||||
// Expect the button to be active
|
||||
assertActiveElement(document.getElementById('btn'))
|
||||
})
|
||||
})
|
||||
})
|
||||
assertActiveElement(document.getElementById("btn"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mouse interactions', () => {
|
||||
it('should be possible to toggle the Switch with a click', async () => {
|
||||
describe("Mouse interactions", () => {
|
||||
it("should be possible to toggle the Switch with a click", async () => {
|
||||
render(TestRenderer, {
|
||||
allProps: [
|
||||
ManagedSwitch,
|
||||
{},
|
||||
]
|
||||
})
|
||||
allProps: [ManagedSwitch, {}],
|
||||
});
|
||||
|
||||
// Ensure checkbox is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Toggle
|
||||
await click(getSwitch())
|
||||
await click(getSwitch());
|
||||
|
||||
// Ensure state is on
|
||||
assertSwitch({ state: SwitchState.On })
|
||||
assertSwitch({ state: SwitchState.On });
|
||||
|
||||
// Toggle
|
||||
await click(getSwitch())
|
||||
await click(getSwitch());
|
||||
|
||||
// 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, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[ManagedSwitch,
|
||||
{},
|
||||
[ManagedSwitch, {}],
|
||||
[SwitchLabel, {}, "The label"],
|
||||
],
|
||||
[SwitchLabel,
|
||||
{},
|
||||
"The label"]
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
// Ensure checkbox is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Toggle
|
||||
await click(getSwitchLabel())
|
||||
await click(getSwitchLabel());
|
||||
|
||||
// Ensure the switch is focused
|
||||
assertActiveElement(getSwitch())
|
||||
assertActiveElement(getSwitch());
|
||||
|
||||
// Ensure state is on
|
||||
assertSwitch({ state: SwitchState.On })
|
||||
assertSwitch({ state: SwitchState.On });
|
||||
|
||||
// Toggle
|
||||
await click(getSwitchLabel())
|
||||
await click(getSwitchLabel());
|
||||
|
||||
// Ensure the switch is focused
|
||||
assertActiveElement(getSwitch())
|
||||
assertActiveElement(getSwitch());
|
||||
|
||||
// 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, {
|
||||
allProps: [
|
||||
SwitchGroup,
|
||||
{},
|
||||
[
|
||||
[ManagedSwitch,
|
||||
{},
|
||||
[ManagedSwitch, {}],
|
||||
[SwitchLabel, { passive: true }, "The label"],
|
||||
],
|
||||
[SwitchLabel,
|
||||
{ passive: true },
|
||||
"The label"]
|
||||
]
|
||||
]
|
||||
})
|
||||
],
|
||||
});
|
||||
|
||||
// Ensure checkbox is off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
|
||||
// Toggle
|
||||
await click(getSwitchLabel())
|
||||
await click(getSwitchLabel());
|
||||
|
||||
// Ensure state is still off
|
||||
assertSwitch({ state: SwitchState.Off })
|
||||
})
|
||||
})
|
||||
assertSwitch({ state: SwitchState.Off });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ export function useActions(
|
||||
return {
|
||||
update(actions: ActionArray) {
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export function portal(element: HTMLElement, target: HTMLElement | null | undefined) {
|
||||
export function portal(
|
||||
element: HTMLElement,
|
||||
target: HTMLElement | null | undefined
|
||||
) {
|
||||
if (target) {
|
||||
target.append(element);
|
||||
}
|
||||
|
||||
@@ -40,46 +40,46 @@ import Strong from "./Strong.svelte";
|
||||
import Ul from "./Ul.svelte";
|
||||
|
||||
const components = {
|
||||
"a": A,
|
||||
"address": Address,
|
||||
"article": Article,
|
||||
"aside": Aside,
|
||||
"b": B,
|
||||
"bdi": Bdi,
|
||||
"bdo": Bdo,
|
||||
"blockquote": Blockquote,
|
||||
"button": Button,
|
||||
"cite": Cite,
|
||||
"code": Code,
|
||||
"data": Data,
|
||||
"datalist": Datalist,
|
||||
"dd": Dd,
|
||||
"dl": Dl,
|
||||
"dt": Dt,
|
||||
"div": Div,
|
||||
"em": Em,
|
||||
"footer": Footer,
|
||||
"form": Form,
|
||||
"h1": H1,
|
||||
"h2": H2,
|
||||
"h3": H3,
|
||||
"h4": H4,
|
||||
"h5": H5,
|
||||
"h6": H6,
|
||||
"header": Header,
|
||||
"i": I,
|
||||
"input": Input,
|
||||
"label": Label,
|
||||
"li": Li,
|
||||
"main": Main,
|
||||
"nav": Nav,
|
||||
"ol": Ol,
|
||||
"p": P,
|
||||
"section": Section,
|
||||
"span": Span,
|
||||
"strong": Strong,
|
||||
"ul": Ul,
|
||||
}
|
||||
a: A,
|
||||
address: Address,
|
||||
article: Article,
|
||||
aside: Aside,
|
||||
b: B,
|
||||
bdi: Bdi,
|
||||
bdo: Bdo,
|
||||
blockquote: Blockquote,
|
||||
button: Button,
|
||||
cite: Cite,
|
||||
code: Code,
|
||||
data: Data,
|
||||
datalist: Datalist,
|
||||
dd: Dd,
|
||||
dl: Dl,
|
||||
dt: Dt,
|
||||
div: Div,
|
||||
em: Em,
|
||||
footer: Footer,
|
||||
form: Form,
|
||||
h1: H1,
|
||||
h2: H2,
|
||||
h3: H3,
|
||||
h4: H4,
|
||||
h5: H5,
|
||||
h6: H6,
|
||||
header: Header,
|
||||
i: I,
|
||||
input: Input,
|
||||
label: Label,
|
||||
li: Li,
|
||||
main: Main,
|
||||
nav: Nav,
|
||||
ol: Ol,
|
||||
p: P,
|
||||
section: Section,
|
||||
span: Span,
|
||||
strong: Strong,
|
||||
ul: Ul,
|
||||
};
|
||||
|
||||
export type SupportedElement = keyof typeof components;
|
||||
export type SupportedAs = SupportedElement | SvelteComponent;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,59 +1,62 @@
|
||||
import { tick } from "svelte";
|
||||
import { fireEvent } from '@testing-library/svelte'
|
||||
import { fireEvent } from "@testing-library/svelte";
|
||||
|
||||
export let Keys: Record<string, Partial<KeyboardEvent>> = {
|
||||
Space: { key: ' ', keyCode: 32, charCode: 32 },
|
||||
Enter: { key: 'Enter', keyCode: 13, charCode: 13 },
|
||||
Escape: { key: 'Escape', keyCode: 27, charCode: 27 },
|
||||
Backspace: { key: 'Backspace', keyCode: 8 },
|
||||
Space: { key: " ", keyCode: 32, charCode: 32 },
|
||||
Enter: { key: "Enter", keyCode: 13, charCode: 13 },
|
||||
Escape: { key: "Escape", keyCode: 27, charCode: 27 },
|
||||
Backspace: { key: "Backspace", keyCode: 8 },
|
||||
|
||||
ArrowLeft: { key: 'ArrowLeft', keyCode: 37 },
|
||||
ArrowUp: { key: 'ArrowUp', keyCode: 38 },
|
||||
ArrowRight: { key: 'ArrowRight', keyCode: 39 },
|
||||
ArrowDown: { key: 'ArrowDown', keyCode: 40 },
|
||||
ArrowLeft: { key: "ArrowLeft", keyCode: 37 },
|
||||
ArrowUp: { key: "ArrowUp", keyCode: 38 },
|
||||
ArrowRight: { key: "ArrowRight", keyCode: 39 },
|
||||
ArrowDown: { key: "ArrowDown", keyCode: 40 },
|
||||
|
||||
Home: { key: 'Home', keyCode: 36 },
|
||||
End: { key: 'End', keyCode: 35 },
|
||||
Home: { key: "Home", keyCode: 36 },
|
||||
End: { key: "End", keyCode: 35 },
|
||||
|
||||
PageUp: { key: 'PageUp', keyCode: 33 },
|
||||
PageDown: { key: 'PageDown', keyCode: 34 },
|
||||
PageUp: { key: "PageUp", keyCode: 33 },
|
||||
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>) {
|
||||
return { ...event, shiftKey: true }
|
||||
return { ...event, shiftKey: true };
|
||||
}
|
||||
|
||||
export function word(input: string): Partial<KeyboardEvent>[] {
|
||||
return input.split('').map(key => ({ key }))
|
||||
return input.split("").map((key) => ({ key }));
|
||||
}
|
||||
|
||||
let Default = Symbol()
|
||||
let Ignore = Symbol()
|
||||
let Default = 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]: {
|
||||
keydown: new Set(['keypress']),
|
||||
keydown: new Set(["keypress"]),
|
||||
keypress: new Set([]),
|
||||
keyup: new Set([]),
|
||||
},
|
||||
[Keys.Enter.key!]: {
|
||||
keydown: new Set(['keypress', 'click']),
|
||||
keypress: new Set(['click']),
|
||||
keydown: new Set(["keypress", "click"]),
|
||||
keypress: new Set(["click"]),
|
||||
keyup: new Set([]),
|
||||
},
|
||||
[Keys.Space.key!]: {
|
||||
keydown: new Set(['keypress', 'click']),
|
||||
keydown: new Set(["keypress", "click"]),
|
||||
keypress: new Set([]),
|
||||
keyup: new Set(['click']),
|
||||
keyup: new Set(["click"]),
|
||||
},
|
||||
[Keys.Tab.key!]: {
|
||||
keydown: new Set(['keypress', 'blur', 'focus']),
|
||||
keydown: new Set(["keypress", "blur", "focus"]),
|
||||
keypress: new Set([]),
|
||||
keyup: new Set([]),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let order: Record<
|
||||
string | typeof Default,
|
||||
@@ -64,105 +67,115 @@ let order: Record<
|
||||
> = {
|
||||
[Default]: [
|
||||
async function keydown(element, event) {
|
||||
return await fireEvent.keyDown(element, event)
|
||||
return await fireEvent.keyDown(element, event);
|
||||
},
|
||||
async function keypress(element, event) {
|
||||
return await fireEvent.keyPress(element, event)
|
||||
return await fireEvent.keyPress(element, event);
|
||||
},
|
||||
async function keyup(element, event) {
|
||||
return await fireEvent.keyUp(element, event)
|
||||
return await fireEvent.keyUp(element, event);
|
||||
},
|
||||
],
|
||||
[Keys.Enter.key!]: [
|
||||
async function keydown(element, event) {
|
||||
return await fireEvent.keyDown(element, event)
|
||||
return await fireEvent.keyDown(element, event);
|
||||
},
|
||||
async function keypress(element, event) {
|
||||
return await fireEvent.keyPress(element, event)
|
||||
return await fireEvent.keyPress(element, event);
|
||||
},
|
||||
async function click(element, event) {
|
||||
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
||||
return Ignore
|
||||
if (element instanceof HTMLButtonElement)
|
||||
return await fireEvent.click(element, event);
|
||||
return Ignore;
|
||||
},
|
||||
async function keyup(element, event) {
|
||||
return await fireEvent.keyUp(element, event)
|
||||
return await fireEvent.keyUp(element, event);
|
||||
},
|
||||
],
|
||||
[Keys.Space.key!]: [
|
||||
async function keydown(element, event) {
|
||||
return await fireEvent.keyDown(element, event)
|
||||
return await fireEvent.keyDown(element, event);
|
||||
},
|
||||
async function keypress(element, event) {
|
||||
return await fireEvent.keyPress(element, event)
|
||||
return await fireEvent.keyPress(element, event);
|
||||
},
|
||||
async function keyup(element, event) {
|
||||
return await fireEvent.keyUp(element, event)
|
||||
return await fireEvent.keyUp(element, event);
|
||||
},
|
||||
async function click(element, event) {
|
||||
if (element instanceof HTMLButtonElement) return await fireEvent.click(element, event)
|
||||
return Ignore
|
||||
if (element instanceof HTMLButtonElement)
|
||||
return await fireEvent.click(element, event);
|
||||
return Ignore;
|
||||
},
|
||||
],
|
||||
[Keys.Tab.key!]: [
|
||||
async function keydown(element, event) {
|
||||
return await fireEvent.keyDown(element, event)
|
||||
return await fireEvent.keyDown(element, event);
|
||||
},
|
||||
async function blurAndfocus(_element, event) {
|
||||
return focusNext(event)
|
||||
return focusNext(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) {
|
||||
jest.useFakeTimers()
|
||||
export async function type(
|
||||
events: Partial<KeyboardEvent>[],
|
||||
element = document.activeElement
|
||||
) {
|
||||
jest.useFakeTimers();
|
||||
|
||||
try {
|
||||
if (element === null) return expect(element).not.toBe(null)
|
||||
if (element === null) return expect(element).not.toBe(null);
|
||||
|
||||
for (let event of events) {
|
||||
let skip = new Set()
|
||||
let actions = order[event.key!] ?? order[Default as any]
|
||||
let skip = new Set();
|
||||
let actions = order[event.key!] ?? order[Default as any];
|
||||
for (let action of actions) {
|
||||
let checks = action.name.split('And')
|
||||
if (checks.some(check => skip.has(check))) continue
|
||||
let checks = action.name.split("And");
|
||||
if (checks.some((check) => skip.has(check))) continue;
|
||||
|
||||
let result: boolean | typeof Ignore | Element = await action(element, {
|
||||
type: action.name,
|
||||
charCode: event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
|
||||
charCode:
|
||||
event.key?.length === 1 ? event.key?.charCodeAt(0) : undefined,
|
||||
...event,
|
||||
})
|
||||
if (result === Ignore) continue
|
||||
});
|
||||
if (result === Ignore) continue;
|
||||
if (result instanceof Element) {
|
||||
element = result
|
||||
element = result;
|
||||
}
|
||||
|
||||
let cancelled = !result
|
||||
let cancelled = !result;
|
||||
if (cancelled) {
|
||||
let skippablesForKey = cancellations[event.key!] ?? cancellations[Default as any]
|
||||
let skippables = skippablesForKey?.[action.name] ?? new Set()
|
||||
let skippablesForKey =
|
||||
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
|
||||
jest.runAllTimers()
|
||||
jest.runAllTimers();
|
||||
|
||||
await tick()
|
||||
await tick();
|
||||
} catch (err: any) {
|
||||
Error.captureStackTrace(err, type)
|
||||
throw err
|
||||
Error.captureStackTrace(err, type);
|
||||
throw err;
|
||||
} finally {
|
||||
jest.useRealTimers()
|
||||
jest.useRealTimers();
|
||||
}
|
||||
}
|
||||
|
||||
export async function press(event: Partial<KeyboardEvent>, element = document.activeElement) {
|
||||
return type([event], element)
|
||||
export async function press(
|
||||
event: Partial<KeyboardEvent>,
|
||||
element = document.activeElement
|
||||
) {
|
||||
return type([event], element);
|
||||
}
|
||||
|
||||
export enum MouseButton {
|
||||
@@ -175,153 +188,158 @@ export async function click(
|
||||
button = MouseButton.Left
|
||||
) {
|
||||
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) {
|
||||
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||
let cancelled = !(await fireEvent.pointerDown(element, options))
|
||||
let cancelled = !(await fireEvent.pointerDown(element, options));
|
||||
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
|
||||
let next: HTMLElement | null = element as HTMLElement | null
|
||||
let next: HTMLElement | null = element as HTMLElement | null;
|
||||
while (next !== null) {
|
||||
if (next.matches(focusableSelector)) {
|
||||
next.focus()
|
||||
break
|
||||
next.focus();
|
||||
break;
|
||||
}
|
||||
next = next.parentElement
|
||||
next = next.parentElement;
|
||||
}
|
||||
|
||||
await fireEvent.pointerUp(element, options)
|
||||
await fireEvent.pointerUp(element, options);
|
||||
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) {
|
||||
// Cancel in pointerDown cancels mouseDown, mouseUp
|
||||
let cancelled = !(await fireEvent.pointerDown(element, options))
|
||||
let cancelled = !(await fireEvent.pointerDown(element, options));
|
||||
if (!cancelled) {
|
||||
await fireEvent.mouseDown(element, options)
|
||||
await fireEvent.mouseDown(element, options);
|
||||
}
|
||||
|
||||
// Only in Firefox:
|
||||
await fireEvent.pointerUp(element, options)
|
||||
await fireEvent.pointerUp(element, options);
|
||||
if (!cancelled) {
|
||||
await fireEvent.mouseUp(element, options)
|
||||
await fireEvent.mouseUp(element, options);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
Error.captureStackTrace(err, click)
|
||||
throw err
|
||||
Error.captureStackTrace(err, click);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function focus(element: Document | Element | Window | null) {
|
||||
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) {
|
||||
Error.captureStackTrace(err, focus)
|
||||
throw err
|
||||
Error.captureStackTrace(err, focus);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
export async function mouseEnter(element: Document | Element | Window | null) {
|
||||
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.pointerEnter(element)
|
||||
await fireEvent.mouseOver(element)
|
||||
await fireEvent.pointerOver(element);
|
||||
await fireEvent.pointerEnter(element);
|
||||
await fireEvent.mouseOver(element);
|
||||
|
||||
await tick()
|
||||
await tick();
|
||||
} catch (err: any) {
|
||||
Error.captureStackTrace(err, mouseEnter)
|
||||
throw err
|
||||
Error.captureStackTrace(err, mouseEnter);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function mouseMove(element: Document | Element | Window | null) {
|
||||
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.mouseMove(element)
|
||||
await fireEvent.pointerMove(element);
|
||||
await fireEvent.mouseMove(element);
|
||||
|
||||
await tick()
|
||||
await tick();
|
||||
} catch (err: any) {
|
||||
Error.captureStackTrace(err, mouseMove)
|
||||
throw err
|
||||
Error.captureStackTrace(err, mouseMove);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function mouseLeave(element: Document | Element | Window | null) {
|
||||
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.pointerLeave(element)
|
||||
await fireEvent.mouseOut(element)
|
||||
await fireEvent.mouseLeave(element)
|
||||
await fireEvent.pointerOut(element);
|
||||
await fireEvent.pointerLeave(element);
|
||||
await fireEvent.mouseOut(element);
|
||||
await fireEvent.mouseLeave(element);
|
||||
|
||||
await tick()
|
||||
await tick();
|
||||
} catch (err: any) {
|
||||
Error.captureStackTrace(err, mouseLeave)
|
||||
throw err
|
||||
Error.captureStackTrace(err, mouseLeave);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
function focusNext(event: Partial<KeyboardEvent>) {
|
||||
let direction = event.shiftKey ? -1 : +1
|
||||
let focusableElements = getFocusableElements()
|
||||
let total = focusableElements.length
|
||||
let direction = event.shiftKey ? -1 : +1;
|
||||
let focusableElements = getFocusableElements();
|
||||
let total = focusableElements.length;
|
||||
|
||||
function innerFocusNext(offset = 0): Element {
|
||||
let currentIdx = focusableElements.indexOf(document.activeElement as HTMLElement)
|
||||
let next = focusableElements[(currentIdx + total + direction + offset) % total] as HTMLElement
|
||||
let currentIdx = focusableElements.indexOf(
|
||||
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)
|
||||
return next
|
||||
if (next !== document.activeElement)
|
||||
return innerFocusNext(offset + direction);
|
||||
return next;
|
||||
}
|
||||
|
||||
return innerFocusNext()
|
||||
return innerFocusNext();
|
||||
}
|
||||
|
||||
// Credit:
|
||||
// - https://stackoverflow.com/a/30753870
|
||||
let focusableSelector = [
|
||||
'[contentEditable=true]',
|
||||
'[tabindex]',
|
||||
'a[href]',
|
||||
'area[href]',
|
||||
'button:not([disabled])',
|
||||
'iframe',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
"[contentEditable=true]",
|
||||
"[tabindex]",
|
||||
"a[href]",
|
||||
"area[href]",
|
||||
"button:not([disabled])",
|
||||
"iframe",
|
||||
"input:not([disabled])",
|
||||
"select:not([disabled])",
|
||||
"textarea:not([disabled])",
|
||||
]
|
||||
.map(
|
||||
process.env.NODE_ENV === 'test'
|
||||
process.env.NODE_ENV === "test"
|
||||
? // TODO: Remove this once JSDOM fixes the issue where an element that is
|
||||
// "hidden" can be the document.activeElement, because this is not possible
|
||||
// in real browsers.
|
||||
selector => `${selector}:not([tabindex='-1']):not([style*='display: none'])`
|
||||
: selector => `${selector}:not([tabindex='-1'])`
|
||||
(selector) =>
|
||||
`${selector}:not([tabindex='-1']):not([style*='display: none'])`
|
||||
: (selector) => `${selector}:not([tabindex='-1'])`
|
||||
)
|
||||
.join(',')
|
||||
.join(",");
|
||||
|
||||
function getFocusableElements(container = document.body) {
|
||||
if (!container) return []
|
||||
return Array.from(container.querySelectorAll(focusableSelector))
|
||||
if (!container) return [];
|
||||
return Array.from(container.querySelectorAll(focusableSelector));
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
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] &
|
||||
string
|
||||
string;
|
||||
|
||||
export function suppressConsoleLogs<T extends unknown[]>(
|
||||
cb: (...args: T) => unknown,
|
||||
type: FunctionPropertyNames<typeof global.console> = 'error'
|
||||
type: FunctionPropertyNames<typeof global.console> = "error"
|
||||
) {
|
||||
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) => {
|
||||
Promise.resolve(cb(...args)).then(resolve, reject)
|
||||
}).finally(() => spy.mockRestore())
|
||||
}
|
||||
Promise.resolve(cb(...args)).then(resolve, reject);
|
||||
}).finally(() => spy.mockRestore());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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;
|
||||
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";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function transition(
|
||||
done?: (reason: Reason) => void
|
||||
) {
|
||||
let d = disposables();
|
||||
let _done = done !== undefined ? once(done) : () => { };
|
||||
let _done = done !== undefined ? once(done) : () => {};
|
||||
|
||||
removeClasses(node, ...entered);
|
||||
addClasses(node, ...base, ...from);
|
||||
|
||||
@@ -5,4 +5,3 @@
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const config = {
|
||||
},
|
||||
files: (filepath) => {
|
||||
return !filepath.endsWith(".test.ts");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// hydrate the <div id="svelte"> element in src/app.html
|
||||
|
||||
Reference in New Issue
Block a user