add `<Switch />` and `<DropdownMenu.SwitchItem />` (#582)

* add <Switch />

* add <DropdownMenu.SwitchItem />

* unify checkbox and switch

* Create warm-countries-cry.md

* fix displayName
This commit is contained in:
pavel 2024-09-27 16:51:28 +02:00 committed by GitHub
parent 7f215e15e9
commit 433558a131
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 174 additions and 9 deletions

View File

@ -0,0 +1,5 @@
---
"@status-im/components": patch
---
Add `<Switch />` and `<DropdownMenu.SwitchItem />`

View File

@ -47,6 +47,7 @@
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",

View File

@ -7,7 +7,7 @@ import { cva } from 'cva'
import type { VariantProps } from 'cva' import type { VariantProps } from 'cva'
type Variants = VariantProps<typeof styles> type Variants = VariantProps<typeof rootStyles>
type Props = Omit<Checkbox.CheckboxProps, 'checked' | 'onCheckedChange'> & { type Props = Omit<Checkbox.CheckboxProps, 'checked' | 'onCheckedChange'> & {
checked: boolean checked: boolean
@ -23,7 +23,7 @@ const Root = forwardRef<React.ElementRef<typeof Checkbox.Root>, Props>(
<Checkbox.Root <Checkbox.Root
{...checkboxProps} {...checkboxProps}
ref={ref} ref={ref}
className={styles({ variant })} className={rootStyles({ variant })}
> >
<Checkbox.Indicator> <Checkbox.Indicator>
<svg <svg
@ -50,9 +50,9 @@ const Root = forwardRef<React.ElementRef<typeof Checkbox.Root>, Props>(
Root.displayName = Checkbox.Root.displayName Root.displayName = Checkbox.Root.displayName
const styles = cva({ const rootStyles = cva({
base: [ base: [
'group inline-flex size-[18px] shrink-0 items-center justify-center overflow-hidden rounded-6 text-white-100 transition-colors', 'group inline-flex size-[18px] shrink-0 cursor-default items-center justify-center overflow-hidden rounded-6 text-white-100 transition-colors',
'border border-neutral-20 hover:border-neutral-30', 'border border-neutral-20 hover:border-neutral-30',
'aria-checked:border-customisation-50 aria-checked:bg-customisation-50 aria-checked:hover:border-customisation-60 aria-checked:hover:bg-customisation-60', 'aria-checked:border-customisation-50 aria-checked:bg-customisation-50 aria-checked:hover:border-customisation-60 aria-checked:hover:bg-customisation-60',
'focus-visible:ring-2 focus-visible:ring-customisation-50 focus-visible:ring-offset-2', 'focus-visible:ring-2 focus-visible:ring-customisation-50 focus-visible:ring-offset-2',

View File

@ -31,9 +31,8 @@ const meta: Meta = {
}, },
render: args => { render: args => {
// eslint-disable-next-line react-hooks/rules-of-hooks const [isChecked, setIsChecked] = useState(false) // eslint-disable-line react-hooks/rules-of-hooks
const [isChecked, setIsChecked] = useState(false) const [isChecked2, setIsChecked2] = useState(false) // eslint-disable-line react-hooks/rules-of-hooks
return ( return (
<DropdownMenu.Root {...args}> <DropdownMenu.Root {...args}>
<DropdownButton>Open</DropdownButton> <DropdownButton>Open</DropdownButton>
@ -100,6 +99,13 @@ const meta: Meta = {
onCheckedChange={setIsChecked} onCheckedChange={setIsChecked}
onSelect={e => e.preventDefault()} onSelect={e => e.preventDefault()}
/> />
<DropdownMenu.SwitchItem
icon={<NotificationsIcon />}
label="Toggle alerts"
checked={isChecked2}
onCheckedChange={setIsChecked2}
onSelect={e => e.preventDefault()}
/>
<DropdownMenu.Separator /> <DropdownMenu.Separator />

View File

@ -11,6 +11,7 @@ import { cva } from 'cva'
import { Checkbox } from '../checkbox' import { Checkbox } from '../checkbox'
import { Input } from '../input' import { Input } from '../input'
import { Switch } from '../switch'
import type { IconElement } from '../types' import type { IconElement } from '../types'
@ -111,7 +112,7 @@ type ItemProps = DropdownMenu.DropdownMenuItemProps & {
const itemStyles = cva({ const itemStyles = cva({
base: [ base: [
'flex h-8 cursor-pointer select-none items-center gap-2 rounded-8 px-2 text-15 transition-colors active:bg-neutral-10', 'flex h-8 select-none items-center gap-2 rounded-8 px-2 text-15 transition-colors active:bg-neutral-10',
'outline-none data-[highlighted]:bg-neutral-5', 'outline-none data-[highlighted]:bg-neutral-5',
'dark:active:bg-customisation-50/10 dark:hover:bg-customisation-50/5', 'dark:active:bg-customisation-50/10 dark:hover:bg-customisation-50/5',
], ],
@ -198,7 +199,7 @@ export const CheckboxItem = forwardRef<
variant="outline" variant="outline"
checked={itemProps.checked} checked={itemProps.checked}
onCheckedChange={() => { onCheckedChange={() => {
// handle by parent // handled by parent
}} }}
/> />
</DropdownMenu.CheckboxItem> </DropdownMenu.CheckboxItem>
@ -207,6 +208,49 @@ export const CheckboxItem = forwardRef<
CheckboxItem.displayName = DropdownMenu.CheckboxItem.displayName CheckboxItem.displayName = DropdownMenu.CheckboxItem.displayName
/**
* SwitchItem
*/
type SwitchItemProps = DropdownMenu.DropdownMenuCheckboxItemProps & {
icon?: IconElement
label: string
checked: boolean
onCheckedChange: (checked: boolean) => void
danger?: boolean
}
export const SwitchItem = forwardRef<
React.ElementRef<typeof DropdownMenu.CheckboxItem>,
SwitchItemProps
>((props, ref) => {
const { label, icon, danger, ...itemProps } = props
const id = useId()
return (
<DropdownMenu.CheckboxItem
ref={ref}
className={itemStyles()}
{...itemProps}
>
{icon && (
<span className={iconStyles({ danger })}>{cloneElement(icon)}</span>
)}
<span className={labelStyles({ danger })}>{label}</span>
<Switch
id={id}
checked={itemProps.checked}
onCheckedChange={() => {
// handled by parent
}}
/>
</DropdownMenu.CheckboxItem>
)
})
SwitchItem.displayName = 'DropdownMenuSwitchItem'
/** /**
* Separator * Separator
*/ */

View File

@ -12,6 +12,7 @@ export { StatusProvider } from './provider'
export { Shortcut } from './shortcut' export { Shortcut } from './shortcut'
export { Skeleton } from './skeleton' export { Skeleton } from './skeleton'
export { Step } from './step' export { Step } from './step'
export { Switch } from './switch'
export { Tabs } from './tabs' export { Tabs } from './tabs'
export { Tag } from './tag' export { Tag } from './tag'
export { Text } from './text' export { Text } from './text'

View File

@ -0,0 +1,2 @@
export type { SwitchProps } from './switch'
export { Switch } from './switch'

View File

@ -0,0 +1,50 @@
import { Switch } from './switch'
import type { Meta, StoryObj } from '@storybook/react'
const Variant = (props: React.ComponentProps<typeof Switch>) => {
return (
<div className="flex gap-3">
<Switch {...props} />
<Switch {...props} defaultChecked />
<Switch {...props} disabled />
</div>
)
}
const meta: Meta<typeof Switch> = {
title: 'Components/Switch',
component: Switch,
args: {
// size: 'md',
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/design/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=370-18368&node-type=frame&m=dev',
},
},
render: props => {
return (
<div className="grid gap-3">
<Variant {...props} />
<Variant {...props} />
</div>
)
},
}
type Story = StoryObj<typeof Switch>
export const Light: Story = {}
export const Dark: Story = {
parameters: {
backgrounds: {
default: 'dark',
},
},
}
export default meta

View File

@ -0,0 +1,43 @@
'use client'
import { forwardRef } from 'react'
import * as Switch from '@radix-ui/react-switch'
import { cva } from 'cva'
type Props = Switch.SwitchProps & {
checked: boolean
onCheckedChange: (checked: boolean) => void
}
const Root = forwardRef<React.ElementRef<typeof Switch.Root>, Props>(
(props, ref) => {
return (
<Switch.Root {...props} ref={ref} className={rootStyles()}>
<Switch.Thumb className={thumbStyles()} />
</Switch.Root>
)
},
)
Root.displayName = Switch.Root.displayName
const rootStyles = cva({
base: [
'relative h-5 w-[30px] shrink-0 cursor-default rounded-full outline-none transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-customisation-50 focus-visible:ring-offset-2',
'bg-neutral-20 data-[state=checked]:bg-customisation-blue-50 hover:bg-neutral-30 data-[state=checked]:hover:bg-customisation-blue-60',
'dark:bg-neutral-80 dark:data-[state=checked]:bg-customisation-blue-60 dark:hover:bg-neutral-70 dark:data-[state=checked]:hover:bg-customisation-blue-50 dark:focus-visible:ring-offset-neutral-80',
'data-[disabled]:pointer-events-none data-[disabled]:opacity-[30%]',
],
})
const thumbStyles = cva({
base: [
'block size-4 rounded-full bg-white-100',
'translate-x-0.5 transition-transform duration-100 data-[state=checked]:translate-x-[12px]',
],
})
export { Root as Switch }
export type { Props as SwitchProps }

View File

@ -2483,6 +2483,19 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.0" "@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-switch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.0.tgz#fcf8e778500f1d60d4b2bec2fc3fad77a7c118e3"
integrity sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-tabs@^1.1.0": "@radix-ui/react-tabs@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz#0a6db1caed56776a1176aae68532060e301cc1c0" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz#0a6db1caed56776a1176aae68532060e301cc1c0"