@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.
- package/dist/DraggableX.d.ts +24 -0
- package/dist/DraggableX.d.ts.map +1 -0
- package/dist/LICENSE.txt +188 -0
- package/dist/styles.css +1 -0
- package/dist/toggle.cjs.js +18 -0
- package/dist/toggle.cjs.js.map +1 -0
- package/dist/toggle.d.ts +59 -0
- package/dist/toggle.d.ts.map +1 -0
- package/dist/toggle.es.js +900 -0
- package/dist/toggle.es.js.map +1 -0
- package/dist/toggle.system.js +18 -0
- package/dist/toggle.system.js.map +1 -0
- package/dist/useToggleDrag.d.ts +18 -0
- package/dist/useToggleDrag.d.ts.map +1 -0
- package/package.json +64 -0
- package/readme.mdx +136 -0
- package/src/DraggableX.tsx +127 -0
- package/src/global.d.ts +4 -0
- package/src/toggle.module.scss +153 -0
- package/src/toggle.stories.tsx +95 -0
- package/src/toggle.test.tsx +71 -0
- package/src/toggle.tsx +155 -0
- package/src/useToggleDrag.ts +58 -0
|
@@ -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
|
+
};
|
package/src/global.d.ts
ADDED
|
@@ -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
|
|
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
|
+
});
|