{"version":3,"file":"ModelessDialog.cjs","sources":["../../../../src/components/Dialog/ModelessDialog/ModelessDialog.tsx"],"sourcesContent":["'use client'\n\nimport {\n  type ComponentProps,\n  type FC,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PropsWithChildren,\n  type ReactNode,\n  type RefObject,\n  memo,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport Draggable from 'react-draggable'\nimport { type VariantProps, tv } from 'tailwind-variants'\n\nimport { useHandleEscape } from '../../../hooks/useHandleEscape'\nimport { useIntl } from '../../../intl'\nimport { debounce } from '../../../libs/debounce'\nimport { dialogSize } from '../../../tailwind'\nimport { Base, type BaseElementProps } from '../../Base'\nimport { Button } from '../../Button'\nimport { Heading } from '../../Heading'\nimport { FaGripIcon, FaXmarkIcon } from '../../Icon'\nimport { DialogBody, type Props as DialogBodyProps } from '../DialogBody'\nimport { DialogOverlap } from '../DialogOverlap'\nimport { useDialogPortal } from '../useDialogPortal'\n\nimport type { DialogSize } from '../types'\n\ntype AbstractProps = PropsWithChildren<{\n  /**\n   * ダイアログのタイトルの内容\n   */\n  heading: ReactNode\n  /**\n   * ダイアログのフッタ部分の内容\n   */\n  footer?: ReactNode\n  /**\n   * ダイアログが開かれているかどうかの真偽値\n   */\n  isOpen: boolean\n  /**\n   * 閉じるボタンを押下したときのハンドラ\n   */\n  onClickClose?: (e: MouseEvent<HTMLButtonElement>) => void\n  /**\n   * ダイアログが開いている状態で Escape キーを押下したときのハンドラ\n   */\n  onPressEscape?: () => void\n  /**\n   * @deprecated ダイアログの幅を指定する場合は、`width` ではなく `size` を使用してください。\n   * ダイアログの幅\n   */\n  width?: string | number\n  /**\n   * ダイアログの大きさ\n   */\n  size?: DialogSize\n  /**\n   * ダイアログの高さ\n   */\n  height?: string | number\n  /**\n   * ダイアログを開いたときの初期 top 位置\n   */\n  top?: string | number\n  /**\n   * ダイアログを開いたときの初期 left 位置\n   */\n  left?: string | number\n  /**\n   * ダイアログを開いたときの初期 right 位置\n   */\n  right?: string | number\n  /**\n   * ダイアログを開いたときの初期 bottom 位置\n   */\n  bottom?: string | number\n  /**\n   * ポータルの container となる DOM 要素を追加する親要素\n   */\n  portalParent?: HTMLElement | RefObject<HTMLElement>\n}>\ntype Props = AbstractProps &\n  Omit<DialogBodyProps, keyof AbstractProps> &\n  Omit<BaseElementProps, keyof AbstractProps> &\n  Omit<VariantProps<typeof classNameGenerator>, keyof AbstractProps>\n\nconst classNameGenerator = tv({\n  slots: {\n    overlap: 'shr-inset-[unset]',\n    wrapper: 'smarthr-ui-ModelessDialog shr-fixed shr-flex shr-flex-col',\n    headerEl: [\n      'smarthr-ui-ModelessDialog-header shr-border-b-shorthand shr-relative shr-flex shr-cursor-move shr-items-center shr-rounded-tl-l shr-rounded-tr-l shr-pe-1 shr-ps-1.5',\n      'hover:shr-bg-white-darken',\n      /* DialogHandlerにフォーカスが当たっているときは、headerもフォーカス状態のスタイルにする。 */\n      'has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-focus-indicator has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-bg-white-darken has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-transition-colors has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-duration-100 has-[.smarthr-ui-ModelessDialog-handle:focus-visible]:shr-ease-in-out',\n    ],\n    dialogHandler: [\n      'smarthr-ui-ModelessDialog-handle shr-absolute shr-inset-x-0 shr-bottom-0 shr-top-[2px] shr-m-auto shr-flex shr-justify-center shr-rounded-tl-s shr-rounded-tr-s shr-border-none shr-text-grey shr-transition-colors shr-duration-100 shr-ease-in-out',\n      'focus-visible:shr-focus-indicator--none shr-cursor-[inherit] shr-bg-[unset]',\n    ],\n    headingEl: ['shr-my-1 shr-me-1'],\n    closeButtonLayout: [\n      'shr-relative' /* DialogHandlerの上に出すためにスタッキングコンテキストを生成 */,\n      'shr-ml-auto shr-shrink-0',\n    ],\n    footerEl: 'smarthr-ui-ModelessDialog-footer shr-border-t-shorthand',\n  },\n  variants: {\n    size: {\n      XS: { wrapper: dialogSize.XS },\n      S: { wrapper: dialogSize.S },\n      M: { wrapper: dialogSize.M },\n      L: { wrapper: dialogSize.L },\n      XL: { wrapper: dialogSize.XL },\n      XXL: { wrapper: dialogSize.XXL },\n      FULL: { wrapper: dialogSize.FULL },\n    },\n    resizable: {\n      true: {\n        wrapper: 'shr-resize shr-overflow-auto',\n      },\n      false: {},\n    },\n  },\n})\n\nexport const ModelessDialog: FC<Props> = ({\n  heading,\n  children,\n  contentBgColor,\n  contentPadding,\n  footer,\n  isOpen,\n  onPressEscape,\n  resizable = false,\n  width,\n  size,\n  height,\n  top,\n  left,\n  right,\n  bottom,\n  portalParent,\n  className,\n  id,\n  onClickClose,\n  ...rest\n}) => {\n  const labelId = useId()\n  const lastFocusElementRef = useRef<HTMLElement | null>(null)\n  const { createPortal } = useDialogPortal(portalParent, id)\n  const { localize } = useIntl()\n\n  const classNames = useMemo(() => {\n    const { overlap, wrapper, headerEl, headingEl, dialogHandler, closeButtonLayout, footerEl } =\n      classNameGenerator()\n\n    return {\n      overlap: overlap({ className }),\n      wrapper: wrapper({ size, resizable }),\n      header: headerEl(),\n      heading: headingEl(),\n      dialogHandler: dialogHandler(),\n      closeButtonLayout: closeButtonLayout(),\n      footer: footerEl(),\n    }\n  }, [className, size, resizable])\n\n  const wrapperRef = useRef<HTMLDivElement>(null)\n  const focusTargetRef = useRef<HTMLDivElement>(null)\n\n  const [wrapperPosition, setWrapperPosition] = useState<DOMRect | undefined>(undefined)\n  const [debouncedLiveRegionText, setDebouncedLiveRegionText] = useState<string>('')\n  const [centering, setCentering] = useState<{\n    top?: number\n    left?: number\n  }>({})\n  const [position, setPosition] = useState<{ x: number; y: number }>({\n    x: 0,\n    y: 0,\n  })\n  const [draggableBounds, setDraggableBounds] =\n    useState<ComponentProps<typeof Draggable>['bounds']>()\n  const debounceLiveRegionText = useMemo(() => debounce(setDebouncedLiveRegionText, 600), [])\n\n  useEffect(() => {\n    if (!wrapperPosition) {\n      setDebouncedLiveRegionText('')\n      return\n    }\n\n    const txt = localize(\n      {\n        id: 'smarthr-ui/ModelessDialog/dialogHandlerLiveRegionText',\n        defaultText: '上から{top}px、左から{left}px',\n      },\n      {\n        top: Math.trunc(wrapperPosition.top).toString(),\n        left: Math.trunc(wrapperPosition.left).toString(),\n      },\n    )\n\n    debounceLiveRegionText(txt)\n  }, [localize, wrapperPosition, debounceLiveRegionText])\n\n  const positionStyle = useMemo(\n    () => ({\n      top: centering.top ?? top,\n      left: centering.left ?? left,\n      right,\n      bottom,\n      width: size ? undefined : width,\n      height,\n    }),\n    [centering, top, left, right, bottom, width, height, size],\n  )\n\n  const handleArrowKey = useCallback(\n    (e: KeyboardEvent) => {\n      if (!isOpen || document.activeElement !== e.currentTarget) {\n        return\n      }\n\n      const movingDistance = 20\n\n      switch (e.key) {\n        case 'ArrowUp':\n          setPosition((prev) => ({\n            x: prev.x,\n            y: prev.y - movingDistance,\n          }))\n          e.preventDefault()\n          break\n        case 'ArrowDown':\n          setPosition((prev) => ({\n            x: prev.x,\n            y: prev.y + movingDistance,\n          }))\n          e.preventDefault()\n          break\n        case 'ArrowLeft':\n          setPosition((prev) => ({\n            x: prev.x - movingDistance,\n            y: prev.y,\n          }))\n          e.preventDefault()\n          break\n        case 'ArrowRight':\n          setPosition((prev) => ({\n            x: prev.x + movingDistance,\n            y: prev.y,\n          }))\n          e.preventDefault()\n          break\n      }\n    },\n    [isOpen],\n  )\n\n  useEffect(() => {\n    if (wrapperRef.current instanceof Element) {\n      setWrapperPosition(wrapperRef.current.getBoundingClientRect())\n    }\n  }, [position])\n\n  useEffect(() => {\n    // 中央寄せの座標計算を行う\n    if (!wrapperRef.current || !isOpen) {\n      return\n    }\n\n    const isXCenter = left === undefined && right === undefined\n    const isYCenter = top === undefined && bottom === undefined\n\n    if (isXCenter || isYCenter) {\n      const rect = wrapperRef.current.getBoundingClientRect()\n\n      setCentering({\n        top: isYCenter ? window.innerHeight / 2 - rect.height / 2 : undefined,\n        left: isXCenter ? window.innerWidth / 2 - rect.width / 2 : undefined,\n      })\n    }\n  }, [bottom, isOpen, left, right, top])\n\n  useEffect(() => {\n    if (!isOpen) return\n\n    if (centering.top) {\n      setDraggableBounds({ top: centering.top * -1 })\n\n      return\n    }\n\n    if (wrapperRef.current) {\n      const rect = wrapperRef.current.getBoundingClientRect()\n\n      setDraggableBounds({ top: rect.top * -1 })\n    }\n  }, [isOpen, centering.top])\n\n  useEffect(() => {\n    if (isOpen) {\n      setPosition({ x: 0, y: 0 })\n      focusTargetRef.current?.focus()\n    }\n  }, [isOpen])\n\n  const actualOnClickClose = useCallback(\n    (e: MouseEvent<HTMLButtonElement>) => {\n      lastFocusElementRef.current?.focus()\n      onClickClose?.(e)\n    },\n    [onClickClose],\n  )\n\n  const actualOnPressEscape = useMemo(\n    () =>\n      onPressEscape\n        ? () => {\n            lastFocusElementRef.current?.focus()\n            onPressEscape()\n          }\n        : undefined,\n    [onPressEscape],\n  )\n\n  useHandleEscape(\n    useMemo(\n      () => (actualOnPressEscape && isOpen ? actualOnPressEscape : undefined),\n      [isOpen, actualOnPressEscape],\n    ),\n  )\n\n  useEffect(() => {\n    const focusHandler = (e: FocusEvent) => {\n      // e.target(現在フォーカスがあたっている要素)がModeless dialog外の要素であれば、lastFocusElementRefに代入する\n      if (e.target instanceof HTMLElement && !wrapperRef?.current?.contains(e.target)) {\n        lastFocusElementRef.current = e.target\n      }\n    }\n\n    document.addEventListener('focus', focusHandler, true)\n\n    return () => document.removeEventListener('focus', focusHandler, true)\n  }, [])\n\n  const onDragStart = useCallback((_: any, data: { x: number; y: number }) => setPosition(data), [])\n  const onDrag = useCallback((_: any, data: { deltaX: number; deltaY: number }) => {\n    setPosition((prev) => ({\n      x: prev.x + data.deltaX,\n      y: prev.y + data.deltaY,\n    }))\n  }, [])\n\n  return createPortal(\n    <DialogOverlap isOpen={isOpen} className={classNames.overlap} as=\"section\">\n      <Draggable\n        handle=\".smarthr-ui-ModelessDialog-handle\"\n        onStart={onDragStart}\n        onDrag={onDrag}\n        position={position}\n        bounds={draggableBounds}\n        nodeRef={wrapperRef}\n      >\n        <Base\n          {...rest}\n          ref={wrapperRef}\n          role=\"dialog\"\n          aria-labelledby={labelId}\n          radius=\"m\"\n          layer={3}\n          overflow=\"auto\"\n          className={classNames.wrapper}\n          style={positionStyle}\n        >\n          {/* dummy element for focus management. */}\n          <div tabIndex={-1} ref={focusTargetRef} />\n          <div className={classNames.header}>\n            <Handler onArrowKeyDown={handleArrowKey} className={classNames.dialogHandler} />\n            <div id={labelId} className={classNames.heading}>\n              {/* eslint-disable-next-line smarthr/a11y-heading-in-sectioning-content */}\n              <Heading>{heading}</Heading>\n            </div>\n            <CloseButton onClick={actualOnClickClose} className={classNames.closeButtonLayout} />\n          </div>\n          <DialogBody\n            contentBgColor={contentBgColor}\n            contentPadding={contentPadding}\n            className=\"smarthr-ui-ModelessDialog-content shr-overscroll-contain\"\n          >\n            {children}\n          </DialogBody>\n          {footer && <div className={classNames.footer}>{footer}</div>}\n          <LiveRegion regionText={debouncedLiveRegionText} />\n        </Base>\n      </Draggable>\n    </DialogOverlap>,\n  )\n}\n\nconst Handler = memo<{\n  className: string\n  onArrowKeyDown: (e: KeyboardEvent) => void\n}>(({ onArrowKeyDown: onDelegateKeyDown, ...rest }) => {\n  const { localize } = useIntl()\n  const accessibleDefaultTexts = useMemo(\n    () => ({\n      dialogHandlerAriaRoleDescription: localize({\n        id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaRoleDescription',\n        defaultText: 'ドラッグ可能',\n      }),\n      dialogHandlerDescription: localize({\n        id: 'smarthr-ui/ModelessDialog/dialogHandlerDescription',\n        defaultText: '矢印キーを押して上下左右に移動できます',\n      }),\n      dialogHandlerAriaLabel: localize({\n        id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaLabel',\n        defaultText: 'ダイアログの位置',\n      }),\n    }),\n    [localize],\n  )\n\n  return (\n    <>\n      <button\n        {...rest}\n        type=\"button\"\n        aria-label={accessibleDefaultTexts.dialogHandlerAriaLabel}\n        aria-roledescription={accessibleDefaultTexts.dialogHandlerAriaRoleDescription}\n        aria-describedby=\"handler-description\"\n        onKeyDown={onDelegateKeyDown}\n      >\n        <FaGripIcon />\n      </button>\n      <div className=\"shr-hidden\" id=\"handler-description\">\n        {accessibleDefaultTexts.dialogHandlerDescription}\n      </div>\n    </>\n  )\n})\n\nconst LiveRegion = ({ regionText }: { regionText: string | undefined }) => (\n  <div\n    role=\"status\"\n    className=\"shr-fixed -shr-m-px shr-h-px shr-w-px shr-overflow-hidden shr-whitespace-nowrap\"\n  >\n    {regionText}\n  </div>\n)\n\nconst CloseButton = memo<{\n  className: string\n  onClick: (e: MouseEvent<HTMLButtonElement>) => void\n}>(({ onClick, className }) => {\n  const { localize } = useIntl()\n  const closeButtonIconAlt = localize({\n    id: 'smarthr-ui/ModelessDialog/closeButtonIconAlt',\n    defaultText: '閉じる',\n  })\n\n  return (\n    <div className={className}>\n      <Button\n        type=\"button\"\n        size=\"S\"\n        onClick={onClick}\n        className=\"smarthr-ui-ModelessDialog-closeButton\"\n      >\n        <FaXmarkIcon alt={closeButtonIconAlt} />\n      </Button>\n    </div>\n  )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+FA;AACE;AACE;AACA;AACA;;;;;AAKC;AACD;;;AAGC;;AAED;AACE;;AAED;AACD;AACD;AACD;AACE;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACD;AACD;AACE;AACE;AACD;AACD;AACD;AACF;AACF;;AAwBC;AACA;;AAEA;AAEA;AACE;;AAIE;;;;;;;;;AAUJ;AACA;;;;AAQA;AACE;AACA;AACD;;AAGD;;;;;;;AAUM;AACA;;;;AAKD;;;AAML;AAEI;AACA;;;;;AAKD;AAIH;;;;;AAQI;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;AAIP;;AAKA;;;AAGF;;;;;;;;AAWE;;AAGE;AACE;AACA;AACD;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;AAEI;AACA;AACF;AAIF;;AAIU;AACA;;AAEJ;;;AAYJ;;AAEE;AACE;;AAEJ;;AAIA;;AAGF;;AAEE;AACE;AACA;AACD;;;AA+CL;AAEA;AAIE;AACA;;AAGM;AACA;;;AAGA;AACA;;;AAGA;AACA;;AAEH;AAIH;AAiBF;AAEA;AASA;AAIE;;AAEE;AACA;AACD;AAED;AAYF;;"}