@purpurds/toggle 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ import { MutableRefObject } from "react";
2
+ import { ToggleProps } from "./toggle";
3
+ export declare const useToggleDrag: ({ checked, onChange }: Pick<ToggleProps, "checked" | "onChange">) => {
4
+ trackRef: MutableRefObject<HTMLDivElement | null>;
5
+ thumbRef: import("react").RefObject<HTMLSpanElement>;
6
+ isDragging: boolean;
7
+ bounds: {
8
+ left: number;
9
+ right: number;
10
+ };
11
+ position: number;
12
+ onDrag: ({ x }: {
13
+ x: number;
14
+ }) => void;
15
+ onStop: () => void;
16
+ onChangeWithDrag: () => void;
17
+ };
18
+ //# sourceMappingURL=useToggleDrag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useToggleDrag.d.ts","sourceRoot":"","sources":["../src/useToggleDrag.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAqC,MAAM,OAAO,CAAC;AAC5E,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,eAAO,MAAM,aAAa,0BAA2B,KAAK,WAAW,EAAE,SAAS,GAAG,UAAU,CAAC;;;;;;;;;;WAqBhE,MAAM;;;;CAiCnC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@purpurds/toggle",
3
+ "version": "3.0.0",
4
+ "license": "AGPL-3.0-only",
5
+ "main": "./dist/toggle.cjs.js",
6
+ "types": "./dist/toggle.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "require": "./dist/toggle.cjs.js",
10
+ "systemjs": "./dist/toggle.system.js",
11
+ "types": "./dist/toggle.d.ts",
12
+ "default": "./dist/toggle.es.js"
13
+ },
14
+ "./styles": "./dist/styles.css"
15
+ },
16
+ "source": "src/toggle.tsx",
17
+ "dependencies": {
18
+ "@radix-ui/react-switch": "~1.0.3",
19
+ "classnames": "~2.5.0",
20
+ "@storybook/client-api": "~7.6.0",
21
+ "@purpurds/label": "3.0.0",
22
+ "@purpurds/paragraph": "3.0.0",
23
+ "@purpurds/tokens": "3.0.0",
24
+ "@purpurds/icon": "3.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@rushstack/eslint-patch": "~1.7.0",
28
+ "@storybook/blocks": "~7.6.0",
29
+ "@storybook/react": "~7.6.0",
30
+ "@telia/base-rig": "~8.2.0",
31
+ "@telia/react-rig": "~3.2.0",
32
+ "@testing-library/dom": "~9.3.3",
33
+ "@testing-library/jest-dom": "~6.3.0",
34
+ "@testing-library/react": "~14.1.2",
35
+ "@types/node": "18",
36
+ "@types/react-dom": "~18.2.17",
37
+ "@types/react": "~18.2.42",
38
+ "eslint-plugin-testing-library": "~6.2.0",
39
+ "eslint": "~8.56.0",
40
+ "jsdom": "~22.1.0",
41
+ "lint-staged": "~10.5.3",
42
+ "prettier": "~2.8.8",
43
+ "react-dom": "~18.2.0",
44
+ "react": "~18.2.0",
45
+ "typescript": "~5.2.2",
46
+ "vite": "~5.0.6",
47
+ "vitest": "~1.2.0",
48
+ "@purpurds/component-rig": "1.0.0"
49
+ },
50
+ "scripts": {
51
+ "build:dev": "vite",
52
+ "build:watch": "vite build --watch",
53
+ "build": "rm -rf dist && vite build && vite build --mode systemjs",
54
+ "ci:build": "rushx build",
55
+ "coverage": "vitest run --coverage",
56
+ "lint:fix": "eslint . --fix",
57
+ "lint": "lint-staged --no-stash 2>&1",
58
+ "sbdev": "rush sbdev",
59
+ "test:unit": "vitest run --passWithNoTests",
60
+ "test:watch": "vitest --watch",
61
+ "test": "rushx test:unit",
62
+ "typecheck": "tsc -p ./tsconfig.json"
63
+ }
64
+ }
package/readme.mdx ADDED
@@ -0,0 +1,136 @@
1
+ import { Meta, Stories, ArgTypes, Primary, Subtitle } from "@storybook/blocks";
2
+
3
+ import * as ToggleStories from "./src/toggle.stories";
4
+ import packageInfo from "./package.json";
5
+
6
+ <Meta name="Docs" title="Components/Toggle" of={ToggleStories} />
7
+
8
+ # Toggle
9
+
10
+ <Subtitle>Version {packageInfo.version}</Subtitle>
11
+
12
+ ### Showcase
13
+
14
+ <Primary />
15
+
16
+ ### Properties
17
+
18
+ <ArgTypes />
19
+
20
+ ### Installation
21
+
22
+ #### Via NPM
23
+
24
+ Add the dependency to your consumer app like `"@purpurds/toggle": "x.y.z"`
25
+
26
+ #### From outside the monorepo (build-time)
27
+
28
+ To install this package, you need to setup access to the artifactory. [Click here to go to the guide on how to do that](https://github.com/telia-company/jfrog-documentation/blob/main/doc/JFrog/JFrog_Onboarding.md#getting-access-to-artifactory-and-other-jfrog-applications).
29
+
30
+ ---
31
+
32
+ In MyApp.tsx
33
+
34
+ ```tsx
35
+ import "@purpurds/tokens/index.css";
36
+ ```
37
+
38
+ and
39
+
40
+ ```tsx
41
+ import "@purpurds/toggle/styles";
42
+ ```
43
+
44
+ ### Examples
45
+
46
+ In MyComponent.tsx
47
+
48
+ #### Controlled.
49
+
50
+ ---
51
+
52
+ For when you have to controll and use the state of the toggle.
53
+
54
+ ```tsx
55
+ import { Toggle } from "@purpurds/toggle";
56
+
57
+ export const MyComponent = () => {
58
+ const [isChecked, setIsChecked] = useState(false);
59
+ return (
60
+ <div>
61
+ <Toggle
62
+ id="my-toggle"
63
+ checked={isChecked}
64
+ onChange={setIsChecked}
65
+ label="My toggle"
66
+ labelPosition="right"
67
+ />
68
+ </div>
69
+ );
70
+ };
71
+ ```
72
+
73
+ #### Uncontrolled
74
+
75
+ ---
76
+
77
+ For when you don't have to controll state of the toggle, e.g. when in a form.
78
+
79
+ _NOTE: do not use toggles instead of checkboxes or radio buttons!_
80
+
81
+ ```tsx
82
+ import { Toggle } from "@purpurds/toggle";
83
+
84
+ export const MyComponent = () => {
85
+ /**
86
+ * Toggle will render checked, and handle it's state itself.
87
+ *
88
+ * Since it is rendered in a form, it will render a checkbox input under the hood
89
+ * that will reflect its value and state.
90
+ */
91
+ return (
92
+ <form>
93
+ <Toggle id="my-toggle" defaultChecked label="My uncontrolled toggle" />
94
+ </form>
95
+ );
96
+ };
97
+ ```
98
+
99
+ #### With custom label (not recommended).
100
+
101
+ ---
102
+
103
+ Use the `aria-labelledby` property and pass the id of the label.
104
+
105
+ ```tsx
106
+ import { Toggle } from "@purpurds/toggle";
107
+
108
+ export const MyComponent = () => {
109
+ return (
110
+ <div>
111
+ <label id="my-custom-label" htmlFor="my-toggle">
112
+ Custom label
113
+ </label>
114
+ <Toggle aria-labeledby="my-custom-label" id="my-toggle" {...otherProps} />
115
+ </div>
116
+ );
117
+ };
118
+ ```
119
+
120
+ #### Without label (not recommended).
121
+
122
+ ---
123
+
124
+ If there should be no label at all, use the `aria-label` to label the toggle for screen readers.
125
+
126
+ ```tsx
127
+ import { Toggle } from "@purpurds/toggle";
128
+
129
+ export const MyComponent = () => {
130
+ return (
131
+ <div>
132
+ <Toggle aria-label="Toggle some awesome stuff!" id="my-toggle" {...otherProps} />
133
+ </div>
134
+ );
135
+ };
136
+ ```
@@ -0,0 +1,127 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ CSSProperties,
5
+ isValidElement,
6
+ ReactElement,
7
+ ReactNode,
8
+ useCallback,
9
+ useEffect,
10
+ useState,
11
+ } from "react";
12
+
13
+ export type DraggableXProps = {
14
+ children?: ReactNode;
15
+ disabled?: boolean;
16
+ position: number;
17
+ bounds: { left: number; right: number };
18
+ onStart?: () => void;
19
+ onDrag?: (args: { x: number }) => void;
20
+ onStop?: () => void;
21
+ style?: CSSProperties;
22
+ };
23
+
24
+ const isTouchEvent = (e: MouseEvent | TouchEvent): e is TouchEvent => e && "touches" in e;
25
+
26
+ /**
27
+ * Bare minimum to make the toggle draggable.
28
+ * It is created for usage in the toggle only but could easily be extracted and used elsewhere.
29
+ *
30
+ * It is called `DraggableX` since it's only for dragging along the x-axis.
31
+ */
32
+ export const DraggableX = ({
33
+ children,
34
+ disabled,
35
+ onStart,
36
+ onDrag,
37
+ onStop,
38
+ bounds,
39
+ position,
40
+ style,
41
+ }: DraggableXProps) => {
42
+ const [dragX, setDragX] = useState<number | undefined>(undefined);
43
+ const [dragStartX, setDragStartX] = useState<number | undefined>(undefined);
44
+ const [isDragging, setIsDragging] = useState(false);
45
+
46
+ const startDrag = (e: React.MouseEvent | React.TouchEvent) => {
47
+ if (!disabled) {
48
+ e.preventDefault();
49
+ const clientX = isTouchEvent(e.nativeEvent)
50
+ ? e.nativeEvent.touches[0].clientX
51
+ : e.nativeEvent.clientX;
52
+ setDragStartX(clientX);
53
+ onStart?.();
54
+ }
55
+ };
56
+
57
+ const onMouseMove = useCallback(
58
+ (e: MouseEvent | TouchEvent) => {
59
+ if (typeof dragStartX !== "number") {
60
+ return;
61
+ }
62
+
63
+ const clientX = isTouchEvent(e) ? e.touches[0].clientX : e.clientX;
64
+ const dragDelta = clientX - dragStartX;
65
+
66
+ if (!dragDelta) {
67
+ return;
68
+ }
69
+
70
+ const nextDragX = position + dragDelta;
71
+ if (!isDragging && Math.abs(nextDragX)) {
72
+ setIsDragging(true);
73
+ }
74
+
75
+ const nextDragXBounded = (() => {
76
+ if (nextDragX > bounds.right) {
77
+ return bounds.right;
78
+ }
79
+
80
+ if (nextDragX < bounds.left) {
81
+ return bounds.left;
82
+ }
83
+
84
+ return nextDragX;
85
+ })();
86
+
87
+ setDragX(nextDragXBounded);
88
+ onDrag?.({ x: nextDragXBounded });
89
+ },
90
+ [onDrag, setDragX, isDragging, dragStartX]
91
+ );
92
+
93
+ const onMouseUp = useCallback(() => {
94
+ setDragStartX(undefined);
95
+ setDragX(undefined);
96
+ setIsDragging(false);
97
+ onStop?.();
98
+ }, [onStop, setDragStartX]);
99
+
100
+ useEffect(() => {
101
+ window.addEventListener("mousemove", onMouseMove);
102
+ window.addEventListener("touchmove", onMouseMove);
103
+
104
+ return () => {
105
+ window.removeEventListener("mousemove", onMouseMove);
106
+ window.removeEventListener("touchmove", onMouseMove);
107
+ };
108
+ }, [onMouseMove]);
109
+
110
+ useEffect(() => {
111
+ window.addEventListener("mouseup", onMouseUp);
112
+ window.addEventListener("touchend", onMouseUp);
113
+
114
+ return () => {
115
+ window.removeEventListener("mouseup", onMouseUp);
116
+ window.removeEventListener("touchend", onMouseUp);
117
+ };
118
+ }, [onMouseUp]);
119
+
120
+ return isValidElement(children)
121
+ ? cloneElement(Children.only<ReactElement>(children), {
122
+ onMouseDown: startDrag,
123
+ onTouchStart: startDrag,
124
+ style: { ...style, transform: `translateX(${isDragging ? dragX : position}px)` },
125
+ })
126
+ : null;
127
+ };
@@ -0,0 +1,4 @@
1
+ declare module "*.scss" {
2
+ const styles: { [className: string]: string };
3
+ export default styles;
4
+ }
@@ -0,0 +1,153 @@
1
+ $track-height: var(--purpur-spacing-300);
2
+ $track-width: calc(var(--purpur-spacing-400) + var(--purpur-spacing-150));
3
+ $thumb-size: calc(var(--purpur-spacing-200) + var(--purpur-spacing-25));
4
+
5
+ .purpur-toggle {
6
+ $root: &;
7
+ all: unset;
8
+ height: $track-width;
9
+ width: $track-width;
10
+ cursor: pointer;
11
+
12
+ &:hover:not(:disabled) > #{$root}__track {
13
+ background-color: var(--purpur-color-background-interactive-transparent-hover);
14
+ border-color: var(--purpur-color-border-interactive-primary);
15
+ }
16
+
17
+ &:active:not(:disabled) > #{$root}__track {
18
+ background-color: var(--purpur-color-background-interactive-transparent-active);
19
+ }
20
+
21
+ &:disabled {
22
+ cursor: default;
23
+
24
+ & > #{$root}__track {
25
+ border-color: var(--purpur-color-border-weak);
26
+ }
27
+ }
28
+
29
+ &:focus-visible {
30
+ &::after {
31
+ content: "";
32
+ position: absolute;
33
+ inset: 0;
34
+ border-radius: var(--purpur-border-radius-xs);
35
+ outline: var(--purpur-border-width-sm) solid var(--purpur-color-border-interactive-focus);
36
+ outline-offset: var(--purpur-spacing-25);
37
+ pointer-events: none;
38
+ }
39
+ }
40
+
41
+ &[data-state="checked"] {
42
+ & > #{$root}__track {
43
+ background-color: var(--purpur-color-background-interactive-primary);
44
+ border-color: var(--purpur-color-functional-transparent);
45
+ }
46
+
47
+ &:hover:not(:disabled) > #{$root}__track {
48
+ background-color: var(--purpur-color-background-interactive-primary-hover);
49
+ border-color: var(--purpur-color-functional-transparent);
50
+ }
51
+
52
+ &:active:not(:disabled) > #{$root}__track {
53
+ background-color: var(--purpur-color-background-interactive-primary-active);
54
+ border-color: var(--purpur-color-functional-transparent);
55
+ }
56
+
57
+ &:disabled > #{$root}__track {
58
+ background-color: var(--purpur-color-background-interactive-disabled);
59
+ border-color: var(--purpur-color-functional-transparent);
60
+ }
61
+
62
+ & #{$root}__checkmark-container {
63
+ opacity: 1;
64
+ }
65
+ }
66
+
67
+ &__track {
68
+ display: block;
69
+ width: $track-width;
70
+ height: $track-height;
71
+ position: relative;
72
+ background-color: var(--purpur-color-text-interactive-on-primary);
73
+ color: var(--purpur-color-text-interactive-on-primary);
74
+ box-sizing: border-box;
75
+ border: var(--purpur-border-width-xs) solid var(--purpur-color-border-interactive-primary);
76
+ border-radius: var(--purpur-border-radius-full);
77
+ transition:
78
+ background-color var(--purpur-motion-duration-150) ease,
79
+ border-color var(--purpur-motion-duration-150) ease;
80
+ }
81
+
82
+ &__thumb {
83
+ position: absolute;
84
+ display: block;
85
+ height: $thumb-size;
86
+ width: $thumb-size;
87
+ border-radius: var(--purpur-border-radius-full);
88
+ background-color: var(--purpur-color-background-interactive-primary);
89
+ transition:
90
+ transform var(--purpur-motion-duration-150) ease,
91
+ background-color var(--purpur-motion-duration-150) ease;
92
+ will-change: transform;
93
+ top: 2px;
94
+
95
+ &[data-disabled] {
96
+ background-color: var(--purpur-color-background-interactive-disabled);
97
+ }
98
+
99
+ &[data-state="checked"] {
100
+ background-color: var(--purpur-color-text-interactive-on-primary);
101
+
102
+ &[data-disabled] {
103
+ background-color: var(--purpur-color-text-interactive-on-primary);
104
+ }
105
+ }
106
+
107
+ &--dragging {
108
+ cursor: grabbing;
109
+ transition:
110
+ transform 35ms ease,
111
+ background-color var(--purpur-motion-duration-150) ease;
112
+ }
113
+ }
114
+
115
+ &__checkmark-container {
116
+ position: absolute;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ top: 50%;
121
+ opacity: 0;
122
+ transform: translateY(-50%);
123
+ left: var(--purpur-spacing-25);
124
+ transition: opacity var(--purpur-motion-duration-150) ease;
125
+ width: $thumb-size;
126
+ height: $thumb-size;
127
+ }
128
+
129
+ &__checkmark {
130
+ display: flex !important;
131
+ }
132
+
133
+ &__label {
134
+ &--right {
135
+ padding-left: var(--purpur-spacing-150);
136
+ }
137
+
138
+ &--left {
139
+ padding-right: var(--purpur-spacing-150);
140
+ }
141
+ }
142
+
143
+ &__container {
144
+ width: fit-content;
145
+ position: relative;
146
+ display: flex;
147
+ align-items: center;
148
+
149
+ & p {
150
+ transition: color var(--purpur-motion-duration-150) ease;
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,95 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Toggle } from "./toggle";
4
+ import { useArgs } from "@storybook/client-api";
5
+
6
+ import "@purpurds/icon/styles";
7
+ import "@purpurds/label/styles";
8
+ import "@purpurds/paragraph/styles";
9
+
10
+ const meta: Meta<typeof Toggle> = {
11
+ title: "Inputs/Toggle",
12
+ component: Toggle,
13
+ };
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof Toggle>;
17
+
18
+ export const Controlled: Story = {
19
+ args: {
20
+ label: "Controlled draggable toggle",
21
+ id: "toggle-showcase",
22
+ checked: false,
23
+ },
24
+ argTypes: {
25
+ defaultChecked: { table: { disable: true } },
26
+ labelPosition: {
27
+ options: [undefined, "left", "right"],
28
+ control: "select",
29
+ },
30
+ },
31
+ parameters: {
32
+ design: [
33
+ {
34
+ name: "Toggle",
35
+ type: "figma",
36
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=1187-108",
37
+ },
38
+ ],
39
+ },
40
+ render: ({ ...args }) => {
41
+ const [{ checked }, updateArgs] = useArgs(); // eslint-disable-line react-hooks/rules-of-hooks
42
+ const setChecked = (value: boolean) => {
43
+ args.onChange?.(value);
44
+ updateArgs({ checked: value });
45
+ };
46
+
47
+ return <Toggle {...args} onChange={setChecked} checked={checked} />;
48
+ },
49
+ };
50
+
51
+ export const Uncontrolled: Story = {
52
+ args: {
53
+ label: "Uncontrolled toggle",
54
+ id: "toggle-uncontrolled",
55
+ },
56
+ argTypes: {
57
+ checked: { table: { disable: true } },
58
+ onChange: { table: { disable: true } },
59
+ },
60
+ parameters: {
61
+ design: [
62
+ {
63
+ name: "Toggle",
64
+ type: "figma",
65
+ url: "https://www.figma.com/file/XEaIIFskrrxIBHMZDkIuIg/Purpur-DS---Component-library-%26-guidelines?type=design&node-id=1187-108",
66
+ },
67
+ ],
68
+ },
69
+ decorators: [
70
+ (Story) => {
71
+ const codeStyle = {
72
+ background: "var(--purpur-color-transparent-black-50)",
73
+ padding: "var(--purpur-spacing-10)",
74
+ borderRadius: "var(--purpur-border-radius-xs)",
75
+ border: "var(--purpur-border-width-xs) solid var(--purpur-color-transparent-black-100)",
76
+ };
77
+
78
+ return (
79
+ <form>
80
+ <Story />
81
+ <hr />
82
+ <p>
83
+ In this case <code style={codeStyle}>checked</code> and&nbsp;
84
+ <code style={codeStyle}>onChange</code> are not passed to the component.
85
+ </p>
86
+ <p>
87
+ Also, it is wrapped in a form which makes the toggle render a checkbox input under the
88
+ hood that reflects its value and state.
89
+ </p>
90
+ </form>
91
+ );
92
+ },
93
+ ],
94
+ render: ({ onChange: _onChange, checked: _checked, ...args }) => <Toggle {...args} />,
95
+ };
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import * as matchers from "@testing-library/jest-dom/matchers";
3
+ import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { Toggle } from "./toggle";
7
+
8
+ expect.extend(matchers);
9
+
10
+ describe("Toggle", () => {
11
+ afterEach(cleanup);
12
+
13
+ it("should render uncontrolled default checked", () => {
14
+ render(<Toggle id="test" defaultChecked />);
15
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked");
16
+ fireEvent.click(screen.getByRole("switch"));
17
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
18
+ });
19
+
20
+ it("should render uncontrolled default unchecked checked", () => {
21
+ render(<Toggle id="test" defaultChecked={false} />);
22
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
23
+ fireEvent.click(screen.getByRole("switch"));
24
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked");
25
+ });
26
+
27
+ it("should render controlled checked", () => {
28
+ render(<Toggle id="test" checked />);
29
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked");
30
+ screen.getByRole("switch").click();
31
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "checked");
32
+ });
33
+
34
+ it("should render controlled unchecked", () => {
35
+ render(<Toggle id="test" checked={false} />);
36
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
37
+ screen.getByRole("switch").click();
38
+ expect(screen.getByRole("switch")).toHaveAttribute("data-state", "unchecked");
39
+ });
40
+
41
+ it("should render disabled", () => {
42
+ render(<Toggle data-testid="test" id="test" disabled />);
43
+ expect(screen.getByRole("switch")).toHaveAttribute("disabled");
44
+ });
45
+
46
+ it("should render label left", () => {
47
+ render(<Toggle data-testid="test" id="test" labelPosition="left" label="Test label" />);
48
+ expect(screen.getByRole("switch").previousSibling).toHaveTextContent("Test label");
49
+ expect(screen.getByRole("switch").nextSibling).toBeNull();
50
+ });
51
+
52
+ it("should render label right", () => {
53
+ render(<Toggle data-testid="test" id="test" labelPosition="right" label="Test label" />);
54
+ expect(screen.getByRole("switch").previousSibling).toBeNull();
55
+ expect(screen.getByRole("switch").nextSibling).toHaveTextContent("Test label");
56
+ });
57
+
58
+ it("should not render label given no label", () => {
59
+ render(<Toggle data-testid="test" id="test" labelPosition="right" />);
60
+ expect(screen.queryByTestId("test-label")).not.toBeInTheDocument();
61
+ });
62
+
63
+ it("should emit onChange", () => {
64
+ const onChangeMock = vi.fn();
65
+ render(<Toggle data-testid="test" id="test" label="Test label" onChange={onChangeMock} />);
66
+ screen.getByTestId("test-label").click();
67
+ screen.getByRole("switch").click();
68
+ screen.getByTestId("test-thumb").click();
69
+ expect(onChangeMock).toHaveBeenCalledTimes(3);
70
+ });
71
+ });