♻️(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 { RiChat1Line } from '@remixicon/react'
|
||||
import { Button } from '@/primitives'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useLayoutContext } from '@livekit/components-react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
@@ -22,8 +22,7 @@ export const ChatToggle = () => {
|
||||
display: 'inline-block',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
toggle
|
||||
<ToggleButton
|
||||
square
|
||||
legacyStyle
|
||||
aria-label={t(`controls.chat.${tooltipLabel}`)}
|
||||
@@ -35,7 +34,7 @@ export const ChatToggle = () => {
|
||||
}}
|
||||
>
|
||||
<RiChat1Line />
|
||||
</Button>
|
||||
</ToggleButton>
|
||||
{!!state?.unreadMessages && (
|
||||
<div
|
||||
className={css({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiGroupLine, RiInfinityLine } from '@remixicon/react'
|
||||
import { Button } from '@/primitives'
|
||||
import { ToggleButton } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useLayoutContext, useParticipants } from '@livekit/components-react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
@@ -31,8 +31,7 @@ export const ParticipantsToggle = () => {
|
||||
display: 'inline-block',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
toggle
|
||||
<ToggleButton
|
||||
square
|
||||
legacyStyle
|
||||
aria-label={t(`controls.participants.${tooltipLabel}`)}
|
||||
@@ -44,7 +43,7 @@ export const ParticipantsToggle = () => {
|
||||
}}
|
||||
>
|
||||
<RiGroupLine />
|
||||
</Button>
|
||||
</ToggleButton>
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1,151 +1,20 @@
|
||||
import { type ReactNode } from 'react'
|
||||
import {
|
||||
Button as RACButton,
|
||||
ToggleButton as RACToggleButton,
|
||||
type ButtonProps as RACButtonsProps,
|
||||
TooltipTrigger,
|
||||
Link,
|
||||
LinkProps,
|
||||
ToggleButtonProps as RACToggleButtonProps,
|
||||
} from 'react-aria-components'
|
||||
import { cva, type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { Tooltip } from './Tooltip'
|
||||
import { type RecipeVariantProps } from '@/styled-system/css'
|
||||
import { buttonRecipe, type ButtonRecipe } from './buttonRecipe'
|
||||
import { TooltipWrapper, type TooltipWrapperProps } from './TooltipWrapper'
|
||||
|
||||
const button = 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-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> &
|
||||
export type ButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
RACButtonsProps &
|
||||
Tooltip & {
|
||||
toggle?: boolean
|
||||
isSelected?: boolean
|
||||
}
|
||||
TooltipWrapperProps
|
||||
|
||||
type LinkButtonProps = RecipeVariantProps<typeof button> & LinkProps & Tooltip
|
||||
type LinkButtonProps = RecipeVariantProps<ButtonRecipe> &
|
||||
LinkProps &
|
||||
TooltipWrapperProps
|
||||
|
||||
type ButtonOrLinkProps = ButtonProps | LinkButtonProps
|
||||
|
||||
@@ -154,22 +23,11 @@ export const Button = ({
|
||||
tooltipType = 'instant',
|
||||
...props
|
||||
}: ButtonOrLinkProps) => {
|
||||
const [variantProps, componentProps] = button.splitVariantProps(props)
|
||||
const [variantProps, componentProps] = buttonRecipe.splitVariantProps(props)
|
||||
if ((props as LinkButtonProps).href !== undefined) {
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<Link className={button(variantProps)} {...componentProps} />
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
if ((props as ButtonProps).toggle) {
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACToggleButton
|
||||
className={button(variantProps)}
|
||||
{...(componentProps as RACToggleButtonProps)}
|
||||
/>
|
||||
<Link className={buttonRecipe(variantProps)} {...componentProps} />
|
||||
</TooltipWrapper>
|
||||
)
|
||||
}
|
||||
@@ -177,26 +35,9 @@ export const Button = ({
|
||||
return (
|
||||
<TooltipWrapper tooltip={tooltip} tooltipType={tooltipType}>
|
||||
<RACButton
|
||||
className={button(variantProps)}
|
||||
className={buttonRecipe(variantProps)}
|
||||
{...(componentProps as RACButtonsProps)}
|
||||
/>
|
||||
</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 {
|
||||
OverlayArrow,
|
||||
Tooltip as RACTooltip,
|
||||
TooltipProps,
|
||||
TooltipTrigger,
|
||||
type TooltipProps,
|
||||
} from 'react-aria-components'
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
const StyledTooltip = styled(RACTooltip, {
|
||||
@@ -80,7 +105,7 @@ const TooltipArrow = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const Tooltip = ({
|
||||
const Tooltip = ({
|
||||
children,
|
||||
...props
|
||||
}: 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 { Popover } from './Popover'
|
||||
export { Text } from './Text'
|
||||
export { ToggleButton } from './ToggleButton'
|
||||
export { Ul } from './Ul'
|
||||
export { VerticallyOffCenter } from './VerticallyOffCenter'
|
||||
|
||||
Reference in New Issue
Block a user