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:
Ryan Gossiaux
2021-12-28 09:23:42 -10:00
parent 23a98b50ed
commit c99b74c089
21 changed files with 1212 additions and 1033 deletions

View File

@@ -2,9 +2,9 @@ name: Jest
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
workflow_dispatch:
jobs:

View File

@@ -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

View File

@@ -1,3 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
}
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
};

View File

@@ -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",
},
}
};

View File

@@ -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!

View File

@@ -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";

View File

@@ -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' },
})
assertDisclosurePanel({ state: DisclosureState.InvisibleUnmounted })
attributes: { id: "headlessui-disclosure-button-1" },
});
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 () => {

View File

@@ -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 () => {

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 }
],
[SwitchLabel,
{},
"Enable notifications"
]
]
]
})
[Switch, { checked: false, onChange: console.log }],
[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: [
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,
{},
]
})
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,
{},
],
[SwitchLabel,
{},
"The label"]
]
]
})
[ManagedSwitch, {}],
[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,
{},
],
[SwitchLabel,
{ passive: true },
"The label"]
]
]
})
[ManagedSwitch, {}],
[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 });
});
});

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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'])`
// "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'])`
)
.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));
}

View File

@@ -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());
};
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -5,4 +5,3 @@
>
<slot />
</a>

View File

@@ -19,7 +19,7 @@ const config = {
},
files: (filepath) => {
return !filepath.endsWith(".test.ts");
}
},
},
// hydrate the <div id="svelte"> element in src/app.html