Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/react/src/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('Radio', () => {
const radio = getByRole('radio')

expect(radio).toBeDefined()
expect(radio).toHaveAttribute('type', 'radio')
})

it('renders data-component attribute', () => {
Expand Down
38 changes: 15 additions & 23 deletions packages/react/src/Radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {clsx} from 'clsx'
import sharedClasses from '../Checkbox/shared.module.css'
import classes from './Radio.module.css'
import type {WithSlotMarker} from '../utils/types'
import {mergeProps} from '../utils/mergeProps'

export type RadioProps = {
/**
Expand Down Expand Up @@ -32,31 +33,20 @@ export type RadioProps = {
* Indicates whether the radio button must be checked before the form can be submitted
*/
required?: boolean
} & InputHTMLAttributes<HTMLInputElement>
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'type'>

/**
* An accessible, native radio component for selecting one option from a list.
*/
const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
(
{
checked,
disabled,
name: nameProp,
onChange,
required,
value,
className,
'aria-hidden': ariaHidden = false,
...rest
}: RadioProps,
{checked, name: nameProp, className, 'aria-hidden': ariaHidden, ...rest}: RadioProps,
ref,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): ReactElement<any> => {
const radioGroupContext = useContext(RadioGroupContext)
const handleOnChange: ChangeEventHandler<HTMLInputElement> = e => {
radioGroupContext?.onChange && radioGroupContext.onChange(e)
onChange && onChange(e)
}
const name = nameProp || radioGroupContext?.name

Expand All @@ -69,17 +59,19 @@ const Radio = React.forwardRef<HTMLInputElement, RadioProps>(

return (
<input
type="radio"
value={value}
name={name}
ref={ref}
disabled={disabled}
checked={checked}
aria-checked={checked ? 'true' : 'false'}
required={required}
onChange={handleOnChange}
className={clsx(className, sharedClasses.Input, classes.Radio)}
{...rest}
{...mergeProps(
{
onChange: handleOnChange,
className: clsx(sharedClasses.Input, classes.Radio, className),
'aria-checked': checked ? ('true' as const) : ('false' as const),
'aria-hidden': ariaHidden,
checked,
name,
},
rest,
)}
type="radio"
data-component="Radio"
/>
)
Expand Down
6 changes: 6 additions & 0 deletions packages/react/src/Radio/Radio.types.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Radio from '.'

export function doesNotAcceptType() {
// @ts-expect-error Radio always renders an input with type="radio"
return <Radio name="example" value="example" type="checkbox" />
}
62 changes: 30 additions & 32 deletions packages/react/src/Token/TokenBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../uti
import classes from './TokenBase.module.css'
import {defaultTokenSize, type TokenSizeKeys} from './constants'
import {isTokenInteractive} from './utils'
import {mergeProps} from '../utils/mergeProps'

export type {TokenSizeKeys}

Expand Down Expand Up @@ -43,46 +44,43 @@ export interface TokenBaseProps

const TokenBase = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | HTMLSpanElement | undefined, TokenBaseProps>(
(
{
onRemove,
onKeyDown,
id,
className,
size = defaultTokenSize,
isSelected: _isSelected,
as: Component = 'span',
...rest
},
{onRemove, id, className, size = defaultTokenSize, isSelected: _isSelected, as: Component = 'span', ...rest},
forwardedRef,
) => {
const handleKeyDown = (event: KeyboardEvent<HTMLSpanElement & HTMLAnchorElement & HTMLButtonElement>) => {
if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) {
onRemove()
}
}
const consumerProps =
Component === 'button'
? (rest as React.ButtonHTMLAttributes<HTMLButtonElement>)
: Component === 'a'
? (rest as React.AnchorHTMLAttributes<HTMLAnchorElement>)
: (rest as React.HTMLAttributes<HTMLSpanElement>)

return (
<Component
onKeyDown={(event: KeyboardEvent<HTMLSpanElement & HTMLAnchorElement & HTMLButtonElement>) => {
onKeyDown && onKeyDown(event)

if ((event.key === 'Backspace' || event.key === 'Delete') && onRemove) {
onRemove()
}
}}
className={clsx(classes.TokenBase, className)}
data-cursor-is-interactive={isTokenInteractive({
as: Component,
onClick: rest.onClick,
onFocus: rest.onFocus,
tabIndex: rest.tabIndex,
disabled: rest.disabled,
})}
data-size={size}
id={id?.toString()}
{...(Component === 'button'
? (rest as React.ButtonHTMLAttributes<HTMLButtonElement>)
: Component === 'a'
? (rest as React.AnchorHTMLAttributes<HTMLAnchorElement>)
: (rest as React.HTMLAttributes<HTMLSpanElement>))}
// TypeScript cannot resolve polymorphic ref types at compile time
// This assertion is safe because the ref will match the actual rendered element
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref={forwardedRef as any}
{...mergeProps(
{
onKeyDown: handleKeyDown,
className: clsx(classes.TokenBase, className),
'data-cursor-is-interactive': isTokenInteractive({
as: Component,
onClick: rest.onClick,
onFocus: rest.onFocus,
tabIndex: rest.tabIndex,
disabled: rest.disabled,
}),
'data-size': size,
id: id?.toString(),
},
consumerProps,
)}
/>
)
},
Expand Down
11 changes: 11 additions & 0 deletions packages/react/src/Token/__tests__/Token.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ const testTokenComponent = (Component: React.ComponentType<React.PropsWithChildr
expect(onRemoveMock).toHaveBeenCalled()
})

it('calls onRemove before the consumer onKeyDown handler', () => {
const calls: string[] = []
const {getByText} = HTMLRender(
<Component text="token" onRemove={() => calls.push('remove')} onKeyDown={() => calls.push('consumer')} />,
)

fireEvent.keyDown(getByText('token'), {key: 'Backspace'})

expect(calls).toEqual(['remove', 'consumer'])
})

it('adds className to rendered component', () => {
const {getByText} = HTMLRender(<Component text="token" className="testing-class" />)
const domNode = getByText('token')
Expand Down
7 changes: 2 additions & 5 deletions packages/react/src/VisuallyHidden/VisuallyHidden.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {clsx} from 'clsx'
import type React from 'react'
import {type HTMLAttributes} from 'react'
import {mergeProps} from '../utils/mergeProps'
import classes from './VisuallyHidden.module.css'

/**
Expand All @@ -14,11 +15,7 @@ import classes from './VisuallyHidden.module.css'
* @see https://www.scottohara.me/blog/2023/03/21/visually-hidden-hack.html
*/
export const VisuallyHidden = ({className, children, ...rest}: VisuallyHiddenProps) => {
return (
<span className={clsx(className, classes.VisuallyHidden)} {...rest}>
{children}
</span>
)
return <span {...mergeProps({className: clsx(classes.VisuallyHidden, className)}, rest)}>{children}</span>
}

export type VisuallyHiddenProps = React.PropsWithChildren<
Expand Down
Loading