Skip to content

Commit 987ceee

Browse files
authored
feat: Improve Switch styling and enable css prop (#518)
1 parent 1a1991c commit 987ceee

File tree

16 files changed

+271
-223
lines changed

16 files changed

+271
-223
lines changed

.storybook/main.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import { type StorybookConfig } from '@storybook/builder-vite'
2-
import { mergeConfig } from 'vite'
3-
4-
import viteConfig from '../vite.config'
1+
import { type StorybookConfig } from '@storybook/react-vite'
52

63
const config: StorybookConfig = {
74
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
8-
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
9-
framework: {
10-
name: '@storybook/react-vite',
11-
},
12-
core: {
13-
builder: '@storybook/builder-vite',
14-
},
15-
viteFinal: async config => (mergeConfig(config, viteConfig)),
5+
addons: [
6+
'@storybook/addon-links',
7+
'@storybook/addon-essentials',
8+
'@storybook/addon-interactions',
9+
],
10+
framework: '@storybook/react-vite',
1611
}
1712

1813
export default config

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@tanstack/react-virtual": "3.0.0-beta.54",
4343
"@types/chroma-js": "2.4.0",
4444
"@types/lodash-es": "4.17.8",
45+
"babel-plugin-styled-components": "2.1.4",
4546
"chroma-js": "2.4.2",
4647
"classnames": "2.3.2",
4748
"grommet": "2.32.2",
@@ -119,7 +120,7 @@
119120
"rimraf": "5.0.1",
120121
"storybook": "7.4.0",
121122
"styled-components": "5.3.11",
122-
"typescript": "4.9.5",
123+
"typescript": "5.2.2",
123124
"vite": "4.4.9",
124125
"vitest": "0.34.3"
125126
},

src/components/Accordion.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import {
1010
useState,
1111
} from 'react'
1212
import React from 'react'
13-
import { animated, useSpring } from 'react-spring'
13+
import { useSpring } from 'react-spring'
1414
import styled, { useTheme } from 'styled-components'
1515

1616
import useResizeObserver from '../hooks/useResizeObserver'
1717
import { type UseDisclosureProps, useDisclosure } from '../hooks/useDisclosure'
1818
import { CaretDownIcon } from '../icons'
1919

2020
import Card from './Card'
21+
import { AnimatedDiv } from './AnimatedDiv'
2122

2223
const paddingTransition = '0.2s ease'
2324

