Skip to content

Commit d71cc1c

Browse files
authored
feat(components): Update Switch component design (#2897)
* Update visual test structure * Move Switch Web.stories.tsx to v9 storybook location * Modernize Switch stories to v9 format - Update imports from ComponentMeta/ComponentStory to Meta/StoryObj from @storybook/react-vite - Update meta export to use satisfies pattern with type export - Remove /Web from title path - Remove unnecessary parameters (viewMode, previewTabs) - Update story definitions to use Story type with render property - Update template functions to use Story["args"] type * Update Switch docs links to point to v9 web storybook * Update Switch design * Remove old invisible label and update switch layout The old label had opacity=0 from the last retheme we did. The label itself was powering the layout of the pip which was confusing and not consistent in the on vs off position. Updated layout to use calculated positions which work much better. * Regen screenshots * Regen snapshots * Add tests * Fix css lint * Regen FeatureSwitch screenshots * Update Switch.stories.tsx * Update Switch.module.css.d.ts SB v7's build had to modify this file. * Make screen readers ignore the icon * Replace basic story with controlled case The controlled case is the ONLY case, so having a basic story that does nothing doesn't make sense. * Replace snapshot tests with actual logical tests
1 parent f7a0a56 commit d71cc1c

16 files changed

+283
-341
lines changed

docs/components/Switch/Switch.stories.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ import { Meta } from "@storybook/addon-docs";
55
# Switch
66

77
[Switch docs](https://atlantis.getjobber.com/components/Switch) have moved to the new site.
8+
9+
[Web](https://atlantis.getjobber.com/storybook/web/?path=/story/components-selections-switch--basic) stories have moved to Storybook v9.

docs/components/Switch/Web.stories.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

packages/components/src/FeatureSwitch/__snapshots__/FeatureSwitch.test.tsx.snap

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -121,30 +121,24 @@ exports[`renders a full FeatureSwitch 1`] = `
121121
type="button"
122122
>
123123
<span
124-
class="toggle"
124+
aria-hidden="true"
125+
class="icon"
125126
>
126-
<span
127-
class="label"
128-
>
129-
<span
130-
class="base bold small uppercase white"
131-
>
132-
On
133-
</span>
134-
</span>
135-
<span
136-
class="pip"
137-
/>
138-
<span
139-
class="label"
127+
<svg
128+
data-testid="checkmark"
129+
style="fill: var(--color-icon); display: inline-block; vertical-align: middle; width: 16px; height: 16px;"
130+
viewBox="0 0 24 24"
131+
xmlns="http://www.w3.org/2000/svg"
140132
>
141-
<span
142-
class="base bold small uppercase greyBlue"
143-
>
144-
Off
145-
</span>
146-
</span>
133+
<path
134+
d="M4.703 12.029a1 1 0 1 0-1.414 1.414l4.699 5.293a1 1 0 0 0 1.414 0L20.695 7.443a1 1 0 1 0-1.414-1.414L8.695 16.615l-3.992-4.586Z"
135+
style="fill: var(--color-surface);"
136+
/>
137+
</svg>
147138
</span>
139+
<span
140+
class="toggle"
141+
/>
148142
</button>
149143
<input
150144
type="hidden"
Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
.switch {
22
--switch--width: 48px;
33
--switch--pipSize: 16px;
4-
--switch--labelWidth: calc(var(--switch--pipSize) * 1.3);
54
--switch--borderOffset: var(--border-thick);
5+
--switch-pipOnPosition: calc(
6+
var(--switch--width) - var(--switch--pipSize) -
7+
(var(--switch--borderOffset) * 2) - var(--space-smallest)
8+
);
9+
--switch-pipOffPosition: var(--space-smallest);
610
}
711

812
.track,
913
.track * {
1014
box-sizing: border-box;
11-
transition: all var(--timing-base);
15+
transition: all var(--timing-base) ease-in-out;
1216
}
1317

1418
.track {
@@ -20,7 +24,7 @@
2024
border-radius: var(--switch--pipSize);
2125
overflow: hidden;
2226
line-height: normal;
23-
background-color: var(--color-surface);
27+
background-color: var(--color-inactive--surface);
2428
cursor: pointer;
2529
appearance: none;
2630
align-items: center;
@@ -41,59 +45,35 @@
4145
background-color: var(--color-interactive);
4246
}
4347

44-
.toggle {
45-
display: flex;
46-
position: relative;
47-
margin-left: calc(var(--switch--width) / -2);
48-
pointer-events: none;
49-
flex: 1 1 100%;
50-
align-items: center;
51-
}
52-
53-
.isChecked .toggle {
54-
margin-left: 0;
55-
}
56-
57-
.label {
58-
display: flex;
59-
position: relative;
60-
min-width: var(--switch--labelWidth);
61-
margin-top: var(--space-minuscule);
62-
user-select: none;
63-
pointer-events: none;
64-
align-items: center;
65-
justify-content: center;
66-
opacity: 0;
48+
.isChecked:hover {
49+
border-color: var(--color-interactive--hover);
50+
background-color: var(--color-interactive--hover);
6751
}
6852

69-
.label:first-of-type {
70-
padding-left: var(--space-small);
71-
}
72-
73-
.label:last-of-type {
74-
padding-right: var(--space-small);
75-
}
76-
77-
.pip {
78-
flex: 0 0 auto;
53+
.toggle {
7954
width: var(--switch--pipSize);
8055
height: var(--switch--pipSize);
8156
border: none;
8257
border-radius: var(--radius-circle);
83-
background-color: var(--color-interactive--subtle);
84-
transition: all var(--timing-quick) ease-out;
58+
background-color: var(--color-inactive--onSurface);
59+
transform: translateX(var(--switch-pipOffPosition));
8560
}
8661

87-
.isChecked .pip {
62+
.isChecked .toggle {
8863
border-color: var(--color-interactive);
8964
background-color: var(--color-surface);
65+
transform: translateX(var(--switch-pipOnPosition));
9066
}
9167

92-
.disabled .pip {
68+
.disabled .toggle {
9369
border-color: var(--color-disabled--secondary);
9470
background-color: var(--color-disabled);
9571
}
9672

73+
.disabled.isChecked .toggle {
74+
background-color: var(--color-surface);
75+
}
76+
9777
.disabled {
9878
border-color: var(--color-disabled);
9979
background-color: var(--color-surface);
@@ -111,3 +91,23 @@
11191
.disabled:hover {
11292
border-color: var(--color-disabled);
11393
}
94+
95+
.icon {
96+
display: flex;
97+
position: absolute;
98+
transform: translateX(
99+
calc(
100+
var(--switch-pipOffPosition) + var(--switch--pipSize) +
101+
var(--space-smaller)
102+
)
103+
);
104+
}
105+
106+
.isChecked .icon {
107+
transform: translateX(
108+
calc(
109+
var(--switch-pipOnPosition) - var(--switch--pipSize) -
110+
var(--space-smaller)
111+
)
112+
);
113+
}

packages/components/src/Switch/Switch.module.css.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ declare const styles: {
33
readonly "track": string;
44
readonly "isChecked": string;
55
readonly "toggle": string;
6-
readonly "label": string;
7-
readonly "pip": string;
86
readonly "disabled": string;
7+
readonly "pip": string;
8+
readonly "icon": string;
99
};
1010
export = styles;
1111

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React, { useState } from "react";
2+
import type { Meta, StoryObj } from "@storybook/react-vite";
3+
import { Switch } from "@jobber/components/Switch";
4+
import { Button } from "@jobber/components/Button";
5+
import { Stack } from "@jobber/components/Stack";
6+
7+
const meta = {
8+
title: "Components/Selections/Switch",
9+
component: Switch,
10+
} satisfies Meta<typeof Switch>;
11+
export default meta;
12+
type Story = StoryObj<typeof meta>;
13+
14+
const BasicTemplate = (args: Story["args"]) => {
15+
const [value, setValue] = useState(false);
16+
17+
return (
18+
<Stack>
19+
<Button onClick={() => setValue(!value)} label="Controlled Example" />
20+
<Switch {...args} value={value} onChange={setValue} />
21+
</Stack>
22+
);
23+
};
24+
25+
export const Basic: Story = {
26+
render: BasicTemplate,
27+
};

0 commit comments

Comments
 (0)