Skip to content

Commit

Permalink
feat: 入力要素のエラー表示を、aria-invalidをベースにした計算方法に修正する
Browse files Browse the repository at this point in the history
  • Loading branch information
AtsushiM committed Sep 17, 2024
1 parent f25b69f commit c402dbd
Show file tree
Hide file tree
Showing 16 changed files with 95 additions and 149 deletions.
17 changes: 4 additions & 13 deletions packages/smarthr-ui/src/components/CheckBox/CheckBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const checkbox = tv({
'peer-disabled:peer-indeterminate:shr-border-default peer-disabled:peer-indeterminate:shr-bg-border',
'peer-focus-visible:shr-focus-indicator',
'peer-hover:shr-shadow-input-hover',
'shr-border-default',
'peer-[[aria-invalid]]:shr-border-danger',
],
input: [
'smarthr-ui-CheckBox-checkBox shr-peer shr-absolute shr-left-0 shr-top-0 shr-m-0 shr-h-full shr-w-full shr-cursor-pointer shr-opacity-0 disabled:shr-pointer-events-none',
Expand All @@ -63,17 +65,6 @@ const checkbox = tv({
label: 'shr-pointer-events-none shr-cursor-not-allowed shr-text-disabled',
},
},
error: {
true: {
box: 'shr-border-danger',
},
false: {
box: 'shr-border-default',
},
},
},
defaultVariants: {
error: false,
},
})

Expand All @@ -92,13 +83,13 @@ export const CheckBox = forwardRef<HTMLInputElement, Props>(
return {
wrapperStyle: wrapper({ className }),
innerWrapperStyle: innerWrapper(),
boxStyle: box({ error }),
boxStyle: box(),
inputStyle: input(),
iconWrapStyle: iconWrap(),
iconStyle: icon(),
labelStyle: label({ disabled: props.disabled }),
}
}, [className, error, props.disabled])
}, [className, props.disabled])

const handleChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
(e) => {
Expand Down
10 changes: 2 additions & 8 deletions packages/smarthr-ui/src/components/ComboBox/MultiComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const multiCombobox = tv({
'smarthr-ui-MultiComboBox',
'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',
'contrast-more:shr-border-high-contrast',
'has-[[aria-invalid]]:shr-border-danger',
],
inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5 shr-overflow-y-auto',
selectedList:
Expand All @@ -103,11 +104,6 @@ const multiCombobox = tv({
wrapper: 'shr-focus-indicator',
},
},
error: {
true: {
wrapper: 'shr-border-danger',
},
},
disabled: {
true: {
wrapper:
Expand All @@ -125,7 +121,6 @@ const multiCombobox = tv({
},
compoundVariants: [
{
error: false,
disabled: false,
className: {
wrapper: 'shr-border-default',
Expand Down Expand Up @@ -423,7 +418,7 @@ const ActualMultiComboBox = <T,>(
...style,
width: widthStyle,
},
className: wrapper({ focused: isFocused, error, disabled, className }),
className: wrapper({ focused: isFocused, disabled, className }),
},
inputAreaStyle: inputArea(),
selectedListStyle: selectedList(),
Expand All @@ -436,7 +431,6 @@ const ActualMultiComboBox = <T,>(
}, [
className,
disabled,
error,
input,
inputArea,
inputWrapper,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export const MultiCombobox: StoryFn = () => {
data-test="multi-combobox-disabled"
/>
</FormControl>
<FormControl title="エラー表示">
<FormControl title="エラー表示" autoBindErrorInput={false}>
<MultiComboBox
name="error"
items={items}
Expand All @@ -181,6 +181,16 @@ export const MultiCombobox: StoryFn = () => {
onSelect={handleSelectItem}
/>
</FormControl>
<FormControl title="エラー表示 with FormControl" errorMessages={['エラーメッセージ']}>
<MultiComboBox
name="error"
items={items}
selectedItems={selectedItems}
dropdownHelpMessage="入力でフィルタリングできます。"
onDelete={handleDelete}
onSelect={handleSelectItem}
/>
</FormControl>
<FormControl title="選択済みアイテムを省略表示 + ツールチップ">
<MultiComboBox
name="selectedItemEllipsis"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,6 @@ const ActualSingleComboBox = <T,>(
aria-haspopup="listbox"
aria-controls={listBoxId}
aria-expanded={isFocused}
aria-invalid={error || undefined}
aria-activedescendant={activeOption?.id}
aria-autocomplete="list"
className={inputStyle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export const SingleCombobox: StoryFn = () => {
aria-label="inputAttributes"
/>
</FormControl>
<FormControl title="エラー表示">
<FormControl title="エラー表示" autoBindErrorInput={false}>
<SingleComboBox
name="error"
items={items}
Expand All @@ -186,6 +186,16 @@ export const SingleCombobox: StoryFn = () => {
onClear={handleClear}
/>
</FormControl>
<FormControl title="エラー表示 with FormControl" errorMessages={['エラーメッセージ']}>
<SingleComboBox
name="error"
items={items}
selectedItem={selectedItem}
dropdownHelpMessage="入力でフィルタリングできます。"
onSelect={handleSelectItem}
onClear={handleClear}
/>
</FormControl>
<FormControl title="読込中">
<SingleComboBox
name="isLoading"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ export const All: StoryFn = () => {
<BottomFormControl title="Place on the page bottom">
<DatePicker name="place_on_the_page_bottom" onChangeDate={action('change')} />
</BottomFormControl>
<FormControl title="error" autoBindErrorInput={false}>
<DatePicker name="error" error={true} />
</FormControl>
<FormControl title="error with FormControl" errorMessages={['エラーメッセージ']}>
<DatePicker name="error" />
</FormControl>
</Stack>
)
}
Expand Down
97 changes: 22 additions & 75 deletions packages/smarthr-ui/src/components/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,6 @@ export const ActualFormControl: React.FC<Props & ElementProps> = ({
}
}
}, [managedHtmlFor])

useEffect(() => {
const inputWrapper = inputWrapperRef?.current

Expand All @@ -223,6 +222,27 @@ export const ActualFormControl: React.FC<Props & ElementProps> = ({
}
}
}, [describedbyIds])
useEffect(() => {
if (!autoBindErrorInput) {
return
}

const inputWrapper = inputWrapperRef?.current

if (inputWrapper) {
const input = inputWrapper.querySelector('[data-smarthr-ui-input="true"]')

if (!input) {
return
}

if (actualErrorMessages.length > 0) {
input.setAttribute('aria-invalid', 'true')
} else {
input.removeAttribute('aria-invalid')
}
}
}, [actualErrorMessages.length, autoBindErrorInput])

return (
<Stack
Expand Down Expand Up @@ -252,9 +272,7 @@ export const ActualFormControl: React.FC<Props & ElementProps> = ({
errorIconStyle={errorIconStyle}
/>
<div className={childrenWrapperStyle} ref={inputWrapperRef}>
{decorateFirstInputElement(children, {
error: autoBindErrorInput && actualErrorMessages.length > 0,
})}
{children}
</div>
<SupplementaryMessageText
supplementaryMessage={supplementaryMessage}
Expand Down Expand Up @@ -368,76 +386,5 @@ const SupplementaryMessageText = React.memo<
) : null,
)

type DecorateFirstInputElementParams = {
error: boolean
}

const decorateFirstInputElement = (
children: ReactNode,
params: DecorateFirstInputElementParams,
) => {
const { error } = params

if (!error) {
return children
}

let foundFirstInput = false

const decorate = (targets: ReactNode): ReactNode[] | ReactNode =>
React.Children.map(targets, (child) => {
if (foundFirstInput || !React.isValidElement(child)) {
return child
}
if (!isInputElement(child)) {
return React.cloneElement(child, {}, decorate(child.props.children))
}

foundFirstInput = true

return React.cloneElement(child, { error: true } as ComponentProps<typeof Input>)
})

return decorate(children)
}

type InputComponent =
| typeof Input
| typeof CurrencyInput
| typeof Textarea
| typeof DatePicker
| typeof TimePicker
| typeof Select
| typeof SingleComboBox
| typeof MultiComboBox
| typeof InputFile
| typeof DropZone

/**
* - CheckBox / RadioButton は内部に label を含むため対象外
* - SearchInput は label を含むため対象外
* - InputWithTooltip は領域が狭く FormControl を置けない場所での使用を想定しているため対象外
*
* @param node
* @returns
*/
const isInputElement = (
element: ReactElement,
): element is React.ReactComponentElement<InputComponent> => {
const type = isStyledComponent(element.type) ? element.type.target : element.type
return (
type === Input ||
type === CurrencyInput ||
type === Textarea ||
type === DatePicker ||
type === TimePicker ||
type === Select ||
type === SingleComboBox ||
type === MultiComboBox ||
type === InputFile ||
type === DropZone
)
}

export const FormControl: React.FC<Omit<Props & ElementProps, 'as' | 'disabled'>> =
ActualFormControl
8 changes: 3 additions & 5 deletions packages/smarthr-ui/src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,12 @@ const wrapper = tv({
'shr-border-shorthand shr-box-border shr-inline-flex shr-cursor-text shr-items-center shr-gap-0.5 shr-rounded-m shr-bg-white shr-px-0.5',
'contrast-more:shr-border-high-contrast',
'focus-within:shr-focus-indicator',
'has-[[aria-invalid]]:shr-border-danger',
],
variants: {
disabled: {
true: 'shr-pointer-events-none shr-bg-white-darken [&&&]:shr-border-default/50',
},
error: {
true: '[&]:shr-border-danger',
},
readOnly: {
true: '[&&&]:shr-border-[theme(backgroundColor.background)] [&&&]:shr-bg-background',
},
Expand Down Expand Up @@ -132,7 +130,7 @@ export const Input = forwardRef<HTMLInputElement, Props & ElementProps>(
const { backgroundColor } = useTheme()

const wrapperStyleProps = useMemo(() => {
const wrapperStyle = wrapper({ disabled, error, readOnly, className })
const wrapperStyle = wrapper({ disabled, readOnly, className })
const color = bgColor
? backgroundColor[bgColors[bgColor] as keyof typeof backgroundColor]
: undefined
Expand All @@ -144,7 +142,7 @@ export const Input = forwardRef<HTMLInputElement, Props & ElementProps>(
width: typeof width === 'number' ? `${width}px` : width,
},
}
}, [backgroundColor, bgColor, className, disabled, error, readOnly, width])
}, [backgroundColor, bgColor, className, disabled, readOnly, width])
const { input, affix } = inner()

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@ export const All: StoryFn = () => (
<FormControl title="Disabled input">
<InputFile name="disabled" label="ファイルを選択" disabled />
</FormControl>
<FormControl title="エラー">
<FormControl title="エラー" autoBindErrorInput={false}>
<InputFile name="error" label="ファイルを選択" error />
</FormControl>
<FormControl title="エラー with FormControl" errorMessages={['エラーメッセージ']}>
<InputFile name="error" label="ファイルを選択" />
</FormControl>
<FormControl title="decoratorで文言変更">
<InputFile
name="decorator"
Expand Down
11 changes: 4 additions & 7 deletions packages/smarthr-ui/src/components/InputFile/InputFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const inputFile = tv({
'shr-border-shorthand shr-relative shr-inline-flex shr-rounded-m shr-bg-white shr-font-bold shr-leading-none',
'contrast-more:shr-border-high-contrast',
'focus-within:shr-focus-indicator',
'has-[[aria-invalid]]:shr-border-danger',
],
input: [
'smarthr-ui-InputFile-input',
Expand All @@ -53,11 +54,6 @@ const inputFile = tv({
inputWrapper: 'hover:shr-border-darken hover:shr-bg-white-darken hover:shr-text-black',
},
},
error: {
true: {
inputWrapper: '[&&&]:shr-border-danger',
},
},
},
})

Expand All @@ -70,6 +66,7 @@ export type Props = VariantProps<typeof inputFile> & {
hasFileList?: boolean
/** コンポーネント内のテキストを変更する関数 */
decorators?: DecoratorsType<'destroy'>
error?: boolean
}
type ElementProps = Omit<ComponentPropsWithRef<'input'>, keyof Props>

Expand All @@ -96,8 +93,8 @@ export const InputFile = forwardRef<HTMLInputElement, Props & ElementProps>(
const { wrapper, fileList, fileItem, inputWrapper, input, prefix } = inputFile()
const wrapperStyle = useMemo(() => wrapper({ className }), [className, wrapper])
const inputWrapperStyle = useMemo(
() => inputWrapper({ size, disabled, error }),
[disabled, error, inputWrapper, size],
() => inputWrapper({ size, disabled }),
[disabled, inputWrapper, size],
)

// Safari において、input.files への直接代入時に onChange が発火することを防ぐためのフラグ
Expand Down
7 changes: 6 additions & 1 deletion packages/smarthr-ui/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ export const All: StoryFn = () => (
</FormControl>
</li>
<li>
<FormControl title="エラー状態">
<FormControl title="エラー状態" autoBindErrorInput={false}>
<Select name="error" error options={options} />
</FormControl>
</li>
<li>
<FormControl title="エラー状態 with FormControl" errorMessages={['エラーメッセージ']}>
<Select name="error" options={options} />
</FormControl>
</li>
<li>
<FormControl title="disabled 状態">
<Select name="disabled" disabled options={options} />
Expand Down
Loading

0 comments on commit c402dbd

Please sign in to comment.