♻️(frontend) extract ToggleButton in a proper component
Refactor the existing Button component to extract a new ToggleButton component that wraps react aria ToggleButton. This will be used to toggle cam/mic on/off, or any other controls in the control bar.
This commit is contained in:
committed by
aleb_the_flash
parent
a8b2c56f4b
commit
6189e6454d
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiChat1Line } from '@remixicon/react'
|
import { RiChat1Line } from '@remixicon/react'
|
||||||
import { Button } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useLayoutContext } from '@livekit/components-react'
|
import { useLayoutContext } from '@livekit/components-react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
@@ -22,8 +22,7 @@ export const ChatToggle = () => {
|
|||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Button
|
<ToggleButton
|
||||||
toggle
|
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.chat.${tooltipLabel}`)}
|
aria-label={t(`controls.chat.${tooltipLabel}`)}
|
||||||
@@ -35,7 +34,7 @@ export const ChatToggle = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiChat1Line />
|
<RiChat1Line />
|
||||||
</Button>
|
</ToggleButton>
|
||||||
{!!state?.unreadMessages && (
|
{!!state?.unreadMessages && (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
|
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
|
||||||
import { Button } from '@/primitives'
|
import { ToggleButton } from '@/primitives'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { useLayoutContext, useParticipants } from '@livekit/components-react'
|
import { useLayoutContext, useParticipants } from '@livekit/components-react'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
@@ -31,8 +31,7 @@ export const ParticipantsToggle = () => {
|
|||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Button
|
<ToggleButton
|
||||||
toggle
|
|
||||||
square
|
square
|
||||||
legacyStyle
|
legacyStyle
|
||||||
aria-label={t(`controls.participants.${tooltipLabel}`)}
|
aria-label={t(`controls.participants.${tooltipLabel}`)}
|
||||||
@@ -44,7 +43,7 @@ export const ParticipantsToggle = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RiGroupLine />
|
<RiGroupLine />
|
||||||
</Button>
|
</ToggleButton>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -1,151 +1,20 @@
|
|||||||
import { type ReactNode } from 'react'
|
|
||||||
import {
|
import {
|
||||||
Button as RACButton,
|
Button as RACButton,
|
||||||
ToggleButton as RACToggleButton,
|
|
||||||
type ButtonProps as RACButtonsProps,
|
type ButtonProps as RACButtonsProps,
|
||||||
TooltipTrigger,
|
|
||||||
Link,
|
Link,
|
||||||
LinkProps,
|
LinkProps,
|
||||||
ToggleButtonProps as RACToggleButtonProps,
|
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||||
import { Tooltip } from './Tooltip'
|
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||||
|
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||||
|
|
||||||
const button = cva({
|
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||||
base: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
transition: 'background 200ms, outline 200ms, border-color 200ms',
|
|
||||||
cursor: 'pointer',
|
|
||||||
border: '1px solid transparent',
|
|
||||||
color: 'colorPalette.text',
|
|
||||||
backgroundColor: 'colorPalette',
|
|
||||||
'&[data-selected]': {
|
|
||||||
background: 'colorPalette.active',
|
|
||||||
},
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'colorPalette.hover',
|
|
||||||
},
|
|
||||||
'&[data-pressed]': {
|
|
||||||
backgroundColor: 'colorPalette.active',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
default: {
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingX: '1',
|
|
||||||
paddingY: '0.625',
|
|
||||||
'--square-padding': '{spacing.0.625}',
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
borderRadius: 4,
|
|
||||||
paddingX: '0.5',
|
|
||||||
paddingY: '0.25',
|
|
||||||
'--square-padding': '{spacing.0.25}',
|
|
||||||
},
|
|
||||||
xs: {
|
|
||||||
borderRadius: 4,
|
|
||||||
'--square-padding': '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
square: {
|
|
||||||
true: {
|
|
||||||
paddingX: 'var(--square-padding)',
|
|
||||||
paddingY: 'var(--square-padding)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
default: {
|
|
||||||
colorPalette: 'control',
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
colorPalette: 'primary',
|
|
||||||
},
|
|
||||||
// @TODO: better handling of colors… this is a mess
|
|
||||||
success: {
|
|
||||||
colorPalette: 'success',
|
|
||||||
borderColor: 'success.300',
|
|
||||||
color: 'success.subtle-text',
|
|
||||||
backgroundColor: 'success.subtle',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'success.200',
|
|
||||||
},
|
|
||||||
'&[data-pressed]': {
|
|
||||||
backgroundColor: 'success.subtle!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outline: {
|
|
||||||
true: {
|
|
||||||
color: 'colorPalette',
|
|
||||||
backgroundColor: 'transparent!',
|
|
||||||
borderColor: 'currentcolor!',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'colorPalette.subtle!',
|
|
||||||
},
|
|
||||||
'&[data-pressed]': {
|
|
||||||
backgroundColor: 'colorPalette.subtle!',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
invisible: {
|
|
||||||
true: {
|
|
||||||
borderColor: 'none!',
|
|
||||||
backgroundColor: 'none!',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'none!',
|
|
||||||
borderColor: 'colorPalette.active!',
|
|
||||||
},
|
|
||||||
'&[data-pressed]': {
|
|
||||||
borderColor: 'currentcolor',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fullWidth: {
|
|
||||||
true: {
|
|
||||||
width: 'full',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legacyStyle: {
|
|
||||||
true: {
|
|
||||||
borderColor: 'gray.400',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
borderColor: 'gray.500',
|
|
||||||
},
|
|
||||||
'&[data-pressed]': {
|
|
||||||
borderColor: 'gray.500',
|
|
||||||
},
|
|
||||||
'&[data-selected]': {
|
|
||||||
background: '#e5e7eb',
|
|
||||||
borderColor: 'gray.400',
|
|
||||||
'&[data-hovered]': {
|
|
||||||
backgroundColor: 'gray.300',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: 'default',
|
|
||||||
variant: 'default',
|
|
||||||
outline: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
type Tooltip = {
|
|
||||||
tooltip?: string
|
|
||||||
tooltipType?: 'instant' | 'delayed'
|
|
||||||
}
|
|
||||||
export type ButtonProps = RecipeVariantProps<typeof button> &
|
|
||||||
RACButtonsProps &
|
RACButtonsProps &
|
||||||
Tooltip & {
|
TooltipWrapperProps
|
||||||
toggle?: boolean
|
|
||||||
isSelected?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type LinkButtonProps = RecipeVariantProps<typeof button> & LinkProps & Tooltip
|
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||||
|
LinkProps &
|
||||||
|
TooltipWrapperProps
|
||||||
|
|
||||||
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
||||||
|
|
||||||
@@ -154,22 +23,11 @@ export const Button = ({
|
|||||||
tooltipType = 'instant',
|
tooltipType = 'instant',
|
||||||
...props
|
...props
|
||||||
}: ButtonOrLinkProps) => {
|
}: ButtonOrLinkProps) => {
|
||||||
const [variantProps, componentProps] = button.splitVariantProps(props)
|
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||||
if ((props as LinkButtonProps).href !== undefined) {
|
if ((props as LinkButtonProps).href !== undefined) {
|
||||||
return (
|
return (
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
<Link className={button(variantProps)} {...componentProps} />
|
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||||
</TooltipWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((props as ButtonProps).toggle) {
|
|
||||||
return (
|
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
|
||||||
<RACToggleButton
|
|
||||||
className={button(variantProps)}
|
|
||||||
{...(componentProps as RACToggleButtonProps)}
|
|
||||||
/>
|
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -177,26 +35,9 @@ export const Button = ({
|
|||||||
return (
|
return (
|
||||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
<RACButton
|
<RACButton
|
||||||
className={button(variantProps)}
|
className={buttonRecipe(variantProps)}
|
||||||
{...(componentProps as RACButtonsProps)}
|
{...(componentProps as RACButtonsProps)}
|
||||||
/>
|
/>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TooltipWrapper = ({
|
|
||||||
tooltip,
|
|
||||||
tooltipType,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: ReactNode
|
|
||||||
} & Tooltip) => {
|
|
||||||
return tooltip ? (
|
|
||||||
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
|
|
||||||
{children}
|
|
||||||
<Tooltip>{tooltip}</Tooltip>
|
|
||||||
</TooltipTrigger>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
25
src/frontend/src/primitives/ToggleButton.tsx
Normal file
25
src/frontend/src/primitives/ToggleButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
ToggleButton as RACToggleButton,
|
||||||
|
ToggleButtonProps,
|
||||||
|
} from 'react-aria-components'
|
||||||
|
import { type ButtonRecipeProps, buttonRecipe } from './buttonRecipe'
|
||||||
|
import { TooltipWrapper, TooltipWrapperProps } from './TooltipWrapper'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React aria ToggleButton with our button styles, that can take a tooltip if needed
|
||||||
|
*/
|
||||||
|
export const ToggleButton = ({
|
||||||
|
tooltip,
|
||||||
|
tooltipType,
|
||||||
|
...props
|
||||||
|
}: ToggleButtonProps & ButtonRecipeProps & TooltipWrapperProps) => {
|
||||||
|
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||||
|
return (
|
||||||
|
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||||
|
<RACToggleButton
|
||||||
|
{...componentProps}
|
||||||
|
className={buttonRecipe(variantProps)}
|
||||||
|
/>
|
||||||
|
</TooltipWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,16 +2,41 @@ import { type ReactNode } from 'react'
|
|||||||
import {
|
import {
|
||||||
OverlayArrow,
|
OverlayArrow,
|
||||||
Tooltip as RACTooltip,
|
Tooltip as RACTooltip,
|
||||||
TooltipProps,
|
TooltipTrigger,
|
||||||
|
type TooltipProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
|
||||||
|
export type TooltipWrapperProps = {
|
||||||
|
tooltip?: string
|
||||||
|
tooltipType?: 'instant' | 'delayed'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a component you want to apply a tooltip on (for example a Button)
|
||||||
|
*
|
||||||
|
* If no tooltip is given, just returns children
|
||||||
|
*/
|
||||||
|
export const TooltipWrapper = ({
|
||||||
|
tooltip,
|
||||||
|
tooltipType,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
} & TooltipWrapperProps) => {
|
||||||
|
return tooltip ? (
|
||||||
|
<TooltipTrigger delay={tooltipType === 'instant' ? 300 : 1000}>
|
||||||
|
{children}
|
||||||
|
<Tooltip>{tooltip}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styled react aria Tooltip component.
|
* Styled react aria Tooltip component.
|
||||||
*
|
*
|
||||||
* Note that tooltips are directly handled by Buttons via the `tooltip` prop,
|
|
||||||
* so you should not need to use this component directly.
|
|
||||||
*
|
|
||||||
* Style taken from example at https://react-spectrum.adobe.com/react-aria/Tooltip.html
|
* Style taken from example at https://react-spectrum.adobe.com/react-aria/Tooltip.html
|
||||||
*/
|
*/
|
||||||
const StyledTooltip = styled(RACTooltip, {
|
const StyledTooltip = styled(RACTooltip, {
|
||||||
@@ -80,7 +105,7 @@ const TooltipArrow = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tooltip = ({
|
const Tooltip = ({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: Omit<TooltipProps, 'children'> & { children: ReactNode }) => {
|
}: Omit<TooltipProps, 'children'> & { children: ReactNode }) => {
|
||||||
125
src/frontend/src/primitives/buttonRecipe.ts
Normal file
125
src/frontend/src/primitives/buttonRecipe.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { type RecipeVariantProps, cva } from '@/styled-system/css'
|
||||||
|
|
||||||
|
export type ButtonRecipe = typeof buttonRecipe
|
||||||
|
|
||||||
|
export type ButtonRecipeProps = RecipeVariantProps<ButtonRecipe>
|
||||||
|
|
||||||
|
export const buttonRecipe = cva({
|
||||||
|
base: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
transition: 'background 200ms, outline 200ms, border-color 200ms',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
color: 'colorPalette.text',
|
||||||
|
backgroundColor: 'colorPalette',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'colorPalette.hover',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
backgroundColor: 'colorPalette.active',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
default: {
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingX: '1',
|
||||||
|
paddingY: '0.625',
|
||||||
|
'--square-padding': '{spacing.0.625}',
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
borderRadius: 4,
|
||||||
|
paddingX: '0.5',
|
||||||
|
paddingY: '0.25',
|
||||||
|
'--square-padding': '{spacing.0.25}',
|
||||||
|
},
|
||||||
|
xs: {
|
||||||
|
borderRadius: 4,
|
||||||
|
'--square-padding': '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
square: {
|
||||||
|
true: {
|
||||||
|
paddingX: 'var(--square-padding)',
|
||||||
|
paddingY: 'var(--square-padding)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
default: {
|
||||||
|
colorPalette: 'control',
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
colorPalette: 'primary',
|
||||||
|
},
|
||||||
|
// @TODO: better handling of colors… this is a mess
|
||||||
|
success: {
|
||||||
|
colorPalette: 'success',
|
||||||
|
borderColor: 'success.300',
|
||||||
|
color: 'success.subtle-text',
|
||||||
|
backgroundColor: 'success.subtle',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'success.200',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
backgroundColor: 'success.subtle!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
true: {
|
||||||
|
color: 'colorPalette',
|
||||||
|
backgroundColor: 'transparent!',
|
||||||
|
borderColor: 'currentcolor!',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'colorPalette.subtle!',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
backgroundColor: 'colorPalette.subtle!',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invisible: {
|
||||||
|
true: {
|
||||||
|
borderColor: 'none!',
|
||||||
|
backgroundColor: 'none!',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'none!',
|
||||||
|
borderColor: 'colorPalette.active!',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
borderColor: 'currentcolor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fullWidth: {
|
||||||
|
true: {
|
||||||
|
width: 'full',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legacyStyle: {
|
||||||
|
true: {
|
||||||
|
borderColor: 'gray.400',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
borderColor: 'gray.500',
|
||||||
|
},
|
||||||
|
'&[data-pressed]': {
|
||||||
|
borderColor: 'gray.500',
|
||||||
|
},
|
||||||
|
'&[data-selected]': {
|
||||||
|
background: '#e5e7eb',
|
||||||
|
borderColor: 'gray.400',
|
||||||
|
'&[data-hovered]': {
|
||||||
|
backgroundColor: 'gray.300',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
size: 'default',
|
||||||
|
variant: 'default',
|
||||||
|
outline: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -24,5 +24,6 @@ export { MenuList } from './MenuList'
|
|||||||
export { P } from './P'
|
export { P } from './P'
|
||||||
export { Popover } from './Popover'
|
export { Popover } from './Popover'
|
||||||
export { Text } from './Text'
|
export { Text } from './Text'
|
||||||
|
export { ToggleButton } from './ToggleButton'
|
||||||
export { Ul } from './Ul'
|
export { Ul } from './Ul'
|
||||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||||
|
|||||||
Reference in New Issue
Block a user