Skip to content

Commit

Permalink
chore: 共通部分をcustom hook化
Browse files Browse the repository at this point in the history
  • Loading branch information
AtsushiM committed Sep 19, 2024
1 parent 0237d6f commit a00ce96
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 224 deletions.
203 changes: 26 additions & 177 deletions packages/smarthr-ui/src/components/Fieldset/Fieldset.tsx
Original file line number Diff line number Diff line change
@@ -1,122 +1,19 @@
import React, {
type ComponentProps,
type ComponentPropsWithoutRef,
type PropsWithChildren,
type ReactNode,
useEffect,
useMemo,
useRef,
} from 'react'
import { useId } from 'react'
import { tv } from 'tailwind-variants'
import React, { type ComponentPropsWithoutRef } from 'react'

import {
ErrorMessageList,
ExampleMessageText,
HelpMessageParagraph,
type Props,
SupplementaryMessageText,
TitleCluster,
childrenWrapper,
formGroup,
useFormControl,
} from '../FormControl'
import { FaCircleExclamationIcon } from '../Icon'
import { Cluster, Stack } from '../Layout'
import { StatusLabel } from '../StatusLabel'
import { Text, TextProps } from '../Text'
import { visuallyHiddenText } from '../VisuallyHiddenText/VisuallyHiddenText'
import { Stack } from '../Layout'

import type { Gap } from '../../types'

type StatusLabelProps = ComponentProps<typeof StatusLabel>

type Props = PropsWithChildren<{
/** グループのタイトル名 */
title: ReactNode
/** タイトルの見出しのタイプ */
titleType?: TextProps['styleType']
/** タイトルの見出しを視覚的に隠すかどうか */
dangerouslyTitleHidden?: boolean
/** label 要素に適用する `htmlFor` 値 */
htmlFor?: string
/** label 要素に適用する `id` 値 */
labelId?: string
/** タイトル群と子要素の間の間隔調整用(基本的には不要) */
innerMargin?: Gap
/** タイトルの隣に表示する `StatusLabel` の Props の配列 */
statusLabelProps?: StatusLabelProps | StatusLabelProps[]
/** タイトルの下に表示するヘルプメッセージ */
helpMessage?: ReactNode
/** タイトルの下に表示する入力例 */
exampleMessage?: ReactNode
/** タイトルの下に表示するエラーメッセージ */
errorMessages?: ReactNode | ReactNode[]
/** エラーがある場合に自動的に入力要素を error にするかどうか */
autoBindErrorInput?: boolean
/** フォームコントロールの下に表示する補足メッセージ */
supplementaryMessage?: ReactNode
/** `true` のとき、文字色を `TEXT_DISABLED` にする */
disabled?: boolean
}>
type ElementProps = Omit<ComponentPropsWithoutRef<'div'>, keyof Props | 'aria-labelledby'>

const formGroup = tv({
slots: {
wrapper: [
'smarthr-ui-FormControl',
'shr-mx-[unset] shr-border-none shr-p-[unset]',
'disabled:shr-text-disabled',
'[&:disabled_.smarthr-ui-FormControl-label_>_span]:shr-text-disabled',
'[&:disabled_.smarthr-ui-FormControl-errorMessage]:shr-text-color-inherit',
'[&:disabled_.smarthr-ui-FormControl-supplementaryMessage]:shr-text-color-inherit',
'[&:disabled_.smarthr-ui-Input]:shr-border-default/50 [&:disabled_.smarthr-ui-Input]:shr-bg-white-darken',
],
label: [
'smarthr-ui-FormControl-label',
// flex-item が stretch してクリッカブル領域が広がりすぎないようにする
'shr-self-start',
'shr-px-[unset]',
],
errorList: ['shr-list-none'],
errorIcon: ['smarthr-ui-FormControl-errorMessage', 'shr-text-danger'],
},
})

