diff options
| author | Oliver <oliver@mohlin.dev> | 2025-07-25 10:29:06 +0200 |
|---|---|---|
| committer | Tobias Järvelöv <tobias.jarvelov@mullvad.net> | 2025-09-22 12:35:43 +0200 |
| commit | b30390fbddc768512a3b850cb19f8fa15ffe5669 (patch) | |
| tree | a2eb4a56ead7ca6ece693415d72bd6cb8211a1e1 | |
| parent | 2444347e63eb48cea554facedd8568f0ead0ff7f (diff) | |
| download | mullvadvpn-b30390fbddc768512a3b850cb19f8fa15ffe5669.tar.xz mullvadvpn-b30390fbddc768512a3b850cb19f8fa15ffe5669.zip | |
Add Switch component
14 files changed, 204 insertions, 0 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx new file mode 100644 index 0000000000..748c689a9c --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/Switch.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { SwitchLabel, SwitchProvider, SwitchThumb, SwitchTrigger } from './components'; + +export interface SwitchProps { + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + disabled?: boolean; + children: React.ReactNode; +} + +function Switch({ checked, onCheckedChange, disabled, children }: SwitchProps) { + const labelId = React.useId(); + return ( + <SwitchProvider + labelId={labelId} + checked={checked} + onCheckedChange={onCheckedChange} + disabled={disabled}> + {children} + </SwitchProvider> + ); +} + +const SwitchNamespace = Object.assign(Switch, { + Label: SwitchLabel, + Thumb: SwitchThumb, + Trigger: SwitchTrigger, +}); + +export { SwitchNamespace as Switch }; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts new file mode 100644 index 0000000000..54c6e9e6b9 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/index.ts @@ -0,0 +1,4 @@ +export * from './switch-thumb'; +export * from './switch-trigger'; +export * from './switch-label'; +export * from './switch-context'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx new file mode 100644 index 0000000000..6e798e24a2 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/SwitchContext.tsx @@ -0,0 +1,32 @@ +import React from 'react'; + +import { SwitchProps } from '../../Switch'; + +interface SwitchContextProps { + labelId: string; + disabled: SwitchProps['disabled']; + checked?: SwitchProps['checked']; + onCheckedChange?: SwitchProps['onCheckedChange']; +} + +const SwitchContext = React.createContext<SwitchContextProps | undefined>(undefined); + +export const useSwitchContext = (): SwitchContextProps => { + const context = React.useContext(SwitchContext); + if (!context) { + throw new Error('useSwitchContext must be used within a SwitchProvider'); + } + return context; +}; + +interface SwitchProviderProps { + labelId: string; + disabled: SwitchProps['disabled']; + checked?: SwitchProps['checked']; + onCheckedChange?: SwitchProps['onCheckedChange']; + children: React.ReactNode; +} + +export function SwitchProvider({ children, ...props }: SwitchProviderProps) { + return <SwitchContext.Provider value={props}>{children}</SwitchContext.Provider>; +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts new file mode 100644 index 0000000000..d4faa81ef4 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-context/index.ts @@ -0,0 +1 @@ +export * from './SwitchContext'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx new file mode 100644 index 0000000000..9350cf2804 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/SwitchLabel.tsx @@ -0,0 +1,14 @@ +import { Text, TextProps } from '../../../typography'; +import { useSwitchContext } from '../switch-context'; + +export type SwitchLabelProps = TextProps; + +export function SwitchLabel({ children, ...props }: SwitchLabelProps) { + const { labelId, disabled } = useSwitchContext(); + + return ( + <Text id={labelId} color={disabled ? 'whiteAlpha40' : 'white'} {...props}> + {children} + </Text> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts new file mode 100644 index 0000000000..fec6b853a8 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-label/index.ts @@ -0,0 +1 @@ +export * from './SwitchLabel'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx new file mode 100644 index 0000000000..2a365b6b63 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/SwitchThumb.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { colors } from '../../../../foundations'; +import { Dot } from '../../../dot'; +import { useSwitchContext } from '../switch-context'; +import { useBackgroundColor, useBorderColor } from './hooks'; + +export type SwitchThumbProps = React.HtmlHTMLAttributes<HTMLDivElement>; + +const StyledSwitchThumbIndicator = styled(Dot)<{ + $checked?: boolean; + $backgroundColor?: string; +}>` + ${({ $checked, $backgroundColor }) => { + return css` + position: absolute; + left: 2px; + background-color: ${$backgroundColor}; + transform: translateX(${$checked ? '11px' : '1px'}); + transition: + width 150ms ease, + height 150ms ease, + transform 150ms ease, + background-color 100ms linear; + `; + }} +`; + +const StyledSwitchThumbTrack = styled.div<{ $borderColor: string }>` + ${({ $borderColor }) => { + return css` + position: relative; + display: flex; + align-items: center; + width: 32px; + height: 20px; + border: 2px solid ${$borderColor}; + border-radius: 100px; + transition: border-color 200ms ease; + + &:focus-visible { + outline: 2px solid ${colors.white}; + outline-offset: 2px; + } + `; + }} +`; + +export function SwitchThumb(props: SwitchThumbProps) { + const { checked } = useSwitchContext(); + const backgroundColor = useBackgroundColor(); + const borderColor = useBorderColor(); + return ( + <StyledSwitchThumbTrack $borderColor={colors[borderColor]} {...props}> + <StyledSwitchThumbIndicator $checked={checked} $backgroundColor={colors[backgroundColor]} /> + </StyledSwitchThumbTrack> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts new file mode 100644 index 0000000000..56a7f81dd4 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useBackgroundColor'; +export * from './useBorderColor'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts new file mode 100644 index 0000000000..6f80aee0ba --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBackgroundColor.ts @@ -0,0 +1,12 @@ +import { Colors } from '../../../../../foundations'; +import { useSwitchContext } from '../../switch-context'; + +export const useBackgroundColor = (): Colors => { + const { disabled, checked } = useSwitchContext(); + if (disabled) { + if (checked) return 'green40'; + else return 'red40'; + } + if (checked) return 'green'; + else return 'red'; +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts new file mode 100644 index 0000000000..37c270a86e --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/hooks/useBorderColor.ts @@ -0,0 +1,8 @@ +import { Colors } from '../../../../../foundations'; +import { useSwitchContext } from '../../switch-context'; + +export const useBorderColor = (): Colors => { + const { disabled } = useSwitchContext(); + if (disabled) return 'whiteAlpha20'; + return 'whiteAlpha80'; +}; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts new file mode 100644 index 0000000000..4f3b2c8b31 --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-thumb/index.ts @@ -0,0 +1 @@ +export * from './SwitchThumb'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx new file mode 100644 index 0000000000..cbef07c6ff --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/SwitchTrigger.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import styled from 'styled-components'; + +import { colors } from '../../../../foundations'; +import { useSwitchContext } from '../switch-context'; + +export type SwitchTriggerProps = React.ComponentPropsWithRef<'button'>; + +export const StyledSwitchTrigger = styled.button<{ $checked?: boolean }>` + background-color: transparent; + width: fit-content; + + &&:focus-visible { + outline: 2px solid ${colors.white}; + outline-offset: -1px; + } +`; + +export function SwitchTrigger(props: SwitchTriggerProps) { + const { labelId, checked, disabled, onCheckedChange } = useSwitchContext(); + const handleClick = React.useCallback(() => { + if (onCheckedChange) { + onCheckedChange(!checked); + } + }, [checked, onCheckedChange]); + + return ( + <StyledSwitchTrigger + onClick={handleClick} + disabled={disabled} + role="switch" + aria-checked={checked ? 'true' : 'false'} + aria-labelledby={labelId} + {...props} + /> + ); +} diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts new file mode 100644 index 0000000000..a32b52020b --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/components/switch-trigger/index.ts @@ -0,0 +1 @@ +export * from './SwitchTrigger'; diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts new file mode 100644 index 0000000000..1b19c1d39c --- /dev/null +++ b/desktop/packages/mullvad-vpn/src/renderer/lib/components/switch/index.ts @@ -0,0 +1 @@ +export * from './Switch'; |