@@ -133,17 +134,19 @@ function AccordionContentUnstyled({
133134
const mOffset = theme.spacing.medium - theme.spacing.small
134135

135136
return (
136-
<animated.div
137-
style={{
138-
overflow: 'hidden',
139-
...(!_unstyled
140-
? {
141-
marginTop: -mOffset,
142-
marginBottom: isOpen ? 0 : mOffset,
143-
}
144-
: {}),
145-
...springs,
146-
}}
137+
<AnimatedDiv
138+
style={
139+
{
140+
overflow: 'hidden',
141+
...(!_unstyled
142+
? {
143+
marginTop: -mOffset,
144+
marginBottom: isOpen ? 0 : mOffset,
145+
}
146+
: {}),
147+
...springs,
148+
} as any
149+
}
147150
>
148151
<div
149152
className={className}
@@ -152,7 +155,7 @@ function AccordionContentUnstyled({
152155
>
153156
{children}
154157
</div>
155-
</animated.div>
158+
</AnimatedDiv>
156159
)
157160
}
158161
const AccordionContent = styled(AccordionContentUnstyled)(

src/components/AnimatedDiv.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Workaround for issue with styled components `css` prop and `animated.div`
2+
// https://github.com/pmndrs/react-spring/issues/1515
3+
import { animated } from 'react-spring'
4+
import styled from 'styled-components'
5+
6+
export const AnimatedDiv = styled(animated.div)<any>``

src/components/Layer.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {
99
useState,
1010
} from 'react'
1111
import { createPortal } from 'react-dom'
12-
import { type UseTransitionProps, animated, useTransition } from 'react-spring'
12+
import { type UseTransitionProps, useTransition } from 'react-spring'
1313
import { isNil } from 'lodash-es'
1414
import styled, { useTheme } from 'styled-components'
1515

1616
import usePrevious from '../hooks/usePrevious'
1717

18+
import { AnimatedDiv } from './AnimatedDiv'
19+
1820
const DIRECTIONS = ['up', 'down', 'left', 'right'] as const
1921

2022
type Direction = (typeof DIRECTIONS)[number]
@@ -290,13 +292,13 @@ function LayerRef(
290292
margin={margin}
291293
>
292294
{transitions((styles) => (
293-
<animated.div
295+
<AnimatedDiv
294296
className="animated"
295297
ref={finalRef}
296298
style={{ ...styles }}
297299
>
298300
{children}
299-
</animated.div>
301+
</AnimatedDiv>
300302
))}
301303
</LayerWrapper>
302304
)

src/components/LightDarkSwitch.tsx

Lines changed: 65 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
import { useEffect, useRef, useState } from 'react'
2-
import { useToggleState } from 'react-stately'
3-
import {
4-
type AriaSwitchProps,
5-
VisuallyHidden,
6-
useFocusRing,
7-
useSwitch,
8-
} from 'react-aria'
1+
import { useEffect, useState } from 'react'
2+
import { VisuallyHidden } from 'react-aria'
93
import styled, { keyframes, useTheme } from 'styled-components'
104

115
import usePrevious from '../hooks/usePrevious'
126

7+
import { type SwitchProps, type SwitchStyleProps, useSwitch } from './Switch'
8+
139
const HANDLE_SIZE = 16
1410
const HANDLE_MARGIN = 4
1511
const SWITCH_WIDTH = 42
@@ -49,7 +45,7 @@ const slideOffAnim = keyframes`
4945
}
5046
`
5147

52-
const MoonSC = styled.svg<{ $selected: boolean }>(({ $selected }) => ({
48+
const MoonSC = styled.svg<SwitchStyleProps>(({ $checked }) => ({
5349
position: 'absolute',
5450
top: 6,
5551
left: 24,
@@ -59,21 +55,21 @@ const MoonSC = styled.svg<{ $selected: boolean }>(({ $selected }) => ({
5955
transition: 'all 0.15s ease 0.1s',
6056
},
6157
'.moonFill': {
62-
opacity: $selected ? 1 : 0,
58+
opacity: $checked ? 1 : 0,
6359
zIndex: 0,
6460
},
6561
'.moonOutline': {
66-
opacity: $selected ? 0 : 1,
62+
opacity: $checked ? 0 : 1,
6763
zIndex: 1,
6864
},
6965
}))
7066

71-
function Moon({ selected }: { selected: boolean }) {
67+
function Moon(props: SwitchStyleProps) {
7268
const theme = useTheme()
7369

7470
return (
7571
<MoonSC
76-
$selected={selected}
72+
{...props}
7773
xmlns="http://www.w3.org/2000/svg"
7874
viewBox="0 0 12 12"
7975
>
@@ -91,7 +87,7 @@ function Moon({ selected }: { selected: boolean }) {
9187
)
9288
}
9389

94-
const SunSC = styled.svg<{ $selected: boolean }>((_) => ({
90+
const SunSC = styled.svg<SwitchStyleProps>((_) => ({
9591
position: 'absolute',
9692
top: 6,
9793
left: 6,
@@ -102,15 +98,15 @@ const SunSC = styled.svg<{ $selected: boolean }>((_) => ({
10298
},
10399
}))
104100

105-
function Sun({ selected }: { selected: boolean }) {
101+
function Sun(props: SwitchStyleProps) {
106102
const theme = useTheme()
107-
const color = selected
103+
const color = !props.$checked
108104
? theme.colors.yellow[500]
109105
: theme.colors['text-primary-disabled']
110106

111107
return (
112108
<SunSC
113-
$selected={selected}
109+
{...props}
114110
xmlns="http://www.w3.org/2000/svg"
115111
viewBox="0 0 12 12"
116112
>
@@ -142,52 +138,48 @@ function Sun({ selected }: { selected: boolean }) {
142138
)
143139
}
144140

145-
const SwitchSC = styled.label<{
146-
$checked: boolean
147-
$disabled: boolean
148-
$readOnly: boolean
149-
}>(({ $disabled, $readOnly, theme }) => ({
150-
display: 'flex',
151-
columnGap: theme.spacing.xsmall,
152-
alignItems: 'center',
153-
...theme.partials.text.body2,
154-
cursor: $disabled ? 'not-allowed' : $readOnly ? 'default' : 'pointer',
155-
color: theme.colors['text-light'],
156-
...($disabled || $readOnly
157-
? {}
158-
: {
159-
'&:hover': {
160-
color: theme.colors.text,
161-
[SwitchToggleSC]: {
162-
backgroundColor: theme.colors['action-input-hover'],
141+
const SwitchSC = styled.label<SwitchStyleProps>(
142+
({ $disabled, $readOnly, theme }) => ({
143+
display: 'flex',
144+
columnGap: theme.spacing.xsmall,
145+
alignItems: 'center',
146+
...theme.partials.text.body2,
147+
cursor: $disabled ? 'not-allowed' : $readOnly ? 'default' : 'pointer',
148+
color: theme.colors['text-light'],
149+
...($disabled || $readOnly
150+
? {}
151+
: {
152+
'&:hover': {
153+
color: theme.colors.text,
154+
[SwitchToggleSC]: {
155+
backgroundColor: theme.colors['action-input-hover'],
156+
},
163157
},
164-
},
165-
}),
166-
}))
167-
168-
const SwitchToggleSC = styled.div<{
169-
$disabled: boolean
170-
$focused: boolean
171-
$checked: boolean
172-
}>(({ $focused, $disabled, theme }) => ({
173-
position: 'relative',
174-
width: 42,
175-
height: 24,
176-
borderRadius: 12,
177-
backgroundColor: 'transparent',
178-
outlineWidth: 1,
179-
outlineStyle: 'solid',
180-
outlineOffset: -1,
181-
outlineColor: $disabled
182-
? theme.colors['border-disabled']
183-
: $focused
184-
? theme.colors['border-outline-focused']
185-
: theme.colors['border-input'],
186-
transition: 'all 0.15s ease',
187-
}))
158+
}),
159+
})
160+
)
161+
162+
const SwitchToggleSC = styled.div<SwitchStyleProps>(
163+
({ $focused, $disabled, theme }) => ({
164+
position: 'relative',
165+
width: 42,
166+
height: 24,
167+
borderRadius: 12,
168+
backgroundColor: 'transparent',
169+
outlineWidth: 1,
170+
outlineStyle: 'solid',
171+
outlineOffset: -1,
172+
outlineColor: $disabled
173+
? theme.colors['border-disabled']
174+
: $focused
175+
? theme.colors['border-outline-focused']
176+
: theme.colors['border-input'],
177+
transition: 'all 0.15s ease',
178+
})
179+
)
188180

189181
const SwitchHandleSC = styled(
190-
styled.div<{ $checked: boolean; $disabled: boolean; $animate: boolean }>(
182+
styled.div<SwitchStyleProps & { $animate: boolean }>(
191183
({ $checked, $disabled, theme }) => ({
192184
position: 'absolute',
193185
width: '100%',
@@ -222,29 +214,12 @@ const SwitchHandleSC = styled(
222214

223215
export function LightDarkSwitch({
224216
children,
225-
checked,
226-
disabled,
227-
readOnly,
217+
as,
218+
className,
228219
...props
229-
}: Omit<
230-
AriaSwitchProps & Parameters<typeof useToggleState>[0],
231-
'isDisabled' | 'isReadonly' | 'isSelected'
232-
> & { checked?: boolean; disabled?: boolean; readOnly?: boolean }) {
233-
const ariaProps: AriaSwitchProps = {
234-
isSelected: checked,
235-
isDisabled: disabled,
236-
isReadOnly: readOnly,
237-
...props,
238-
}
239-
240-
const state = useToggleState(ariaProps)
241-
const ref = useRef<HTMLInputElement>(null)
242-
const { inputProps, isSelected, isDisabled, isReadOnly } = useSwitch(
243-
{ ...ariaProps },
244-
state,
245-
ref
246-
)
247-
const { focusProps, isFocusVisible } = useFocusRing()
220+
}: SwitchProps) {
221+
const { inputProps, styleProps, state } = useSwitch(props)
222+
const { isSelected } = state
248223
const wasSelected = usePrevious(isSelected) ?? isSelected
249224
const [animate, setAnimate] = useState(false)
250225

@@ -256,27 +231,18 @@ export function LightDarkSwitch({
256231

257232
return (
258233
<SwitchSC
259-
$disabled={isDisabled}
260-
$checked={isSelected}
261-
$readOnly={isReadOnly}
234+
as={as}
235+
className={className}
236+
{...styleProps}
262237
>
263238
<VisuallyHidden>
264-
<input
265-
{...inputProps}
266-
{...focusProps}
267-
ref={ref}
268-
/>
239+
<input {...inputProps} />
269240
</VisuallyHidden>
270-
<SwitchToggleSC
271-
$focused={isFocusVisible}
272-
$disabled={isDisabled}
273-
$checked={isSelected}
274-
>
275-
<Sun selected={!isSelected} />
276-
<Moon selected={isSelected} />
241+
<SwitchToggleSC {...styleProps}>
242+
<Sun {...styleProps} />
243+
<Moon {...styleProps} />
277244
<SwitchHandleSC
278-
$disabled={isDisabled}
279-
$checked={isSelected}
245+
{...styleProps}
280246
$animate={animate}
281247
/>
282248
</SwitchToggleSC>

0 commit comments

Comments
 (0)