Skip to content

Commit

Permalink
feat: Switchのchildrenを必須にし、label要素として紐づけることでa11yを改善する (#4874)
Browse files Browse the repository at this point in the history
  • Loading branch information
AtsushiM authored Sep 5, 2024
1 parent 335fcd8 commit eb3b2c0
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 31 deletions.
23 changes: 18 additions & 5 deletions packages/smarthr-ui/src/components/Switch/Switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,37 @@ export const Default: StoryFn = () => (
<Section>
<Stack gap={0.5} align="flex-start">
<Heading type="blockTitle">標準</Heading>
<Switch onChange={action('clicked')} />
<Switch onChange={action('clicked')}>ラベル</Switch>
</Stack>
</Section>
<Section>
<Stack gap={0.5} align="flex-start">
<Heading type="blockTitle">ラベルをVisuallyHidden</Heading>
<Switch dangerouslyLabelHidden={true} onChange={action('clicked')}>
非表示ラベル
</Switch>
</Stack>
</Section>
<Section>
<Stack gap={0.5} align="flex-start">
<Heading type="blockTitle">デフォルト値変更</Heading>
<Switch defaultChecked={true} onChange={action('clicked')} />
<Switch defaultChecked={true} onChange={action('clicked')}>
ラベル
</Switch>
</Stack>
</Section>
<Section>
<Stack gap={0.5} align="flex-start">
<Heading type="blockTitle">disabled</Heading>
<Cluster>
<Switch disabled onChange={action('clicked')} />
<Switch disabled defaultChecked={true} onChange={action('clicked')} />
<Switch disabled onChange={action('clicked')}>
ラベル1
</Switch>
<Switch disabled defaultChecked={true} onChange={action('clicked')}>
ラベル2
</Switch>
</Cluster>
</Stack>
</Section>
<p>※ 実際に使う場合には必ずラベルを指定してください。</p>
</Stack>
)
53 changes: 39 additions & 14 deletions packages/smarthr-ui/src/components/Switch/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { InputHTMLAttributes, forwardRef, useMemo } from 'react'
import React, { InputHTMLAttributes, ReactNode, forwardRef, useMemo } from 'react'
import { tv } from 'tailwind-variants'

import { useId } from '../../hooks/useId'
import { FaCheckIcon } from '../Icon'
import { Cluster } from '../Layout'
import { Text } from '../Text'
import { VisuallyHiddenText } from '../VisuallyHiddenText'

const switchStyle = tv({
slots: {
Expand Down Expand Up @@ -42,17 +46,38 @@ const switchStyle = tv({
},
})

type Props = InputHTMLAttributes<HTMLInputElement>
type Props = InputHTMLAttributes<HTMLInputElement> & {
children: ReactNode
/** ラベルを視覚的に隠すかどうか */
dangerouslyLabelHidden?: boolean
}

export const Switch = forwardRef<HTMLInputElement, Props>(({ className, ...props }, ref) => {
const { wrapper, input, icon, iconWrapper } = useMemo(() => switchStyle(), [])
return (
<span className={wrapper({ className })}>
{/* eslint-disable-next-line smarthr/a11y-input-has-name-attribute */}
<input {...props} type="checkbox" role="switch" className={input()} ref={ref} />
<span className={iconWrapper()}>
<FaCheckIcon className={icon()} size="XXS" />
</span>
</span>
)
})
export const Switch = forwardRef<HTMLInputElement, Props>(
({ children, dangerouslyLabelHidden, className, id, ...props }, ref) => {
const { wrapper, input, icon, iconWrapper } = useMemo(() => switchStyle(), [])
const ActualLabelComponent = dangerouslyLabelHidden ? VisuallyHiddenText : Text
const inputId = useId(id)

return (
<Cluster align="center">
<ActualLabelComponent as="label" htmlFor={inputId}>
{children}
</ActualLabelComponent>
<span className={wrapper({ className })}>
{/* eslint-disable-next-line smarthr/a11y-input-has-name-attribute */}
<input
{...props}
type="checkbox"
role="switch"
id={inputId}
className={input()}
ref={ref}
/>
<span className={iconWrapper()}>
<FaCheckIcon className={icon()} size="XXS" />
</span>
</span>
</Cluster>
)
},
)
10 changes: 5 additions & 5 deletions packages/smarthr-ui/src/components/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,24 +81,24 @@ const text = tv({
})

// VariantProps を使うとコメントが書けない〜🥹
export type TextProps = VariantProps<typeof text> & {
export type TextProps<T extends React.ElementType = 'span'> = VariantProps<typeof text> & {
/** テキストコンポーネントの HTML タグ名。初期値は span */
as?: string | React.ComponentType<any> | undefined
as?: T
/** 強調するかどうかの真偽値。指定すると em 要素になる */
emphasis?: boolean
/** 見た目の種類 */
styleType?: StyleType
}

export const Text: React.FC<PropsWithChildren<TextProps & ComponentProps<'span'>>> = ({
export const Text = <T extends React.ElementType = 'span'>({
emphasis,
styleType,
weight = emphasis ? 'bold' : undefined,
as: Component = emphasis ? 'em' : 'span',
...props
}) => {
}: PropsWithChildren<TextProps<T> & ComponentProps<T>>) => {
const { size, italic, color, leading, whiteSpace, className, ...others } = props
const styleTypeValues = styleType ? STYLE_TYPE_MAP[styleType] : null
const styleTypeValues = styleType ? STYLE_TYPE_MAP[styleType as StyleType] : null

const styles = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ export const visuallyHiddenText = tv({
base: 'shr-absolute -shr-top-px shr-left-0 shr-h-px shr-w-px shr-overflow-hidden shr-whitespace-nowrap shr-border-0 shr-p-0 [clip-path:inset(100%)] [clip:rect(0_0_0_0)]',
})

export const VisuallyHiddenText: React.FC<
PropsWithChildren<
{
as?: string | React.ComponentType<any>
} & ComponentProps<'span'>
>
> = ({ as: Component = 'span', className, ...props }) => {
type Props<T extends React.ElementType> = PropsWithChildren<{
as?: T
}> &
ComponentProps<T>

export const VisuallyHiddenText = <T extends React.ElementType = 'span'>({
as: Component = 'span',
className,
...props
}: Props<T>) => {
const styles = useMemo(() => visuallyHiddenText({ className }), [className])

return <Component {...props} className={styles} />
Expand Down

0 comments on commit eb3b2c0

Please sign in to comment.