const childrenWrapper = tv({
variants: {
innerMargin: {
0: '[&&&]:shr-mt-0',
0.25: '[&&&]:shr-mt-0.25',
0.5: '[&&&]:shr-mt-0.5',
0.75: '[&&&]:shr-mt-0.75',
1: '[&&&]:shr-mt-1',
1.25: '[&&&]:shr-mt-1.25',
1.5: '[&&&]:shr-mt-1.5',
2: '[&&&]:shr-mt-2',
2.5: '[&&&]:shr-mt-2.5',
3: '[&&&]:shr-mt-3',
3.5: '[&&&]:shr-mt-3.5',
4: '[&&&]:shr-mt-4',
8: '[&&&]:shr-mt-8',
X3S: '[&&&]:shr-mt-0.25',
XXS: '[&&&]:shr-mt-0.5',
XS: '[&&&]:shr-mt-1',
S: '[&&&]:shr-mt-1.5',
M: '[&&&]:shr-mt-2',
L: '[&&&]:shr-mt-2.5',
XL: '[&&&]:shr-mt-3',
XXL: '[&&&]:shr-mt-3.5',
X3L: '[&&&]:shr-mt-4',
} as { [key in Gap]: string },
},
compoundVariants: [
{
innerMargin: undefined,
className: '[:not([hidden])_~_&&&]:shr-mt-1',
},
{
innerMargin: undefined,
className: '[:not([hidden])_~_&&&]:shr-mt-0.5',
},
],
})
type ElementProps = Omit<ComponentPropsWithoutRef<'fieldset'>, keyof Props | 'aria-labelledby'>

export const Fieldset: React.FC<Props & ElementProps> = ({
title,
Expand All @@ -135,80 +32,32 @@ export const Fieldset: React.FC<Props & ElementProps> = ({
children,
...props
}) => {
const defaultHtmlFor = useId()
const defaultLabelId = useId()
const managedHtmlFor = htmlFor || defaultHtmlFor
const managedLabelId = labelId || defaultLabelId
const inputWrapperRef = useRef<HTMLDivElement>(null)
const statusLabelList = Array.isArray(statusLabelProps) ? statusLabelProps : [statusLabelProps]

const describedbyIds = useMemo(() => {
const temp = []

if (helpMessage) {
temp.push(`${managedHtmlFor}_helpMessage`)
}
if (exampleMessage) {
temp.push(`${managedHtmlFor}_exampleMessage`)
}
if (supplementaryMessage) {
temp.push(`${managedHtmlFor}_supplementaryMessage`)
}
if (errorMessages) {
temp.push(`${managedHtmlFor}_errorMessages`)
}

return temp.join(' ')
}, [helpMessage, exampleMessage, supplementaryMessage, errorMessages, managedHtmlFor])
const actualErrorMessages = useMemo(() => {
if (!errorMessages) {
return []
}

return Array.isArray(errorMessages) ? errorMessages : [errorMessages]
}, [errorMessages])

const { wrapperStyle, labelStyle, errorListStyle, errorIconStyle, childrenWrapperStyle } =
useMemo(() => {
const { wrapper, label, errorList, errorIcon } = formGroup()
return {
wrapperStyle: wrapper({ className }),
labelStyle: label({ className: dangerouslyTitleHidden ? visuallyHiddenText() : '' }),
errorListStyle: errorList(),
errorIconStyle: errorIcon(),
childrenWrapperStyle: childrenWrapper({ innerMargin }),
}
}, [className, dangerouslyTitleHidden, innerMargin])

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])
const {
managedHtmlFor,
managedLabelId,
inputWrapperRef,
statusLabelList,
describedbyIds,
actualErrorMessages,
styles: { wrapperStyle, labelStyle, errorListStyle, errorIconStyle, childrenWrapperStyle },
} = useFormControl({
htmlFor,
labelId,
statusLabelProps,
helpMessage,
exampleMessage,
supplementaryMessage,
errorMessages,
autoBindErrorInput,
})

return (
<Stack
{...props}
as="fieldset"
gap={innerMargin ?? 0.5}
aria-labelledby={managedLabelId}
aria-describedby={describedbyIds ? describedbyIds : undefined}
aria-describedby={describedbyIds || undefined}
className={wrapperStyle}
>
<TitleCluster
Expand Down
Loading

0 comments on commit a00ce96

Please sign in to comment.