{"version":3,"file":"FormControl.cjs","sources":["../../../src/components/FormControl/FormControl.tsx"],"sourcesContent":["'use client'\n\nimport {\n  type ComponentProps,\n  type ComponentPropsWithoutRef,\n  type ComponentType,\n  type FC,\n  type FunctionComponentElement,\n  type PropsWithChildren,\n  type ReactNode,\n  memo,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { useId } from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useObjectAttributes } from '../../hooks/useObjectAttributes'\nimport { FaCircleExclamationIcon } from '../Icon'\nimport { Cluster, Stack } from '../Layout'\nimport { Text, type TextProps } from '../Text'\nimport { VisuallyHiddenText, visuallyHiddenTextClassName } from '../VisuallyHiddenText'\n\nimport type { Gap } from '../../types'\nimport type { StatusLabel } from '../StatusLabel'\n\ntype StatusLabelType = FunctionComponentElement<ComponentProps<typeof StatusLabel>>\ntype IconType = ComponentProps<typeof Text>['icon']\n\ntype ObjectLabelType = {\n  text: ReactNode\n  /** ラベルの表示タイプ */\n  styleType?: TextProps['styleType']\n  /** ラベル左に設置するアイコン */\n  icon?: IconType\n  /** ラベルを視覚的に隠すかどうか */\n  unrecommendedHide?: boolean\n  /** ラベルを紐づける入力要素のID属性と同じ値 */\n  htmlFor?: string\n  /** ラベルに適用する `id` 値 */\n  id?: string\n}\ntype AbstractProps = PropsWithChildren<{\n  /** グループのラベル名 */\n  label: ReactNode | ObjectLabelType\n  /** タイトル右の領域 */\n  subActionArea?: ReactNode\n  /** タイトル群と子要素の間の間隔調整用（基本的には不要） */\n  innerMargin?: Gap\n  /** タイトルの隣に表示する `StatusLabel` の配列 */\n  statusLabels?: StatusLabelType | StatusLabelType[]\n  /** タイトルの下に表示するヘルプメッセージ */\n  helpMessage?: ReactNode\n  /** タイトルの下に表示する入力例 */\n  exampleMessage?: ReactNode\n  /** タイトルの下に表示するエラーメッセージ */\n  errorMessages?: ReactNode | ReactNode[]\n  /** エラーがある場合に自動的に入力要素を error にするかどうか */\n  autoBindErrorInput?: boolean\n  /** フォームコントロールの下に表示する補足メッセージ */\n  supplementaryMessage?: ReactNode\n  /** `true` のとき、文字色を `TEXT_DISABLED` にする */\n  disabled?: boolean\n  as?: string | ComponentType<any>\n}>\ntype Props = AbstractProps &\n  Omit<ComponentPropsWithoutRef<'div'>, keyof AbstractProps | 'aria-labelledby'>\n\nconst labelObjectConverter = (label: ReactNode) => ({ text: label })\n\nconst classNameGenerator = tv({\n  slots: {\n    wrapper: [\n      'smarthr-ui-FormControl',\n      'shr-mx-[unset] shr-border-none shr-p-[unset]',\n      'disabled:shr-text-disabled',\n      '[&:disabled_.smarthr-ui-FormControl-label_>_span]:shr-text-disabled',\n      '[&:disabled_.smarthr-ui-FormControl-exampleMessage]:shr-text-color-inherit',\n      '[&:disabled_.smarthr-ui-FormControl-errorMessage-Icon]:shr-text-color-inherit',\n      '[&:disabled_.smarthr-ui-FormControl-supplementaryMessage]:shr-text-color-inherit',\n      '[&:disabled_.smarthr-ui-Input]:shr-border-default/50 [&:disabled_.smarthr-ui-Input]:shr-bg-white-darken',\n    ],\n    label: ['smarthr-ui-FormControl-label'],\n    errorList: ['shr-list-none'],\n    errorIcon: ['smarthr-ui-FormControl-errorMessage-Icon', 'shr-text-danger'],\n    errorMessage: ['smarthr-ui-FormControl-errorMessage'],\n    underLabelStack: ['[&&&]:shr-mt-0'],\n    childrenWrapper: [],\n  },\n  variants: {\n    innerMargin: {\n      0: {},\n      0.25: {},\n      0.5: {},\n      0.75: {},\n      1: {},\n      1.25: {},\n      1.5: {},\n      2: {},\n      2.5: {},\n      3: {},\n      3.5: {},\n      4: {},\n      8: {},\n      X3S: {},\n      XXS: {},\n      XS: {},\n      S: {},\n      M: {},\n      L: {},\n      XL: {},\n      XXL: {},\n      X3L: {},\n    } as { [key in Gap]: string },\n    isFieldset: {\n      true: {},\n      false: {},\n    },\n  },\n  compoundVariants: [\n    // TODO: innerMarginが未指定、初期値の場合、かつFieldsetの場合、childrenの上部の余白を広げることで\n    // FormControltとの差をわかりやすくしている\n    // 微妙な方法ではあるので、必要に応じてinnerMarginではない属性を用意する\n    // https://kufuinc.slack.com/archives/CGC58MW01/p1737944965871159?thread_ts=1737541173.404369&cid=CGC58MW01\n    {\n      innerMargin: undefined,\n      isFieldset: true,\n      class: {\n        childrenWrapper: '[:not([hidden])_~_&&&]:shr-mt-1',\n      },\n    },\n    {\n      innerMargin: undefined,\n      isFieldset: false,\n      class: {\n        childrenWrapper: '[:not([hidden])_~_&&&]:shr-mt-0.5',\n      },\n    },\n  ],\n})\n\nconst SMARTHR_UI_INPUT_SELECTOR = '[data-smarthr-ui-input=\"true\"]'\n\nexport const ActualFormControl: FC<Props> = ({\n  label: orgLabel,\n  subActionArea,\n  innerMargin,\n  statusLabels,\n  helpMessage,\n  exampleMessage,\n  errorMessages,\n  autoBindErrorInput = true,\n  supplementaryMessage,\n  as = 'div',\n  className,\n  children,\n  ...rest\n}) => {\n  const label = useObjectAttributes<ReactNode | ObjectLabelType, ObjectLabelType>(\n    orgLabel,\n    labelObjectConverter,\n  )\n  const defaultHtmlFor = useId()\n  const defaultLabelId = useId()\n  const [childInputId, setChildInputId] = useState<string>('')\n  const managedHtmlFor = label.htmlFor || childInputId || defaultHtmlFor\n  const managedLabelId = label.id || defaultLabelId\n  const inputWrapperRef = useRef<HTMLDivElement>(null)\n  const isFieldset = as === 'fieldset'\n\n  const describedbyIds = useMemo(() => {\n    const temp = []\n\n    if (helpMessage) {\n      temp.push(`${managedHtmlFor}_helpMessage`)\n    }\n    if (exampleMessage) {\n      temp.push(`${managedHtmlFor}_exampleMessage`)\n    }\n    if (supplementaryMessage) {\n      temp.push(`${managedHtmlFor}_supplementaryMessage`)\n    }\n    if (errorMessages) {\n      temp.push(`${managedHtmlFor}_errorMessages`)\n    }\n\n    return temp.join(' ')\n  }, [helpMessage, exampleMessage, supplementaryMessage, errorMessages, managedHtmlFor])\n\n  const actualStatusLabels = useMemo(\n    () => (statusLabels ? (Array.isArray(statusLabels) ? statusLabels : [statusLabels]) : []),\n    [statusLabels],\n  )\n\n  const actualErrorMessages = useMemo(() => {\n    if (!errorMessages) {\n      return []\n    }\n\n    return Array.isArray(errorMessages) ? errorMessages : [errorMessages]\n  }, [errorMessages])\n\n  const actualInnerMargin = useMemo(() => innerMargin ?? 0.5, [innerMargin])\n\n  const classNames = useMemo(() => {\n    const generators = classNameGenerator({ innerMargin, isFieldset })\n\n    return {\n      wrapper: generators.wrapper({ className }),\n      label: generators.label({\n        className: label.unrecommendedHide ? visuallyHiddenTextClassName : '',\n      }),\n      errorList: generators.errorList(),\n      errorIcon: generators.errorIcon(),\n      errorMessage: generators.errorMessage(),\n      underLabelStack: generators.underLabelStack(),\n      childrenWrapper: generators.childrenWrapper(),\n    }\n  }, [innerMargin, isFieldset, label.unrecommendedHide, className])\n\n  useEffect(() => {\n    if (\n      isFieldset ||\n      !inputWrapperRef?.current ||\n      // HINT: 対象idを持つ要素が既に存在する場合、何もしない\n      document.getElementById(managedHtmlFor)\n    ) {\n      return\n    }\n\n    const input = inputWrapperRef.current.querySelector(SMARTHR_UI_INPUT_SELECTOR)\n\n    if (!input) {\n      return\n    }\n\n    const inputId = input.getAttribute('id')\n\n    if (inputId) {\n      setChildInputId(inputId)\n    } else {\n      input.setAttribute('id', managedHtmlFor)\n    }\n\n    if (input instanceof HTMLInputElement && input.type === 'file') {\n      const attrName = 'aria-labelledby'\n      const inputLabelledByIds = input.getAttribute(attrName)\n\n      if (inputLabelledByIds) {\n        // InputFileの場合はlabel要素の可視ラベルをアクセシブルネームに含める\n        input.setAttribute(attrName, `${inputLabelledByIds} ${managedLabelId}`)\n      }\n    }\n  }, [managedHtmlFor, isFieldset, managedLabelId])\n\n  useEffect(() => {\n    if (!describedbyIds || !inputWrapperRef?.current) {\n      return\n    }\n\n    const inputWrapper = inputWrapperRef.current\n    const attrName = 'aria-describedby'\n\n    if (inputWrapper.querySelector(`[${attrName}=\"${describedbyIds}\"]`)) {\n      return\n    }\n\n    const input = inputWrapper.querySelector(SMARTHR_UI_INPUT_SELECTOR)\n\n    if (input) {\n      const attribute = input.getAttribute(attrName)\n\n      input.setAttribute(attrName, attribute ? `${attribute} ${describedbyIds}` : describedbyIds)\n    }\n  }, [describedbyIds])\n\n  useEffect(() => {\n    if (!autoBindErrorInput || !inputWrapperRef?.current) {\n      return\n    }\n\n    const input = inputWrapperRef.current.querySelector(SMARTHR_UI_INPUT_SELECTOR)\n\n    if (input) {\n      const attrName = 'aria-invalid'\n\n      if (actualErrorMessages.length > 0) {\n        input.setAttribute(attrName, 'true')\n      } else {\n        input.removeAttribute(attrName)\n      }\n    }\n  }, [actualErrorMessages.length, autoBindErrorInput])\n\n  // HINT: Fieldset内の可視ラベルが無いinputに、legend文言をアクセシブルネームに追加する\n  // https://waic.jp/translations/WCAG21/Understanding/label-in-name.html\n  useEffect(() => {\n    if (!isFieldset || !inputWrapperRef.current) return\n\n    const inputs =\n      inputWrapperRef.current.querySelectorAll<HTMLInputElement>(SMARTHR_UI_INPUT_SELECTOR)\n\n    if (!inputs.length) return\n\n    const legendText = innerText(label.text)\n\n    if (!legendText) return\n\n    inputs.forEach((input: HTMLInputElement) => {\n      const accessibleName =\n        input.getAttribute('aria-label') ||\n        (input.labels?.[0]?.classList.contains('smarthr-ui-VisuallyHiddenText')\n          ? input.labels![0].textContent\n          : '')\n\n      if (\n        accessibleName &&\n        !accessibleName.includes(legendText) &&\n        !legendText.includes(accessibleName)\n      ) {\n        input.setAttribute('aria-label', `${accessibleName} ${legendText}`)\n      }\n    })\n  }, [isFieldset, label.text])\n\n  let body = (\n    <>\n      <HelpMessageParagraph helpMessage={helpMessage} managedHtmlFor={managedHtmlFor} />\n      <ExampleMessageText exampleMessage={exampleMessage} managedHtmlFor={managedHtmlFor} />\n      <ErrorMessageList\n        errorMessages={actualErrorMessages}\n        managedHtmlFor={managedHtmlFor}\n        classNames={classNames}\n      />\n      <div className={classNames.childrenWrapper} ref={inputWrapperRef}>\n        {children}\n      </div>\n      <SupplementaryMessageText\n        supplementaryMessage={supplementaryMessage}\n        managedHtmlFor={managedHtmlFor}\n      />\n    </>\n  )\n\n  // HINT: label.unrecommendedHideの場合、body以下の余白の計算を簡略化するため\n  // Stackをネストし、そのStackに対してmargin-top: 0を指定する\n  // こうすることでinner Stack以下の要素は擬似的にStackの最初の要素になる\n  if (label.unrecommendedHide) {\n    body = (\n      <Stack gap={actualInnerMargin} className={classNames.underLabelStack}>\n        {body}\n      </Stack>\n    )\n  }\n\n  return (\n    <Stack\n      {...rest}\n      as={as}\n      gap={actualInnerMargin}\n      aria-describedby={isFieldset && describedbyIds ? describedbyIds : undefined}\n      className={classNames.wrapper}\n    >\n      <LabelCluster\n        isFieldset={isFieldset}\n        managedHtmlFor={managedHtmlFor}\n        managedLabelId={managedLabelId}\n        unrecommendedHideLabel={label.unrecommendedHide}\n        labelType={label.styleType}\n        label={label.text}\n        labelIcon={label.icon}\n        statusLabels={actualStatusLabels}\n        subActionArea={subActionArea}\n        labelClassName={classNames.label}\n      />\n      {body}\n    </Stack>\n  )\n}\n\nconst LabelCluster = memo<\n  Pick<Props, 'subActionArea'> & {\n    label: ReactNode\n    labelType: TextProps['styleType']\n    labelIcon?: IconType\n    unrecommendedHideLabel?: boolean\n    isFieldset: boolean\n    managedHtmlFor: string\n    managedLabelId: string\n    labelClassName: string\n    statusLabels: StatusLabelType[]\n  }\n>(\n  ({\n    isFieldset,\n    managedHtmlFor,\n    managedLabelId,\n    unrecommendedHideLabel,\n    labelType = 'blockTitle',\n    label,\n    labelIcon,\n    subActionArea,\n    labelClassName,\n    statusLabels,\n  }) => {\n    const body = (\n      <>\n        <Text styleType={labelType} icon={labelIcon}>\n          {label}\n        </Text>\n        <StatusLabelCluster statusLabels={statusLabels} />\n      </>\n    )\n\n    const attrs = useMemo(() => {\n      if (unrecommendedHideLabel) {\n        return {\n          label: null,\n          visuallyHidden: isFieldset\n            ? {\n                as: 'legend',\n              }\n            : {\n                as: 'label',\n                htmlFor: managedHtmlFor,\n                id: managedLabelId,\n              },\n        }\n      }\n\n      if (isFieldset) {\n        return {\n          label: { 'aria-hidden': 'true' } as const,\n          visuallyHidden: { as: 'legend' },\n        }\n      }\n\n      return {\n        label: {\n          as: 'label' as const,\n          htmlFor: managedHtmlFor,\n          id: managedLabelId,\n        },\n        visuallyHidden: null,\n      }\n    }, [managedLabelId, managedHtmlFor, unrecommendedHideLabel, isFieldset])\n\n    return (\n      <>\n        {attrs.visuallyHidden && (\n          <VisuallyHiddenText {...attrs.visuallyHidden}>\n            {\n              // HINT: innerTextでは正しく文字が取得できない場合がある\n              // 安全策としてinnerTextが空を取得してきたらbody自体を埋めこみます\n              innerText(body) || body\n            }\n          </VisuallyHiddenText>\n        )}\n        {attrs.label && (\n          <Cluster\n            justify=\"space-between\"\n            // HINT: UI上、常にトップの要素になるため、Stackの計算が狂わないよう、\n            // 常にmargin-topを0にする\n            className=\"[&&&]:shr--mt-0\"\n          >\n            <Cluster {...attrs.label} align=\"center\" className={labelClassName}>\n              {body}\n            </Cluster>\n            {subActionArea && <div className=\"shr-grow\">{subActionArea}</div>}\n          </Cluster>\n        )}\n      </>\n    )\n  },\n)\n\nconst StatusLabelCluster = memo<{ statusLabels: StatusLabelType[] }>(({ statusLabels }) =>\n  statusLabels.length === 0 ? null : (\n    <Cluster gap={0.25} as=\"span\">\n      {statusLabels}\n    </Cluster>\n  ),\n)\n\nconst HelpMessageParagraph = memo<Pick<Props, 'helpMessage'> & { managedHtmlFor: string }>(\n  ({ helpMessage, managedHtmlFor }) =>\n    helpMessage ? (\n      <p className=\"smarthr-ui-FormControl-helpMessage\" id={`${managedHtmlFor}_helpMessage`}>\n        {helpMessage}\n      </p>\n    ) : null,\n)\n\nconst ExampleMessageText = memo<Pick<Props, 'exampleMessage'> & { managedHtmlFor: string }>(\n  ({ exampleMessage, managedHtmlFor }) =>\n    exampleMessage ? (\n      <Text\n        as=\"p\"\n        color=\"TEXT_GREY\"\n        italic\n        id={`${managedHtmlFor}_exampleMessage`}\n        className=\"smarthr-ui-FormControl-exampleMessage\"\n      >\n        {exampleMessage}\n      </Text>\n    ) : null,\n)\n\nconst ErrorMessageList = memo<{\n  errorMessages: ReactNode[]\n  managedHtmlFor: string\n  classNames: {\n    errorList: string\n    errorIcon: string\n    errorMessage: string\n  }\n}>(({ errorMessages, managedHtmlFor, classNames }) =>\n  errorMessages.length > 0 ? (\n    <div id={`${managedHtmlFor}_errorMessages`} className={classNames.errorList} role=\"alert\">\n      {errorMessages.map((message, index) => (\n        <p key={index}>\n          <Text\n            className={classNames.errorMessage}\n            icon={<FaCircleExclamationIcon className={classNames.errorIcon} />}\n          >\n            {message}\n          </Text>\n        </p>\n      ))}\n    </div>\n  ) : null,\n)\n\nconst SupplementaryMessageText = memo<\n  Pick<Props, 'supplementaryMessage'> & { managedHtmlFor: string }\n>(({ supplementaryMessage, managedHtmlFor }) =>\n  supplementaryMessage ? (\n    <Text\n      as=\"p\"\n      size=\"S\"\n      color=\"TEXT_GREY\"\n      id={`${managedHtmlFor}_supplementaryMessage`}\n      className=\"smarthr-ui-FormControl-supplementaryMessage\"\n    >\n      {supplementaryMessage}\n    </Text>\n  ) : null,\n)\n\nexport const FormControl: FC<Omit<Props, 'as' | 'disabled'>> = ActualFormControl\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAuEA;AAEA;AACE;AACE;;;;;;;;;AASC;;;AAGD;;;AAGA;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAC2B;AAC7B;AACE;AACA;AACD;AACF;AACD;;;;;AAKE;AACE;AACA;AACA;AACE;AACD;AACF;AACD;AACE;AACA;AACA;AACE;AACD;AACF;AACF;AACF;AAED;AAEO;;AAmBL;AACA;;;AAGA;AACA;AACA;AAEA;;;AAII;;;AAGA;;;AAGA;;;AAGA;;AAGF;AACF;AAEA;AAKA;;AAEI;;AAGF;AACF;AAEA;AAEA;;;;AAKI;;;AAGA;AACA;AACA;AACA;AACA;;AAEJ;;AAGE;;;AAIE;;;;;;;;;;;;AAgBA;;;;;;;;;;;;;;;AAmBF;;;;;;;;AAYE;;AAEJ;;;;;;;;AAYI;AACE;;;AAEA;;;;;;;AAQJ;;;;;;AASA;;AAEA;AACE;AAEE;;;AAIF;AAEE;AACA;;;AAIJ;;AAGF;;;;AAsBA;AACE;;AAOF;AAuBF;AAEA;;AAkCI;;;AAGM;AACA;AACE;AACI;AACD;AACH;AACI;AACA;AACA;AACD;;;;;AAML;AACA;;;;AAKF;AACE;AACA;AACA;AACD;AACD;;;AAIJ;;;AAOU;;;AASF;AAUV;AAGF;AAQA;AASA;AAeA;AAyBA;AAgBO;;;"}