From 2c17a17648547687d1450a22ecd43bd48e27542e Mon Sep 17 00:00:00 2001 From: Mizoue Atsushi Date: Thu, 19 Sep 2024 14:40:52 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20FormControl=E3=81=AElabel=E8=87=AA?= =?UTF-8?q?=E5=8B=95=E7=B4=90=E3=81=A5=E3=81=91=E3=81=8C=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E3=81=AA=E5=A0=B4=E5=90=88=E3=81=AE=E5=AF=BE=E5=BF=9C=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#4918)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/FormControl/FormControl.tsx | 266 ++++++++++++------ 1 file changed, 185 insertions(+), 81 deletions(-) diff --git a/packages/smarthr-ui/src/components/FormControl/FormControl.tsx b/packages/smarthr-ui/src/components/FormControl/FormControl.tsx index 3e1e6a7332..092dc36e97 100644 --- a/packages/smarthr-ui/src/components/FormControl/FormControl.tsx +++ b/packages/smarthr-ui/src/components/FormControl/FormControl.tsx @@ -154,14 +154,31 @@ export const ActualFormControl: React.FC = ({ const isRoleGroup = as === 'fieldset' const statusLabelList = Array.isArray(statusLabelProps) ? statusLabelProps : [statusLabelProps] - const describedbyIds = useMemo( - () => - Object.entries({ helpMessage, exampleMessage, supplementaryMessage, errorMessages }) - .filter(({ 1: value }) => value) - .map(([key]) => `${managedHtmlFor}_${key}`) - .join(' '), - [helpMessage, exampleMessage, supplementaryMessage, errorMessages, managedHtmlFor], - ) + 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(' ') + }, [ + isRoleGroup, + helpMessage, + exampleMessage, + supplementaryMessage, + errorMessages, + managedHtmlFor, + ]) const actualErrorMessages = useMemo(() => { if (!errorMessages) { return [] @@ -183,16 +200,42 @@ export const ActualFormControl: React.FC = ({ }, [className, dangerouslyTitleHidden, innerMargin, isRoleGroup]) useEffect(() => { + if (isRoleGroup) { + return + } + const inputWrapper = inputWrapperRef?.current if (inputWrapper) { + // HINT: 対象idを持つ要素が既に存在する場合、何もしない + if (document.getElementById(managedHtmlFor)) { + return + } + const input = inputWrapper.querySelector('[data-smarthr-ui-input="true"]') if (input && !input.getAttribute('id')) { input.setAttribute('id', managedHtmlFor) } } - }, [managedHtmlFor]) + }, [managedHtmlFor, isRoleGroup]) + + useEffect(() => { + const inputWrapper = inputWrapperRef?.current + + if (inputWrapper) { + // HINT: 対象idを持つ要素が既に存在する場合、何もしない + if (!describedbyIds || inputWrapper.querySelector(`[aria-describedby="${describedbyIds}"]`)) { + return + } + + const input = inputWrapper.querySelector('[data-smarthr-ui-input="true"]') + + if (input && !input.getAttribute('aria-describedby')) { + input.setAttribute('aria-describedby', describedbyIds) + } + } + }, [describedbyIds, isRoleGroup]) return ( = ({ aria-describedby={isRoleGroup && describedbyIds ? describedbyIds : undefined} className={wrapperStyle} > - - - {helpMessage && ( -

- {helpMessage} -

- )} - {exampleMessage && ( - - {exampleMessage} - - )} - - {actualErrorMessages.length > 0 && ( - - )} - + + + +
{decorateFirstInputElement(children, { - describedbyIds, error: autoBindErrorInput && actualErrorMessages.length > 0, })}
- - {supplementaryMessage && ( - - {supplementaryMessage} - - )} +
) } +const TitleCluster = React.memo< + Pick & { + titleType: TextProps['styleType'] + statusLabelList: StatusLabelProps[] + isRoleGroup: boolean + managedHtmlFor: string + managedLabelId: string + labelStyle: string + } +>( + ({ + isRoleGroup, + managedHtmlFor, + managedLabelId, + labelStyle, + dangerouslyTitleHidden, + titleType, + title, + statusLabelList, + }) => ( + + ), +) + +const HelpMessageParagraph = React.memo & { managedHtmlFor: string }>( + ({ helpMessage, managedHtmlFor }) => + helpMessage ? ( +

+ {helpMessage} +

+ ) : null, +) + +const ExampleMessageText = React.memo & { managedHtmlFor: string }>( + ({ exampleMessage, managedHtmlFor }) => + exampleMessage ? ( + + {exampleMessage} + + ) : null, +) + +const ErrorMessageList = React.memo<{ + errorMessages: ReactNode[] + managedHtmlFor: string + errorListStyle: string + errorIconStyle: string +}>(({ errorMessages, managedHtmlFor, errorListStyle, errorIconStyle }) => { + if (errorMessages.length === 0) { + return null + } + + return ( + + ) +}) + +const SupplementaryMessageText = React.memo< + Pick & { managedHtmlFor: string } +>(({ supplementaryMessage, managedHtmlFor }) => + supplementaryMessage ? ( + + {supplementaryMessage} + + ) : null, +) + type DecorateFirstInputElementParams = { - describedbyIds: string error: boolean } @@ -282,7 +389,12 @@ const decorateFirstInputElement = ( children: ReactNode, params: DecorateFirstInputElementParams, ) => { - const { describedbyIds, error } = params + const { error } = params + + if (!error) { + return children + } + let foundFirstInput = false const decorate = (targets: ReactNode): ReactNode[] | ReactNode => @@ -296,15 +408,7 @@ const decorateFirstInputElement = ( foundFirstInput = true - const inputAttributes: ComponentProps = {} - if (error) { - inputAttributes.error = true - } - if (describedbyIds !== '') { - inputAttributes['aria-describedby'] = describedbyIds - } - - return React.cloneElement(child, inputAttributes) + return React.cloneElement(child, { error: true } as ComponentProps) }) return decorate(children)