smarthr-ui 96.1.3 → 97.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/lib/components/Combobox/helper.cjs +29 -13
  2. package/lib/components/Combobox/helper.cjs.map +1 -1
  3. package/lib/components/Combobox/helper.d.ts +1 -1
  4. package/lib/components/Combobox/helper.js +29 -13
  5. package/lib/components/Combobox/helper.js.map +1 -1
  6. package/lib/components/Combobox/useOptions.cjs +21 -17
  7. package/lib/components/Combobox/useOptions.cjs.map +1 -1
  8. package/lib/components/Combobox/useOptions.js +21 -17
  9. package/lib/components/Combobox/useOptions.js.map +1 -1
  10. package/lib/components/Dialog/DialogContentInner.cjs +8 -1
  11. package/lib/components/Dialog/DialogContentInner.cjs.map +1 -1
  12. package/lib/components/Dialog/DialogContentInner.js +9 -2
  13. package/lib/components/Dialog/DialogContentInner.js.map +1 -1
  14. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs +13 -11
  15. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs.map +1 -1
  16. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js +13 -11
  17. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js.map +1 -1
  18. package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs +5 -2
  19. package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs.map +1 -1
  20. package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js +5 -2
  21. package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js.map +1 -1
  22. package/lib/components/Disclosure/DisclosureTrigger.cjs +39 -18
  23. package/lib/components/Disclosure/DisclosureTrigger.cjs.map +1 -1
  24. package/lib/components/Disclosure/DisclosureTrigger.js +40 -19
  25. package/lib/components/Disclosure/DisclosureTrigger.js.map +1 -1
  26. package/lib/components/Disclosure/useDisclosure.cjs +8 -14
  27. package/lib/components/Disclosure/useDisclosure.cjs.map +1 -1
  28. package/lib/components/Disclosure/useDisclosure.js +8 -14
  29. package/lib/components/Disclosure/useDisclosure.js.map +1 -1
  30. package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.cjs +4 -2
  31. package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.cjs.map +1 -1
  32. package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.js +4 -2
  33. package/lib/components/Dropdown/DropdownMenuButton/DropdownMenuButton.js.map +1 -1
  34. package/lib/components/Dropdown/DropdownTrigger.cjs +3 -0
  35. package/lib/components/Dropdown/DropdownTrigger.cjs.map +1 -1
  36. package/lib/components/Dropdown/DropdownTrigger.js +3 -0
  37. package/lib/components/Dropdown/DropdownTrigger.js.map +1 -1
  38. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +12 -17
  39. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
  40. package/lib/components/InputFile/InputFileMultiplyAppendable.js +12 -17
  41. package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
  42. package/lib/components/LineClamp/LineClamp.cjs +14 -3
  43. package/lib/components/LineClamp/LineClamp.cjs.map +1 -1
  44. package/lib/components/LineClamp/LineClamp.js +14 -3
  45. package/lib/components/LineClamp/LineClamp.js.map +1 -1
  46. package/lib/components/Select/Select.cjs +11 -11
  47. package/lib/components/Select/Select.cjs.map +1 -1
  48. package/lib/components/Select/Select.js +12 -12
  49. package/lib/components/Select/Select.js.map +1 -1
  50. package/lib/components/TabBar/TabItem.cjs +11 -9
  51. package/lib/components/TabBar/TabItem.cjs.map +1 -1
  52. package/lib/components/TabBar/TabItem.d.ts +2 -2
  53. package/lib/components/TabBar/TabItem.js +13 -11
  54. package/lib/components/TabBar/TabItem.js.map +1 -1
  55. package/lib/hooks/useHandleEscape.cjs +4 -4
  56. package/lib/hooks/useHandleEscape.cjs.map +1 -1
  57. package/lib/hooks/useHandleEscape.d.ts +1 -1
  58. package/lib/hooks/useHandleEscape.js +4 -4
  59. package/lib/hooks/useHandleEscape.js.map +1 -1
  60. package/lib/intl/Localizer.d.ts +3 -3
  61. package/lib/intl/locales/en_us.json.cjs +133 -0
  62. package/lib/intl/locales/en_us.json.cjs.map +1 -0
  63. package/lib/intl/locales/en_us.json.js +131 -0
  64. package/lib/intl/locales/en_us.json.js.map +1 -0
  65. package/lib/intl/locales/id_id.json.cjs +133 -0
  66. package/lib/intl/locales/id_id.json.cjs.map +1 -0
  67. package/lib/intl/locales/id_id.json.js +131 -0
  68. package/lib/intl/locales/id_id.json.js.map +1 -0
  69. package/lib/intl/locales/index.cjs +24 -18
  70. package/lib/intl/locales/index.cjs.map +1 -1
  71. package/lib/intl/locales/index.d.ts +1135 -1136
  72. package/lib/intl/locales/index.js +25 -19
  73. package/lib/intl/locales/index.js.map +1 -1
  74. package/lib/intl/locales/ja.json.cjs +133 -0
  75. package/lib/intl/locales/ja.json.cjs.map +1 -0
  76. package/lib/intl/locales/ja.json.js +131 -0
  77. package/lib/intl/locales/ja.json.js.map +1 -0
  78. package/lib/intl/locales/ja_easy.json.cjs +133 -0
  79. package/lib/intl/locales/ja_easy.json.cjs.map +1 -0
  80. package/lib/intl/locales/ja_easy.json.js +131 -0
  81. package/lib/intl/locales/ja_easy.json.js.map +1 -0
  82. package/lib/intl/locales/ko_kr.json.cjs +133 -0
  83. package/lib/intl/locales/ko_kr.json.cjs.map +1 -0
  84. package/lib/intl/locales/ko_kr.json.js +131 -0
  85. package/lib/intl/locales/ko_kr.json.js.map +1 -0
  86. package/lib/intl/locales/pt_br.json.cjs +133 -0
  87. package/lib/intl/locales/pt_br.json.cjs.map +1 -0
  88. package/lib/intl/locales/pt_br.json.js +131 -0
  89. package/lib/intl/locales/pt_br.json.js.map +1 -0
  90. package/lib/intl/locales/vi_vn.json.cjs +133 -0
  91. package/lib/intl/locales/vi_vn.json.cjs.map +1 -0
  92. package/lib/intl/locales/vi_vn.json.js +131 -0
  93. package/lib/intl/locales/vi_vn.json.js.map +1 -0
  94. package/lib/intl/locales/zh_hans_cn.json.cjs +133 -0
  95. package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -0
  96. package/lib/intl/locales/zh_hans_cn.json.js +131 -0
  97. package/lib/intl/locales/zh_hans_cn.json.js.map +1 -0
  98. package/lib/intl/locales/zh_hant_tw.json.cjs +133 -0
  99. package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -0
  100. package/lib/intl/locales/zh_hant_tw.json.js +131 -0
  101. package/lib/intl/locales/zh_hant_tw.json.js.map +1 -0
  102. package/lib/intl/useIntl.cjs.map +1 -1
  103. package/lib/intl/useIntl.d.ts +3 -3
  104. package/lib/intl/useIntl.js.map +1 -1
  105. package/metadata.json +1 -1
  106. package/package.json +2 -1
  107. package/lib/intl/locales/en_us.cjs +0 -135
  108. package/lib/intl/locales/en_us.cjs.map +0 -1
  109. package/lib/intl/locales/en_us.d.ts +0 -128
  110. package/lib/intl/locales/en_us.js +0 -133
  111. package/lib/intl/locales/en_us.js.map +0 -1
  112. package/lib/intl/locales/id_id.cjs +0 -135
  113. package/lib/intl/locales/id_id.cjs.map +0 -1
  114. package/lib/intl/locales/id_id.d.ts +0 -128
  115. package/lib/intl/locales/id_id.js +0 -133
  116. package/lib/intl/locales/id_id.js.map +0 -1
  117. package/lib/intl/locales/ja_easy.cjs +0 -135
  118. package/lib/intl/locales/ja_easy.cjs.map +0 -1
  119. package/lib/intl/locales/ja_easy.d.ts +0 -128
  120. package/lib/intl/locales/ja_easy.js +0 -133
  121. package/lib/intl/locales/ja_easy.js.map +0 -1
  122. package/lib/intl/locales/ko_kr.cjs +0 -135
  123. package/lib/intl/locales/ko_kr.cjs.map +0 -1
  124. package/lib/intl/locales/ko_kr.d.ts +0 -128
  125. package/lib/intl/locales/ko_kr.js +0 -133
  126. package/lib/intl/locales/ko_kr.js.map +0 -1
  127. package/lib/intl/locales/pt_br.cjs +0 -135
  128. package/lib/intl/locales/pt_br.cjs.map +0 -1
  129. package/lib/intl/locales/pt_br.d.ts +0 -128
  130. package/lib/intl/locales/pt_br.js +0 -133
  131. package/lib/intl/locales/pt_br.js.map +0 -1
  132. package/lib/intl/locales/vi_vn.cjs +0 -135
  133. package/lib/intl/locales/vi_vn.cjs.map +0 -1
  134. package/lib/intl/locales/vi_vn.d.ts +0 -128
  135. package/lib/intl/locales/vi_vn.js +0 -133
  136. package/lib/intl/locales/vi_vn.js.map +0 -1
  137. package/lib/intl/locales/zh_hans_cn.cjs +0 -135
  138. package/lib/intl/locales/zh_hans_cn.cjs.map +0 -1
  139. package/lib/intl/locales/zh_hans_cn.d.ts +0 -128
  140. package/lib/intl/locales/zh_hans_cn.js +0 -133
  141. package/lib/intl/locales/zh_hans_cn.js.map +0 -1
  142. package/lib/intl/locales/zh_hant_tw.cjs +0 -135
  143. package/lib/intl/locales/zh_hant_tw.cjs.map +0 -1
  144. package/lib/intl/locales/zh_hant_tw.d.ts +0 -128
  145. package/lib/intl/locales/zh_hant_tw.js +0 -133
  146. package/lib/intl/locales/zh_hant_tw.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ModelessDialog.js","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 {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- 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;;"}
1
+ {"version":3,"file":"ModelessDialog.js","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 // 外部propsをrefに保存\n const unstableRef = useRef({ isOpen, onClickClose, onPressEscape })\n unstableRef.current = { isOpen, onClickClose, onPressEscape }\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((e: KeyboardEvent) => {\n if (!unstableRef.current.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\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((e: MouseEvent<HTMLButtonElement>) => {\n lastFocusElementRef.current?.focus()\n unstableRef.current.onClickClose?.(e)\n }, [])\n\n // stableなcallbackを作成\n const memoizedOnPressEscape = useCallback(() => {\n lastFocusElementRef.current?.focus()\n unstableRef.current.onPressEscape?.()\n }, [])\n\n useHandleEscape(isOpen ? memoizedOnPressEscape : undefined)\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 {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- 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;;;;AAOL;;AAGA;AAEI;AACA;;;;;AAKD;AAIH;AACE;;;;AAMA;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;;;AAOL;;;AAGF;;;;;;;;AAWE;;AAGE;AACE;AACA;AACD;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;AACE;;;;AAKF;AACE;AACA;;;;AAMA;;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;;"}
@@ -59,8 +59,11 @@ const RemoteDialogTrigger = ({ targetId, children, onClick }) => {
59
59
  setupElement();
60
60
  });
61
61
  observer.observe(currentRef, {
62
- childList: true, // 直接の子要素の追加・削除を監視
63
- subtree: true, // 子孫要素の変更も監視
62
+ childList: true,
63
+ subtree: true,
64
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
65
+ attributes: true,
66
+ attributeFilter: ['disabled', 'aria-disabled'],
64
67
  });
65
68
  return () => {
66
69
  observer.disconnect();
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteDialogTrigger.cjs","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true, // 直接の子要素の追加・削除を監視\n subtree: true, // 子孫要素の変更も監視\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;AAEE;AACA;;;;AAIJ;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;;;AAGC;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
1
+ {"version":3,"file":"RemoteDialogTrigger.cjs","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;AAEE;AACA;;;;AAIJ;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
@@ -57,8 +57,11 @@ const RemoteDialogTrigger = ({ targetId, children, onClick }) => {
57
57
  setupElement();
58
58
  });
59
59
  observer.observe(currentRef, {
60
- childList: true, // 直接の子要素の追加・削除を監視
61
- subtree: true, // 子孫要素の変更も監視
60
+ childList: true,
61
+ subtree: true,
62
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
63
+ attributes: true,
64
+ attributeFilter: ['disabled', 'aria-disabled'],
62
65
  });
63
66
  return () => {
64
67
  observer.disconnect();
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteDialogTrigger.js","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true, // 直接の子要素の追加・削除を監視\n subtree: true, // 子孫要素の変更も監視\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;AAEE;AACA;;;;AAIJ;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;;;AAGC;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
1
+ {"version":3,"file":"RemoteDialogTrigger.js","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;AAEE;AACA;;;;AAIJ;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
@@ -8,32 +8,53 @@ var components_Disclosure_useDisclosure = require('./useDisclosure.cjs');
8
8
  const DisclosureTrigger = ({ targetId, children, onClick }) => {
9
9
  const [expanded, setExpanded] = components_Disclosure_useDisclosure.useDisclosure(targetId);
10
10
  const ref = React.useRef(null);
11
- const actualOnClick = React.useMemo(() => {
11
+ const unstableRef = React.useRef({ onClick, setExpanded });
12
+ unstableRef.current = { onClick, setExpanded };
13
+ const actualOnClick = React.useCallback((e) => {
12
14
  const toggleExpanded = () => {
13
- setExpanded((current) => !current);
15
+ unstableRef.current.setExpanded((current) => !current);
14
16
  };
15
- if (onClick) {
16
- return (e) => {
17
- onClick(toggleExpanded, e);
18
- };
17
+ if (unstableRef.current.onClick) {
18
+ unstableRef.current.onClick(toggleExpanded, e);
19
+ }
20
+ else {
21
+ toggleExpanded();
19
22
  }
20
- return toggleExpanded;
21
- }, [onClick, setExpanded]);
23
+ }, []);
22
24
  React.useEffect(() => {
23
- if (!ref.current) {
25
+ const wrapper = ref.current;
26
+ if (!wrapper) {
24
27
  return;
25
28
  }
26
- const button = ref.current.querySelector('button');
27
- if (!button) {
28
- throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください');
29
- }
30
- button.setAttribute('aria-expanded', expanded.toString());
31
- button.setAttribute('aria-controls', targetId);
32
- button.addEventListener('click', actualOnClick);
29
+ let currentCleanup;
30
+ const setupButton = () => {
31
+ currentCleanup?.();
32
+ currentCleanup = undefined;
33
+ const button = wrapper.querySelector('button');
34
+ if (!button) {
35
+ throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください');
36
+ }
37
+ button.setAttribute('aria-expanded', expanded.toString());
38
+ button.setAttribute('aria-controls', targetId);
39
+ button.addEventListener('click', actualOnClick);
40
+ currentCleanup = () => {
41
+ button.removeEventListener('click', actualOnClick);
42
+ };
43
+ };
44
+ setupButton();
45
+ const observer = new MutationObserver(setupButton);
46
+ observer.observe(wrapper, {
47
+ childList: true,
48
+ subtree: true,
49
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
50
+ attributes: true,
51
+ attributeFilter: ['disabled', 'aria-disabled'],
52
+ });
33
53
  return () => {
34
- button.removeEventListener('click', actualOnClick);
54
+ currentCleanup?.();
55
+ observer.disconnect();
35
56
  };
36
- }, [expanded, children, actualOnClick, targetId]);
57
+ }, [expanded, targetId, actualOnClick]);
37
58
  // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。
38
59
  // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる
39
60
  // TODO: 将来的にspan -> Fragmentに変更する
@@ -1 +1 @@
1
- {"version":3,"file":"DisclosureTrigger.cjs","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useEffect, useMemo, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const actualOnClick = useMemo(() => {\n const toggleExpanded = () => {\n setExpanded((current) => !current)\n }\n\n if (onClick) {\n return (e: MouseEvent) => {\n onClick(toggleExpanded, e)\n }\n }\n\n return toggleExpanded\n }, [onClick, setExpanded])\n\n useEffect(() => {\n if (!ref.current) {\n return\n }\n\n const button = ref.current.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n return () => {\n button.removeEventListener('click', actualOnClick)\n }\n }, [expanded, children, actualOnClick, targetId])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAoBO;;AAEL;AAEA;;;AAGE;;;AAII;AACF;;AAGF;AACF;;AAGE;;;;;AAOE;;;AAIF;AACA;AAEA;AACE;AACF;;;;;AAMF;AAKF;;"}
1
+ {"version":3,"file":"DisclosureTrigger.cjs","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n currentCleanup = () => {\n button.removeEventListener('click', actualOnClick)\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(wrapper, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n currentCleanup?.()\n observer.disconnect()\n }\n }, [expanded, targetId, actualOnClick])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAoBO;;AAEL;;;AAKA;;AAEI;AACF;AAEA;;;;AAGE;;;;AAKF;;;;AAKA;;;;;;AASI;;;AAIF;AACA;;AAGE;AACF;AACF;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;;AAGA;;;;;AAMF;AAKF;;"}
@@ -1,37 +1,58 @@
1
1
  "use client";
2
2
  import { jsx } from 'react/jsx-runtime';
3
- import { useRef, useMemo, useEffect } from 'react';
3
+ import { useRef, useCallback, useEffect } from 'react';
4
4
  import { useDisclosure } from './useDisclosure.js';
5
5
 
6
6
  const DisclosureTrigger = ({ targetId, children, onClick }) => {
7
7
  const [expanded, setExpanded] = useDisclosure(targetId);
8
8
  const ref = useRef(null);
9
- const actualOnClick = useMemo(() => {
9
+ const unstableRef = useRef({ onClick, setExpanded });
10
+ unstableRef.current = { onClick, setExpanded };
11
+ const actualOnClick = useCallback((e) => {
10
12
  const toggleExpanded = () => {
11
- setExpanded((current) => !current);
13
+ unstableRef.current.setExpanded((current) => !current);
12
14
  };
13
- if (onClick) {
14
- return (e) => {
15
- onClick(toggleExpanded, e);
16
- };
15
+ if (unstableRef.current.onClick) {
16
+ unstableRef.current.onClick(toggleExpanded, e);
17
+ }
18
+ else {
19
+ toggleExpanded();
17
20
  }
18
- return toggleExpanded;
19
- }, [onClick, setExpanded]);
21
+ }, []);
20
22
  useEffect(() => {
21
- if (!ref.current) {
23
+ const wrapper = ref.current;
24
+ if (!wrapper) {
22
25
  return;
23
26
  }
24
- const button = ref.current.querySelector('button');
25
- if (!button) {
26
- throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください');
27
- }
28
- button.setAttribute('aria-expanded', expanded.toString());
29
- button.setAttribute('aria-controls', targetId);
30
- button.addEventListener('click', actualOnClick);
27
+ let currentCleanup;
28
+ const setupButton = () => {
29
+ currentCleanup?.();
30
+ currentCleanup = undefined;
31
+ const button = wrapper.querySelector('button');
32
+ if (!button) {
33
+ throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください');
34
+ }
35
+ button.setAttribute('aria-expanded', expanded.toString());
36
+ button.setAttribute('aria-controls', targetId);
37
+ button.addEventListener('click', actualOnClick);
38
+ currentCleanup = () => {
39
+ button.removeEventListener('click', actualOnClick);
40
+ };
41
+ };
42
+ setupButton();
43
+ const observer = new MutationObserver(setupButton);
44
+ observer.observe(wrapper, {
45
+ childList: true,
46
+ subtree: true,
47
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
48
+ attributes: true,
49
+ attributeFilter: ['disabled', 'aria-disabled'],
50
+ });
31
51
  return () => {
32
- button.removeEventListener('click', actualOnClick);
52
+ currentCleanup?.();
53
+ observer.disconnect();
33
54
  };
34
- }, [expanded, children, actualOnClick, targetId]);
55
+ }, [expanded, targetId, actualOnClick]);
35
56
  // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。
36
57
  // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる
37
58
  // TODO: 将来的にspan -> Fragmentに変更する
@@ -1 +1 @@
1
- {"version":3,"file":"DisclosureTrigger.js","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useEffect, useMemo, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const actualOnClick = useMemo(() => {\n const toggleExpanded = () => {\n setExpanded((current) => !current)\n }\n\n if (onClick) {\n return (e: MouseEvent) => {\n onClick(toggleExpanded, e)\n }\n }\n\n return toggleExpanded\n }, [onClick, setExpanded])\n\n useEffect(() => {\n if (!ref.current) {\n return\n }\n\n const button = ref.current.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n return () => {\n button.removeEventListener('click', actualOnClick)\n }\n }, [expanded, children, actualOnClick, targetId])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAoBO;;AAEL;AAEA;;;AAGE;;;AAII;AACF;;AAGF;AACF;;AAGE;;;;;AAOE;;;AAIF;AACA;AAEA;AACE;AACF;;;;;AAMF;AAKF;;"}
1
+ {"version":3,"file":"DisclosureTrigger.js","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n currentCleanup = () => {\n button.removeEventListener('click', actualOnClick)\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(wrapper, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n currentCleanup?.()\n observer.disconnect()\n }\n }, [expanded, targetId, actualOnClick])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAoBO;;AAEL;;;AAKA;;AAEI;AACF;AAEA;;;;AAGE;;;;AAKF;;;;AAKA;;;;;;AASI;;;AAIF;AACA;;AAGE;AACF;AACF;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;;AAGA;;;;;AAMF;AAKF;;"}
@@ -13,29 +13,23 @@ const useDisclosure = (id) => {
13
13
  detail: { id, expanded },
14
14
  }));
15
15
  }, [expanded, id]);
16
- const handleDisclosureChange = React.useCallback((e) => {
17
- if (id === e.detail.id) {
18
- setExpanded(e.detail.expanded);
19
- }
20
- }, [id]);
21
16
  React.useEffect(() => {
17
+ const handleDisclosureChange = (e) => {
18
+ if (id === e.detail.id) {
19
+ setExpanded(e.detail.expanded);
20
+ }
21
+ };
22
22
  document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange);
23
23
  return () => {
24
24
  document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange);
25
25
  };
26
- }, [id, handleDisclosureChange]);
26
+ }, [id]);
27
27
  const safeSetExpanded = React.useCallback((value) => {
28
28
  // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する
29
29
  requestAnimationFrame(() => {
30
- if (typeof value === 'function') {
31
- const newValue = value(expanded);
32
- setExpanded(newValue);
33
- }
34
- else {
35
- setExpanded(value);
36
- }
30
+ setExpanded(value);
37
31
  });
38
- }, [expanded]);
32
+ }, []);
39
33
  return [expanded, safeSetExpanded];
40
34
  };
41
35
 
@@ -1 +1 @@
1
- {"version":3,"file":"useDisclosure.cjs","sources":["../../../src/components/Disclosure/useDisclosure.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\n\nconst DISCLOSURE_CHANGE_EVENT = 'smarthr-ui:disclosure-change'\ntype DisclosureChangeEventDetail = { id: string; expanded: boolean }\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n interface DocumentEventMap {\n [DISCLOSURE_CHANGE_EVENT]: CustomEvent<DisclosureChangeEventDetail>\n }\n}\n\ntype Setter = (value: boolean | ((prev: boolean) => boolean)) => void\n\ntype UseDisclosureResult = [expanded: boolean, setExpanded: Setter]\n\n/**\n * 同じ `id` で呼ぶとイベント経由で状態が同期される custom hook\n */\nexport const useDisclosure = (id: string): UseDisclosureResult => {\n const [expanded, setExpanded] = useState(false)\n\n useEffect(() => {\n document.dispatchEvent(\n new CustomEvent<DisclosureChangeEventDetail>(DISCLOSURE_CHANGE_EVENT, {\n detail: { id, expanded },\n }),\n )\n }, [expanded, id])\n\n const handleDisclosureChange = useCallback(\n (e: CustomEvent<DisclosureChangeEventDetail>) => {\n if (id === e.detail.id) {\n setExpanded(e.detail.expanded)\n }\n },\n [id],\n )\n\n useEffect(() => {\n document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n return () => {\n document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n }\n }, [id, handleDisclosureChange])\n\n const safeSetExpanded: Setter = useCallback(\n (value) => {\n // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する\n requestAnimationFrame(() => {\n if (typeof value === 'function') {\n const newValue = value(expanded)\n setExpanded(newValue)\n } else {\n setExpanded(value)\n }\n })\n },\n [expanded],\n )\n\n return [expanded, safeSetExpanded]\n}\n"],"names":["useState","useEffect","useCallback"],"mappings":";;;;AAEA,MAAM,uBAAuB,GAAG,8BAA8B;AAc9D;;AAEG;AACI,MAAM,aAAa,GAAG,CAAC,EAAU,KAAyB;IAC/D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAGA,cAAQ,CAAC,KAAK,CAAC;IAE/CC,eAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAA8B,uBAAuB,EAAE;AACpE,YAAA,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;AACzB,SAAA,CAAC,CACH;AACH,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAElB,IAAA,MAAM,sBAAsB,GAAGC,iBAAW,CACxC,CAAC,CAA2C,KAAI;QAC9C,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE;AACtB,YAAA,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;QAChC;AACF,IAAA,CAAC,EACD,CAAC,EAAE,CAAC,CACL;IAEDD,eAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,gBAAgB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC1E,QAAA,OAAO,MAAK;AACV,YAAA,QAAQ,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC/E,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,EAAE,EAAE,sBAAsB,CAAC,CAAC;AAEhC,IAAA,MAAM,eAAe,GAAWC,iBAAW,CACzC,CAAC,KAAK,KAAI;;QAER,qBAAqB,CAAC,MAAK;AACzB,YAAA,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE;AAC/B,gBAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;gBAChC,WAAW,CAAC,QAAQ,CAAC;YACvB;iBAAO;gBACL,WAAW,CAAC,KAAK,CAAC;YACpB;AACF,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,EACD,CAAC,QAAQ,CAAC,CACX;AAED,IAAA,OAAO,CAAC,QAAQ,EAAE,eAAe,CAAC;AACpC;;;;"}
1
+ {"version":3,"file":"useDisclosure.cjs","sources":["../../../src/components/Disclosure/useDisclosure.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\n\nconst DISCLOSURE_CHANGE_EVENT = 'smarthr-ui:disclosure-change'\ntype DisclosureChangeEventDetail = { id: string; expanded: boolean }\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n interface DocumentEventMap {\n [DISCLOSURE_CHANGE_EVENT]: CustomEvent<DisclosureChangeEventDetail>\n }\n}\n\ntype Setter = (value: boolean | ((prev: boolean) => boolean)) => void\n\ntype UseDisclosureResult = [expanded: boolean, setExpanded: Setter]\n\n/**\n * 同じ `id` で呼ぶとイベント経由で状態が同期される custom hook\n */\nexport const useDisclosure = (id: string): UseDisclosureResult => {\n const [expanded, setExpanded] = useState(false)\n\n useEffect(() => {\n document.dispatchEvent(\n new CustomEvent<DisclosureChangeEventDetail>(DISCLOSURE_CHANGE_EVENT, {\n detail: { id, expanded },\n }),\n )\n }, [expanded, id])\n\n useEffect(() => {\n const handleDisclosureChange = (e: CustomEvent<DisclosureChangeEventDetail>) => {\n if (id === e.detail.id) {\n setExpanded(e.detail.expanded)\n }\n }\n\n document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n return () => {\n document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n }\n }, [id])\n\n const safeSetExpanded: Setter = useCallback((value) => {\n // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する\n requestAnimationFrame(() => {\n setExpanded(value)\n })\n }, [])\n\n return [expanded, safeSetExpanded]\n}\n"],"names":["useState","useEffect","useCallback"],"mappings":";;;;AAEA,MAAM,uBAAuB,GAAG,8BAA8B;AAc9D;;AAEG;AACI,MAAM,aAAa,GAAG,CAAC,EAAU,KAAyB;IAC/D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAGA,cAAQ,CAAC,KAAK,CAAC;IAE/CC,eAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAA8B,uBAAuB,EAAE;AACpE,YAAA,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;AACzB,SAAA,CAAC,CACH;AACH,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAElBA,eAAS,CAAC,MAAK;AACb,QAAA,MAAM,sBAAsB,GAAG,CAAC,CAA2C,KAAI;YAC7E,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE;AACtB,gBAAA,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;YAChC;AACF,QAAA,CAAC;AAED,QAAA,QAAQ,CAAC,gBAAgB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC1E,QAAA,OAAO,MAAK;AACV,YAAA,QAAQ,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC/E,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAER,IAAA,MAAM,eAAe,GAAWC,iBAAW,CAAC,CAAC,KAAK,KAAI;;QAEpD,qBAAqB,CAAC,MAAK;YACzB,WAAW,CAAC,KAAK,CAAC;AACpB,QAAA,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,OAAO,CAAC,QAAQ,EAAE,eAAe,CAAC;AACpC;;;;"}
@@ -11,29 +11,23 @@ const useDisclosure = (id) => {
11
11
  detail: { id, expanded },
12
12
  }));
13
13
  }, [expanded, id]);
14
- const handleDisclosureChange = useCallback((e) => {
15
- if (id === e.detail.id) {
16
- setExpanded(e.detail.expanded);
17
- }
18
- }, [id]);
19
14
  useEffect(() => {
15
+ const handleDisclosureChange = (e) => {
16
+ if (id === e.detail.id) {
17
+ setExpanded(e.detail.expanded);
18
+ }
19
+ };
20
20
  document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange);
21
21
  return () => {
22
22
  document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange);
23
23
  };
24
- }, [id, handleDisclosureChange]);
24
+ }, [id]);
25
25
  const safeSetExpanded = useCallback((value) => {
26
26
  // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する
27
27
  requestAnimationFrame(() => {
28
- if (typeof value === 'function') {
29
- const newValue = value(expanded);
30
- setExpanded(newValue);
31
- }
32
- else {
33
- setExpanded(value);
34
- }
28
+ setExpanded(value);
35
29
  });
36
- }, [expanded]);
30
+ }, []);
37
31
  return [expanded, safeSetExpanded];
38
32
  };
39
33
 
@@ -1 +1 @@
1
- {"version":3,"file":"useDisclosure.js","sources":["../../../src/components/Disclosure/useDisclosure.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\n\nconst DISCLOSURE_CHANGE_EVENT = 'smarthr-ui:disclosure-change'\ntype DisclosureChangeEventDetail = { id: string; expanded: boolean }\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n interface DocumentEventMap {\n [DISCLOSURE_CHANGE_EVENT]: CustomEvent<DisclosureChangeEventDetail>\n }\n}\n\ntype Setter = (value: boolean | ((prev: boolean) => boolean)) => void\n\ntype UseDisclosureResult = [expanded: boolean, setExpanded: Setter]\n\n/**\n * 同じ `id` で呼ぶとイベント経由で状態が同期される custom hook\n */\nexport const useDisclosure = (id: string): UseDisclosureResult => {\n const [expanded, setExpanded] = useState(false)\n\n useEffect(() => {\n document.dispatchEvent(\n new CustomEvent<DisclosureChangeEventDetail>(DISCLOSURE_CHANGE_EVENT, {\n detail: { id, expanded },\n }),\n )\n }, [expanded, id])\n\n const handleDisclosureChange = useCallback(\n (e: CustomEvent<DisclosureChangeEventDetail>) => {\n if (id === e.detail.id) {\n setExpanded(e.detail.expanded)\n }\n },\n [id],\n )\n\n useEffect(() => {\n document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n return () => {\n document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n }\n }, [id, handleDisclosureChange])\n\n const safeSetExpanded: Setter = useCallback(\n (value) => {\n // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する\n requestAnimationFrame(() => {\n if (typeof value === 'function') {\n const newValue = value(expanded)\n setExpanded(newValue)\n } else {\n setExpanded(value)\n }\n })\n },\n [expanded],\n )\n\n return [expanded, safeSetExpanded]\n}\n"],"names":[],"mappings":";;AAEA,MAAM,uBAAuB,GAAG,8BAA8B;AAc9D;;AAEG;AACI,MAAM,aAAa,GAAG,CAAC,EAAU,KAAyB;IAC/D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;IAE/C,SAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAA8B,uBAAuB,EAAE;AACpE,YAAA,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;AACzB,SAAA,CAAC,CACH;AACH,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAElB,IAAA,MAAM,sBAAsB,GAAG,WAAW,CACxC,CAAC,CAA2C,KAAI;QAC9C,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE;AACtB,YAAA,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;QAChC;AACF,IAAA,CAAC,EACD,CAAC,EAAE,CAAC,CACL;IAED,SAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,gBAAgB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC1E,QAAA,OAAO,MAAK;AACV,YAAA,QAAQ,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC/E,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,EAAE,EAAE,sBAAsB,CAAC,CAAC;AAEhC,IAAA,MAAM,eAAe,GAAW,WAAW,CACzC,CAAC,KAAK,KAAI;;QAER,qBAAqB,CAAC,MAAK;AACzB,YAAA,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE;AAC/B,gBAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;gBAChC,WAAW,CAAC,QAAQ,CAAC;YACvB;iBAAO;gBACL,WAAW,CAAC,KAAK,CAAC;YACpB;AACF,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,EACD,CAAC,QAAQ,CAAC,CACX;AAED,IAAA,OAAO,CAAC,QAAQ,EAAE,eAAe,CAAC;AACpC;;;;"}
1
+ {"version":3,"file":"useDisclosure.js","sources":["../../../src/components/Disclosure/useDisclosure.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from 'react'\n\nconst DISCLOSURE_CHANGE_EVENT = 'smarthr-ui:disclosure-change'\ntype DisclosureChangeEventDetail = { id: string; expanded: boolean }\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/consistent-type-definitions\n interface DocumentEventMap {\n [DISCLOSURE_CHANGE_EVENT]: CustomEvent<DisclosureChangeEventDetail>\n }\n}\n\ntype Setter = (value: boolean | ((prev: boolean) => boolean)) => void\n\ntype UseDisclosureResult = [expanded: boolean, setExpanded: Setter]\n\n/**\n * 同じ `id` で呼ぶとイベント経由で状態が同期される custom hook\n */\nexport const useDisclosure = (id: string): UseDisclosureResult => {\n const [expanded, setExpanded] = useState(false)\n\n useEffect(() => {\n document.dispatchEvent(\n new CustomEvent<DisclosureChangeEventDetail>(DISCLOSURE_CHANGE_EVENT, {\n detail: { id, expanded },\n }),\n )\n }, [expanded, id])\n\n useEffect(() => {\n const handleDisclosureChange = (e: CustomEvent<DisclosureChangeEventDetail>) => {\n if (id === e.detail.id) {\n setExpanded(e.detail.expanded)\n }\n }\n\n document.addEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n return () => {\n document.removeEventListener(DISCLOSURE_CHANGE_EVENT, handleDisclosureChange)\n }\n }, [id])\n\n const safeSetExpanded: Setter = useCallback((value) => {\n // DisclosureTrigger と DisclosureContent のレンダリング順序に影響しないように animation frame を待ってから state を更新する\n requestAnimationFrame(() => {\n setExpanded(value)\n })\n }, [])\n\n return [expanded, safeSetExpanded]\n}\n"],"names":[],"mappings":";;AAEA,MAAM,uBAAuB,GAAG,8BAA8B;AAc9D;;AAEG;AACI,MAAM,aAAa,GAAG,CAAC,EAAU,KAAyB;IAC/D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;IAE/C,SAAS,CAAC,MAAK;AACb,QAAA,QAAQ,CAAC,aAAa,CACpB,IAAI,WAAW,CAA8B,uBAAuB,EAAE;AACpE,YAAA,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE;AACzB,SAAA,CAAC,CACH;AACH,IAAA,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAElB,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,sBAAsB,GAAG,CAAC,CAA2C,KAAI;YAC7E,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE;AACtB,gBAAA,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;YAChC;AACF,QAAA,CAAC;AAED,QAAA,QAAQ,CAAC,gBAAgB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC1E,QAAA,OAAO,MAAK;AACV,YAAA,QAAQ,CAAC,mBAAmB,CAAC,uBAAuB,EAAE,sBAAsB,CAAC;AAC/E,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AAER,IAAA,MAAM,eAAe,GAAW,WAAW,CAAC,CAAC,KAAK,KAAI;;QAEpD,qBAAqB,CAAC,MAAK;YACzB,WAAW,CAAC,KAAK,CAAC;AACpB,QAAA,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC;AAEN,IAAA,OAAO,CAAC,QAAQ,EAAE,eAAe,CAAC;AACpC;;;;"}
@@ -3,7 +3,6 @@
3
3
 
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
5
  var React = require('react');
6
- var index$1 = require('../../../_virtual/index3.cjs');
7
6
  var index = require('./../../../vendor/.pnpm/tailwind-variants@0.3.1_tailwindcss@3.4.19_ts-node@10.9.2_@swc_core@1.15.17_@types_node@20.19.35_typescript@5.9.3__/vendor/tailwind-variants/dist/index.cjs');
8
7
  var hooks_useObjectAttributes = require('../../../hooks/useObjectAttributes.cjs');
9
8
  require('../../../intl/IntlProvider.cjs');
@@ -78,7 +77,7 @@ const TriggerLabelText = React.memo(({ children, onlyIconTrigger }) => {
78
77
  return children;
79
78
  }
80
79
  const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || components_Icon_FaIcon.FaEllipsisIcon;
81
- return jsxRuntime.jsx(Icon, { alt: typeof children === 'string' ? children : index$1(children) });
80
+ return jsxRuntime.jsx(Icon, { alt: children });
82
81
  });
83
82
  const renderButtonList = (children) => React.Children.map(children, (item) => {
84
83
  if (!item || !React.isValidElement(item)) {
@@ -111,6 +110,9 @@ const ButtonListItem = ({ children }) => {
111
110
  observer.observe(listItem, {
112
111
  childList: true,
113
112
  subtree: true,
113
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
114
+ attributes: true,
115
+ attributeFilter: ['disabled', 'aria-disabled'],
114
116
  });
115
117
  return () => {
116
118
  observer.disconnect();
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenuButton.cjs","sources":["../../../../src/components/Dropdown/DropdownMenuButton/DropdownMenuButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n type ComponentProps,\n type ComponentPropsWithRef,\n type ComponentType,\n type FC,\n Fragment,\n type ReactElement,\n type ReactNode,\n isValidElement,\n memo,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useObjectAttributes } from '../../../hooks/useObjectAttributes'\nimport { useIntl } from '../../../intl'\nimport { type AnchorButton, Button, type AbstractProps as ButtonProps } from '../../Button'\nimport { FaCaretDownIcon, FaEllipsisIcon } from '../../Icon'\nimport { Dropdown, DropdownContext } from '../Dropdown'\nimport { DropdownCloser } from '../DropdownCloser'\nimport { DropdownContent } from '../DropdownContent'\nimport { DropdownTrigger } from '../DropdownTrigger'\n\nimport { DropdownMenuGroup } from './DropdownMenuGroup'\nimport useKeyboardNavigation from './useKeyboardNavigation'\n\nimport type { RemoteDialogTrigger } from '../../Dialog'\n\ntype Actions = ActionItem | ActionItem[]\n\n// これでコンポーネントを絞れるわけではないが Button[variant=text] を使ってほしいんだよ! という気持ち\ntype ActionItem =\n | ReactElement<ComponentProps<typeof Button>>\n | ReactElement<ComponentProps<typeof AnchorButton>>\n | ReactElement<ComponentProps<typeof RemoteDialogTrigger>>\n | ReactNode\n\ntype ObjectTriggerType = {\n /** 引き金となるボタンラベル */\n children: ReactNode\n /** 引き金となるボタンの大きさ */\n size?: ButtonProps['size']\n /** 引き金となるボタンをアイコンのみとするかどうか */\n onlyIcon?:\n | boolean\n | {\n /** 引き金となるアイコンを差し替えたい場合(onlyIcon=true の場合のみ有効) */\n component?: ComponentType<ComponentProps<typeof FaCaretDownIcon>>\n }\n}\ntype AbstractProps = {\n /** 引き金となるボタン */\n trigger: ReactNode | ObjectTriggerType\n /** 操作群 */\n children: Actions\n /** ドロップダウンメニューが開かれた際のイベント */\n onOpen?: () => void\n /** ドロップダウンメニューが閉じられた際のイベント */\n onClose?: () => void\n}\ntype ElementProps = Omit<ComponentPropsWithRef<'button'>, keyof AbstractProps>\ntype Props = AbstractProps & ElementProps\n\nconst triggerObjectConverter = (trigger: ReactNode) => ({\n children: trigger,\n})\n\nconst classNameGenerator = tv({\n slots: {\n triggerWrapper: 'smarthr-ui-DropdownMenuButton',\n triggerButton:\n 'smarthr-ui-DropdownMenuButton-trigger [&[aria-expanded=\"true\"]_.smarthr-ui-Icon:last-child]:shr-rotate-180',\n actionList: [\n 'smarthr-ui-DropdownMenuButton-panel',\n 'shr-list-none shr-py-0.5',\n [\n /* unset した Button の右 padding 分 */\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-pe-1',\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-gap-x-0.5',\n '[&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-w-[unset] [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-bg-transparent [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-pe-[unset]',\n ],\n ],\n actionListItemButton: [\n // HINT: 実際にレンダリングされた要素のclassに対して追加されるため、優先度を上げる必要がある\n '[&&]:shr-w-full [&&]:shr-justify-start [&&]:shr-rounded-none [&&]:shr-border-none [&&]:shr-py-0.5 [&&]:shr-font-normal',\n '[&&]:focus-visible:shr-focus-indicator',\n ],\n },\n})\n\nconst { triggerWrapper, triggerButton, actionList, actionListItemButton } = classNameGenerator()\n\nexport const DropdownMenuButton: FC<Props> = ({\n trigger,\n children,\n onOpen,\n onClose,\n className,\n ...rest\n}) => {\n const {\n children: triggerChildren,\n size: triggerSize,\n onlyIcon: onlyIconTrigger,\n } = useObjectAttributes<ReactNode | ObjectTriggerType, ObjectTriggerType>(\n trigger,\n triggerObjectConverter,\n )\n\n const containerRef = useRef<HTMLUListElement>(null)\n\n useKeyboardNavigation(containerRef)\n\n const classNames = useMemo(\n () => ({\n triggerWrapper: triggerWrapper({ className }),\n triggerButton: triggerButton(),\n actionList: actionList(),\n }),\n [className],\n )\n\n return (\n <Dropdown onOpen={onOpen} onClose={onClose}>\n <MemoizedTriggerButton\n {...rest}\n onlyIconTrigger={onlyIconTrigger}\n triggerSize={triggerSize}\n classNames={classNames}\n >\n {triggerChildren}\n </MemoizedTriggerButton>\n <DropdownContent controllable={true}>\n <menu ref={containerRef} role=\"menu\" className={classNames.actionList}>\n {renderButtonList(children)}\n </menu>\n </DropdownContent>\n </Dropdown>\n )\n}\n\nconst MemoizedTriggerButton = memo<\n ElementProps & {\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n triggerSize: ObjectTriggerType['size']\n children: ObjectTriggerType['children']\n classNames: {\n triggerWrapper: string\n triggerButton: string\n }\n }\n>(({ onlyIconTrigger, triggerSize, children, classNames, ...rest }) => {\n const { localize } = useIntl()\n\n const { active } = useContext(DropdownContext)\n\n return (\n <DropdownTrigger\n className={classNames.triggerWrapper}\n tooltip={{ show: !!onlyIconTrigger, message: children }}\n >\n <Button\n {...rest}\n suffix={\n !onlyIconTrigger && (\n <FaCaretDownIcon\n alt={\n active\n ? localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerActive',\n defaultText: '候補を閉じる',\n })\n : localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerInactive',\n defaultText: '候補を開く',\n })\n }\n />\n )\n }\n size={triggerSize}\n className={classNames.triggerButton}\n >\n <TriggerLabelText onlyIconTrigger={onlyIconTrigger}>{children}</TriggerLabelText>\n </Button>\n </DropdownTrigger>\n )\n})\n\nconst TriggerLabelText = memo<{\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n children: ObjectTriggerType['children']\n}>(({ children, onlyIconTrigger }) => {\n if (!onlyIconTrigger) {\n return children\n }\n\n const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || FaEllipsisIcon\n\n return <Icon alt={typeof children === 'string' ? children : innerText(children)} />\n})\n\nexport const renderButtonList = (children: Actions) =>\n Children.map(children, (item): ReactNode => {\n if (!item || !isValidElement(item)) {\n return null\n }\n\n switch (item.type) {\n case Fragment:\n return renderButtonList(item.props.children)\n case DropdownMenuGroup:\n return item\n }\n\n return <ButtonListItem>{item}</ButtonListItem>\n })\n\nconst ButtonListItem: FC<{ children: ReactElement }> = ({ children }) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n const listItem = ref.current\n if (!listItem) {\n return\n }\n\n const setupButton = () => {\n const button = listItem.querySelector('button,a')\n\n if (button) {\n button.setAttribute('role', 'menuitem')\n button.setAttribute(\n 'class',\n actionListItemButton({ className: button.getAttribute('class') }),\n )\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(listItem, {\n childList: true,\n subtree: true,\n })\n\n return () => {\n observer.disconnect()\n }\n }, [])\n\n return (\n <li role=\"presentation\" ref={ref}>\n <DropdownCloser>{children}</DropdownCloser>\n </li>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsEA;AACE;AACD;AAED;AACE;AACE;AACA;AAEA;;;AAGE;;;;;AAKC;AACF;AACD;;;;AAIC;AACF;AACF;AAED;;;AAmBE;;AAIA;AAEI;;;AAGD;AAIH;AAiBF;AAEA;AAWE;;AAIA;;AAaoB;AACA;;;AAGA;AACA;;AAatB;AAEA;;AAKI;;AAGF;;AAGF;AAEO;;AAGD;;AAGF;AACE;;AAEA;AACE;;AAGJ;AACF;AAEF;AACE;;AAGE;;;;;;;AASI;AACA;;AAKJ;AAEA;AAEA;AACA;AACE;AACA;AACD;AAED;;AAEA;;AAGF;AAKF;;;"}
1
+ {"version":3,"file":"DropdownMenuButton.cjs","sources":["../../../../src/components/Dropdown/DropdownMenuButton/DropdownMenuButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n type ComponentProps,\n type ComponentPropsWithRef,\n type ComponentType,\n type FC,\n Fragment,\n type ReactElement,\n type ReactNode,\n isValidElement,\n memo,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useObjectAttributes } from '../../../hooks/useObjectAttributes'\nimport { useIntl } from '../../../intl'\nimport { type AnchorButton, Button, type AbstractProps as ButtonProps } from '../../Button'\nimport { FaCaretDownIcon, FaEllipsisIcon } from '../../Icon'\nimport { Dropdown, DropdownContext } from '../Dropdown'\nimport { DropdownCloser } from '../DropdownCloser'\nimport { DropdownContent } from '../DropdownContent'\nimport { DropdownTrigger } from '../DropdownTrigger'\n\nimport { DropdownMenuGroup } from './DropdownMenuGroup'\nimport useKeyboardNavigation from './useKeyboardNavigation'\n\nimport type { RemoteDialogTrigger } from '../../Dialog'\n\ntype Actions = ActionItem | ActionItem[]\n\n// これでコンポーネントを絞れるわけではないが Button[variant=text] を使ってほしいんだよ! という気持ち\ntype ActionItem =\n | ReactElement<ComponentProps<typeof Button>>\n | ReactElement<ComponentProps<typeof AnchorButton>>\n | ReactElement<ComponentProps<typeof RemoteDialogTrigger>>\n | ReactNode\n\ntype ObjectTriggerType = {\n /** 引き金となるボタンラベル */\n children: ReactNode\n /** 引き金となるボタンの大きさ */\n size?: ButtonProps['size']\n /** 引き金となるボタンをアイコンのみとするかどうか */\n onlyIcon?:\n | boolean\n | {\n /** 引き金となるアイコンを差し替えたい場合(onlyIcon=true の場合のみ有効) */\n component?: ComponentType<ComponentProps<typeof FaCaretDownIcon>>\n }\n}\ntype AbstractProps = {\n /** 引き金となるボタン */\n trigger: ReactNode | ObjectTriggerType\n /** 操作群 */\n children: Actions\n /** ドロップダウンメニューが開かれた際のイベント */\n onOpen?: () => void\n /** ドロップダウンメニューが閉じられた際のイベント */\n onClose?: () => void\n}\ntype ElementProps = Omit<ComponentPropsWithRef<'button'>, keyof AbstractProps>\ntype Props = AbstractProps & ElementProps\n\nconst triggerObjectConverter = (trigger: ReactNode) => ({\n children: trigger,\n})\n\nconst classNameGenerator = tv({\n slots: {\n triggerWrapper: 'smarthr-ui-DropdownMenuButton',\n triggerButton:\n 'smarthr-ui-DropdownMenuButton-trigger [&[aria-expanded=\"true\"]_.smarthr-ui-Icon:last-child]:shr-rotate-180',\n actionList: [\n 'smarthr-ui-DropdownMenuButton-panel',\n 'shr-list-none shr-py-0.5',\n [\n /* unset した Button の右 padding 分 */\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-pe-1',\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-gap-x-0.5',\n '[&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-w-[unset] [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-bg-transparent [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-pe-[unset]',\n ],\n ],\n actionListItemButton: [\n // HINT: 実際にレンダリングされた要素のclassに対して追加されるため、優先度を上げる必要がある\n '[&&]:shr-w-full [&&]:shr-justify-start [&&]:shr-rounded-none [&&]:shr-border-none [&&]:shr-py-0.5 [&&]:shr-font-normal',\n '[&&]:focus-visible:shr-focus-indicator',\n ],\n },\n})\n\nconst { triggerWrapper, triggerButton, actionList, actionListItemButton } = classNameGenerator()\n\nexport const DropdownMenuButton: FC<Props> = ({\n trigger,\n children,\n onOpen,\n onClose,\n className,\n ...rest\n}) => {\n const {\n children: triggerChildren,\n size: triggerSize,\n onlyIcon: onlyIconTrigger,\n } = useObjectAttributes<ReactNode | ObjectTriggerType, ObjectTriggerType>(\n trigger,\n triggerObjectConverter,\n )\n\n const containerRef = useRef<HTMLUListElement>(null)\n\n useKeyboardNavigation(containerRef)\n\n const classNames = useMemo(\n () => ({\n triggerWrapper: triggerWrapper({ className }),\n triggerButton: triggerButton(),\n actionList: actionList(),\n }),\n [className],\n )\n\n return (\n <Dropdown onOpen={onOpen} onClose={onClose}>\n <MemoizedTriggerButton\n {...rest}\n onlyIconTrigger={onlyIconTrigger}\n triggerSize={triggerSize}\n classNames={classNames}\n >\n {triggerChildren}\n </MemoizedTriggerButton>\n <DropdownContent controllable={true}>\n <menu ref={containerRef} role=\"menu\" className={classNames.actionList}>\n {renderButtonList(children)}\n </menu>\n </DropdownContent>\n </Dropdown>\n )\n}\n\nconst MemoizedTriggerButton = memo<\n ElementProps & {\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n triggerSize: ObjectTriggerType['size']\n children: ObjectTriggerType['children']\n classNames: {\n triggerWrapper: string\n triggerButton: string\n }\n }\n>(({ onlyIconTrigger, triggerSize, children, classNames, ...rest }) => {\n const { localize } = useIntl()\n\n const { active } = useContext(DropdownContext)\n\n return (\n <DropdownTrigger\n className={classNames.triggerWrapper}\n tooltip={{ show: !!onlyIconTrigger, message: children }}\n >\n <Button\n {...rest}\n suffix={\n !onlyIconTrigger && (\n <FaCaretDownIcon\n alt={\n active\n ? localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerActive',\n defaultText: '候補を閉じる',\n })\n : localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerInactive',\n defaultText: '候補を開く',\n })\n }\n />\n )\n }\n size={triggerSize}\n className={classNames.triggerButton}\n >\n <TriggerLabelText onlyIconTrigger={onlyIconTrigger}>{children}</TriggerLabelText>\n </Button>\n </DropdownTrigger>\n )\n})\n\nconst TriggerLabelText = memo<{\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n children: ObjectTriggerType['children']\n}>(({ children, onlyIconTrigger }) => {\n if (!onlyIconTrigger) {\n return children\n }\n\n const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || FaEllipsisIcon\n\n return <Icon alt={children} />\n})\n\nexport const renderButtonList = (children: Actions) =>\n Children.map(children, (item): ReactNode => {\n if (!item || !isValidElement(item)) {\n return null\n }\n\n switch (item.type) {\n case Fragment:\n return renderButtonList(item.props.children)\n case DropdownMenuGroup:\n return item\n }\n\n return <ButtonListItem>{item}</ButtonListItem>\n })\n\nconst ButtonListItem: FC<{ children: ReactElement }> = ({ children }) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n const listItem = ref.current\n if (!listItem) {\n return\n }\n\n const setupButton = () => {\n const button = listItem.querySelector('button,a')\n\n if (button) {\n button.setAttribute('role', 'menuitem')\n button.setAttribute(\n 'class',\n actionListItemButton({ className: button.getAttribute('class') }),\n )\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(listItem, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n }\n }, [])\n\n return (\n <li role=\"presentation\" ref={ref}>\n <DropdownCloser>{children}</DropdownCloser>\n </li>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAqEA;AACE;AACD;AAED;AACE;AACE;AACA;AAEA;;;AAGE;;;;;AAKC;AACF;AACD;;;;AAIC;AACF;AACF;AAED;;;AAmBE;;AAIA;AAEI;;;AAGD;AAIH;AAiBF;AAEA;AAWE;;AAIA;;AAaoB;AACA;;;AAGA;AACA;;AAatB;AAEA;;AAKI;;AAGF;AAEA;AACF;AAEO;;AAGD;;AAGF;AACE;;AAEA;AACE;;AAGJ;AACF;AAEF;AACE;;AAGE;;;;;;;AASI;AACA;;AAKJ;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEA;;AAGF;AAKF;;;"}
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import { memo, useContext, useRef, useMemo, Children, isValidElement, Fragment, useEffect } from 'react';
4
- import innerText from '../../../_virtual/index3.js';
5
4
  import { tv as ce } from './../../../vendor/.pnpm/tailwind-variants@0.3.1_tailwindcss@3.4.19_ts-node@10.9.2_@swc_core@1.15.17_@types_node@20.19.35_typescript@5.9.3__/vendor/tailwind-variants/dist/index.js';
6
5
  import { useObjectAttributes } from '../../../hooks/useObjectAttributes.js';
7
6
  import '../../../intl/IntlProvider.js';
@@ -76,7 +75,7 @@ const TriggerLabelText = memo(({ children, onlyIconTrigger }) => {
76
75
  return children;
77
76
  }
78
77
  const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || FaEllipsisIcon;
79
- return jsx(Icon, { alt: typeof children === 'string' ? children : innerText(children) });
78
+ return jsx(Icon, { alt: children });
80
79
  });
81
80
  const renderButtonList = (children) => Children.map(children, (item) => {
82
81
  if (!item || !isValidElement(item)) {
@@ -109,6 +108,9 @@ const ButtonListItem = ({ children }) => {
109
108
  observer.observe(listItem, {
110
109
  childList: true,
111
110
  subtree: true,
111
+ // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す
112
+ attributes: true,
113
+ attributeFilter: ['disabled', 'aria-disabled'],
112
114
  });
113
115
  return () => {
114
116
  observer.disconnect();
@@ -1 +1 @@
1
- {"version":3,"file":"DropdownMenuButton.js","sources":["../../../../src/components/Dropdown/DropdownMenuButton/DropdownMenuButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n type ComponentProps,\n type ComponentPropsWithRef,\n type ComponentType,\n type FC,\n Fragment,\n type ReactElement,\n type ReactNode,\n isValidElement,\n memo,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useObjectAttributes } from '../../../hooks/useObjectAttributes'\nimport { useIntl } from '../../../intl'\nimport { type AnchorButton, Button, type AbstractProps as ButtonProps } from '../../Button'\nimport { FaCaretDownIcon, FaEllipsisIcon } from '../../Icon'\nimport { Dropdown, DropdownContext } from '../Dropdown'\nimport { DropdownCloser } from '../DropdownCloser'\nimport { DropdownContent } from '../DropdownContent'\nimport { DropdownTrigger } from '../DropdownTrigger'\n\nimport { DropdownMenuGroup } from './DropdownMenuGroup'\nimport useKeyboardNavigation from './useKeyboardNavigation'\n\nimport type { RemoteDialogTrigger } from '../../Dialog'\n\ntype Actions = ActionItem | ActionItem[]\n\n// これでコンポーネントを絞れるわけではないが Button[variant=text] を使ってほしいんだよ! という気持ち\ntype ActionItem =\n | ReactElement<ComponentProps<typeof Button>>\n | ReactElement<ComponentProps<typeof AnchorButton>>\n | ReactElement<ComponentProps<typeof RemoteDialogTrigger>>\n | ReactNode\n\ntype ObjectTriggerType = {\n /** 引き金となるボタンラベル */\n children: ReactNode\n /** 引き金となるボタンの大きさ */\n size?: ButtonProps['size']\n /** 引き金となるボタンをアイコンのみとするかどうか */\n onlyIcon?:\n | boolean\n | {\n /** 引き金となるアイコンを差し替えたい場合(onlyIcon=true の場合のみ有効) */\n component?: ComponentType<ComponentProps<typeof FaCaretDownIcon>>\n }\n}\ntype AbstractProps = {\n /** 引き金となるボタン */\n trigger: ReactNode | ObjectTriggerType\n /** 操作群 */\n children: Actions\n /** ドロップダウンメニューが開かれた際のイベント */\n onOpen?: () => void\n /** ドロップダウンメニューが閉じられた際のイベント */\n onClose?: () => void\n}\ntype ElementProps = Omit<ComponentPropsWithRef<'button'>, keyof AbstractProps>\ntype Props = AbstractProps & ElementProps\n\nconst triggerObjectConverter = (trigger: ReactNode) => ({\n children: trigger,\n})\n\nconst classNameGenerator = tv({\n slots: {\n triggerWrapper: 'smarthr-ui-DropdownMenuButton',\n triggerButton:\n 'smarthr-ui-DropdownMenuButton-trigger [&[aria-expanded=\"true\"]_.smarthr-ui-Icon:last-child]:shr-rotate-180',\n actionList: [\n 'smarthr-ui-DropdownMenuButton-panel',\n 'shr-list-none shr-py-0.5',\n [\n /* unset した Button の右 padding 分 */\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-pe-1',\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-gap-x-0.5',\n '[&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-w-[unset] [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-bg-transparent [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-pe-[unset]',\n ],\n ],\n actionListItemButton: [\n // HINT: 実際にレンダリングされた要素のclassに対して追加されるため、優先度を上げる必要がある\n '[&&]:shr-w-full [&&]:shr-justify-start [&&]:shr-rounded-none [&&]:shr-border-none [&&]:shr-py-0.5 [&&]:shr-font-normal',\n '[&&]:focus-visible:shr-focus-indicator',\n ],\n },\n})\n\nconst { triggerWrapper, triggerButton, actionList, actionListItemButton } = classNameGenerator()\n\nexport const DropdownMenuButton: FC<Props> = ({\n trigger,\n children,\n onOpen,\n onClose,\n className,\n ...rest\n}) => {\n const {\n children: triggerChildren,\n size: triggerSize,\n onlyIcon: onlyIconTrigger,\n } = useObjectAttributes<ReactNode | ObjectTriggerType, ObjectTriggerType>(\n trigger,\n triggerObjectConverter,\n )\n\n const containerRef = useRef<HTMLUListElement>(null)\n\n useKeyboardNavigation(containerRef)\n\n const classNames = useMemo(\n () => ({\n triggerWrapper: triggerWrapper({ className }),\n triggerButton: triggerButton(),\n actionList: actionList(),\n }),\n [className],\n )\n\n return (\n <Dropdown onOpen={onOpen} onClose={onClose}>\n <MemoizedTriggerButton\n {...rest}\n onlyIconTrigger={onlyIconTrigger}\n triggerSize={triggerSize}\n classNames={classNames}\n >\n {triggerChildren}\n </MemoizedTriggerButton>\n <DropdownContent controllable={true}>\n <menu ref={containerRef} role=\"menu\" className={classNames.actionList}>\n {renderButtonList(children)}\n </menu>\n </DropdownContent>\n </Dropdown>\n )\n}\n\nconst MemoizedTriggerButton = memo<\n ElementProps & {\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n triggerSize: ObjectTriggerType['size']\n children: ObjectTriggerType['children']\n classNames: {\n triggerWrapper: string\n triggerButton: string\n }\n }\n>(({ onlyIconTrigger, triggerSize, children, classNames, ...rest }) => {\n const { localize } = useIntl()\n\n const { active } = useContext(DropdownContext)\n\n return (\n <DropdownTrigger\n className={classNames.triggerWrapper}\n tooltip={{ show: !!onlyIconTrigger, message: children }}\n >\n <Button\n {...rest}\n suffix={\n !onlyIconTrigger && (\n <FaCaretDownIcon\n alt={\n active\n ? localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerActive',\n defaultText: '候補を閉じる',\n })\n : localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerInactive',\n defaultText: '候補を開く',\n })\n }\n />\n )\n }\n size={triggerSize}\n className={classNames.triggerButton}\n >\n <TriggerLabelText onlyIconTrigger={onlyIconTrigger}>{children}</TriggerLabelText>\n </Button>\n </DropdownTrigger>\n )\n})\n\nconst TriggerLabelText = memo<{\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n children: ObjectTriggerType['children']\n}>(({ children, onlyIconTrigger }) => {\n if (!onlyIconTrigger) {\n return children\n }\n\n const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || FaEllipsisIcon\n\n return <Icon alt={typeof children === 'string' ? children : innerText(children)} />\n})\n\nexport const renderButtonList = (children: Actions) =>\n Children.map(children, (item): ReactNode => {\n if (!item || !isValidElement(item)) {\n return null\n }\n\n switch (item.type) {\n case Fragment:\n return renderButtonList(item.props.children)\n case DropdownMenuGroup:\n return item\n }\n\n return <ButtonListItem>{item}</ButtonListItem>\n })\n\nconst ButtonListItem: FC<{ children: ReactElement }> = ({ children }) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n const listItem = ref.current\n if (!listItem) {\n return\n }\n\n const setupButton = () => {\n const button = listItem.querySelector('button,a')\n\n if (button) {\n button.setAttribute('role', 'menuitem')\n button.setAttribute(\n 'class',\n actionListItemButton({ className: button.getAttribute('class') }),\n )\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(listItem, {\n childList: true,\n subtree: true,\n })\n\n return () => {\n observer.disconnect()\n }\n }, [])\n\n return (\n <li role=\"presentation\" ref={ref}>\n <DropdownCloser>{children}</DropdownCloser>\n </li>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsEA;AACE;AACD;AAED;AACE;AACE;AACA;AAEA;;;AAGE;;;;;AAKC;AACF;AACD;;;;AAIC;AACF;AACF;AAED;;;AAmBE;;AAIA;AAEI;;;AAGD;AAIH;AAiBF;AAEA;AAWE;;AAIA;;AAaoB;AACA;;;AAGA;AACA;;AAatB;AAEA;;AAKI;;AAGF;;AAGF;AAEO;;AAGD;;AAGF;AACE;;AAEA;AACE;;AAGJ;AACF;AAEF;AACE;;AAGE;;;;;;;AASI;AACA;;AAKJ;AAEA;AAEA;AACA;AACE;AACA;AACD;AAED;;AAEA;;AAGF;AAKF;;"}
1
+ {"version":3,"file":"DropdownMenuButton.js","sources":["../../../../src/components/Dropdown/DropdownMenuButton/DropdownMenuButton.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n type ComponentProps,\n type ComponentPropsWithRef,\n type ComponentType,\n type FC,\n Fragment,\n type ReactElement,\n type ReactNode,\n isValidElement,\n memo,\n useContext,\n useEffect,\n useMemo,\n useRef,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useObjectAttributes } from '../../../hooks/useObjectAttributes'\nimport { useIntl } from '../../../intl'\nimport { type AnchorButton, Button, type AbstractProps as ButtonProps } from '../../Button'\nimport { FaCaretDownIcon, FaEllipsisIcon } from '../../Icon'\nimport { Dropdown, DropdownContext } from '../Dropdown'\nimport { DropdownCloser } from '../DropdownCloser'\nimport { DropdownContent } from '../DropdownContent'\nimport { DropdownTrigger } from '../DropdownTrigger'\n\nimport { DropdownMenuGroup } from './DropdownMenuGroup'\nimport useKeyboardNavigation from './useKeyboardNavigation'\n\nimport type { RemoteDialogTrigger } from '../../Dialog'\n\ntype Actions = ActionItem | ActionItem[]\n\n// これでコンポーネントを絞れるわけではないが Button[variant=text] を使ってほしいんだよ! という気持ち\ntype ActionItem =\n | ReactElement<ComponentProps<typeof Button>>\n | ReactElement<ComponentProps<typeof AnchorButton>>\n | ReactElement<ComponentProps<typeof RemoteDialogTrigger>>\n | ReactNode\n\ntype ObjectTriggerType = {\n /** 引き金となるボタンラベル */\n children: ReactNode\n /** 引き金となるボタンの大きさ */\n size?: ButtonProps['size']\n /** 引き金となるボタンをアイコンのみとするかどうか */\n onlyIcon?:\n | boolean\n | {\n /** 引き金となるアイコンを差し替えたい場合(onlyIcon=true の場合のみ有効) */\n component?: ComponentType<ComponentProps<typeof FaCaretDownIcon>>\n }\n}\ntype AbstractProps = {\n /** 引き金となるボタン */\n trigger: ReactNode | ObjectTriggerType\n /** 操作群 */\n children: Actions\n /** ドロップダウンメニューが開かれた際のイベント */\n onOpen?: () => void\n /** ドロップダウンメニューが閉じられた際のイベント */\n onClose?: () => void\n}\ntype ElementProps = Omit<ComponentPropsWithRef<'button'>, keyof AbstractProps>\ntype Props = AbstractProps & ElementProps\n\nconst triggerObjectConverter = (trigger: ReactNode) => ({\n children: trigger,\n})\n\nconst classNameGenerator = tv({\n slots: {\n triggerWrapper: 'smarthr-ui-DropdownMenuButton',\n triggerButton:\n 'smarthr-ui-DropdownMenuButton-trigger [&[aria-expanded=\"true\"]_.smarthr-ui-Icon:last-child]:shr-rotate-180',\n actionList: [\n 'smarthr-ui-DropdownMenuButton-panel',\n 'shr-list-none shr-py-0.5',\n [\n /* unset した Button の右 padding 分 */\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-pe-1',\n '[&_.smarthr-ui-Button-disabledWrapper]:shr-gap-x-0.5',\n '[&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-w-[unset] [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-bg-transparent [&_.smarthr-ui-Button-disabledWrapper_>_.smarthr-ui-Button]:shr-pe-[unset]',\n ],\n ],\n actionListItemButton: [\n // HINT: 実際にレンダリングされた要素のclassに対して追加されるため、優先度を上げる必要がある\n '[&&]:shr-w-full [&&]:shr-justify-start [&&]:shr-rounded-none [&&]:shr-border-none [&&]:shr-py-0.5 [&&]:shr-font-normal',\n '[&&]:focus-visible:shr-focus-indicator',\n ],\n },\n})\n\nconst { triggerWrapper, triggerButton, actionList, actionListItemButton } = classNameGenerator()\n\nexport const DropdownMenuButton: FC<Props> = ({\n trigger,\n children,\n onOpen,\n onClose,\n className,\n ...rest\n}) => {\n const {\n children: triggerChildren,\n size: triggerSize,\n onlyIcon: onlyIconTrigger,\n } = useObjectAttributes<ReactNode | ObjectTriggerType, ObjectTriggerType>(\n trigger,\n triggerObjectConverter,\n )\n\n const containerRef = useRef<HTMLUListElement>(null)\n\n useKeyboardNavigation(containerRef)\n\n const classNames = useMemo(\n () => ({\n triggerWrapper: triggerWrapper({ className }),\n triggerButton: triggerButton(),\n actionList: actionList(),\n }),\n [className],\n )\n\n return (\n <Dropdown onOpen={onOpen} onClose={onClose}>\n <MemoizedTriggerButton\n {...rest}\n onlyIconTrigger={onlyIconTrigger}\n triggerSize={triggerSize}\n classNames={classNames}\n >\n {triggerChildren}\n </MemoizedTriggerButton>\n <DropdownContent controllable={true}>\n <menu ref={containerRef} role=\"menu\" className={classNames.actionList}>\n {renderButtonList(children)}\n </menu>\n </DropdownContent>\n </Dropdown>\n )\n}\n\nconst MemoizedTriggerButton = memo<\n ElementProps & {\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n triggerSize: ObjectTriggerType['size']\n children: ObjectTriggerType['children']\n classNames: {\n triggerWrapper: string\n triggerButton: string\n }\n }\n>(({ onlyIconTrigger, triggerSize, children, classNames, ...rest }) => {\n const { localize } = useIntl()\n\n const { active } = useContext(DropdownContext)\n\n return (\n <DropdownTrigger\n className={classNames.triggerWrapper}\n tooltip={{ show: !!onlyIconTrigger, message: children }}\n >\n <Button\n {...rest}\n suffix={\n !onlyIconTrigger && (\n <FaCaretDownIcon\n alt={\n active\n ? localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerActive',\n defaultText: '候補を閉じる',\n })\n : localize({\n id: 'smarthr-ui/DropdownMenuButton/triggerInactive',\n defaultText: '候補を開く',\n })\n }\n />\n )\n }\n size={triggerSize}\n className={classNames.triggerButton}\n >\n <TriggerLabelText onlyIconTrigger={onlyIconTrigger}>{children}</TriggerLabelText>\n </Button>\n </DropdownTrigger>\n )\n})\n\nconst TriggerLabelText = memo<{\n onlyIconTrigger: ObjectTriggerType['onlyIcon']\n children: ObjectTriggerType['children']\n}>(({ children, onlyIconTrigger }) => {\n if (!onlyIconTrigger) {\n return children\n }\n\n const Icon = (typeof onlyIconTrigger === 'object' && onlyIconTrigger.component) || FaEllipsisIcon\n\n return <Icon alt={children} />\n})\n\nexport const renderButtonList = (children: Actions) =>\n Children.map(children, (item): ReactNode => {\n if (!item || !isValidElement(item)) {\n return null\n }\n\n switch (item.type) {\n case Fragment:\n return renderButtonList(item.props.children)\n case DropdownMenuGroup:\n return item\n }\n\n return <ButtonListItem>{item}</ButtonListItem>\n })\n\nconst ButtonListItem: FC<{ children: ReactElement }> = ({ children }) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n const listItem = ref.current\n if (!listItem) {\n return\n }\n\n const setupButton = () => {\n const button = listItem.querySelector('button,a')\n\n if (button) {\n button.setAttribute('role', 'menuitem')\n button.setAttribute(\n 'class',\n actionListItemButton({ className: button.getAttribute('class') }),\n )\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(listItem, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n }\n }, [])\n\n return (\n <li role=\"presentation\" ref={ref}>\n <DropdownCloser>{children}</DropdownCloser>\n </li>\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAqEA;AACE;AACD;AAED;AACE;AACE;AACA;AAEA;;;AAGE;;;;;AAKC;AACF;AACD;;;;AAIC;AACF;AACF;AAED;;;AAmBE;;AAIA;AAEI;;;AAGD;AAIH;AAiBF;AAEA;AAWE;;AAIA;;AAaoB;AACA;;;AAGA;AACA;;AAatB;AAEA;;AAKI;;AAGF;AAEA;AACF;AAEO;;AAGD;;AAGF;AACE;;AAEA;AACE;;AAGJ;AACF;AAEF;AACE;;AAGE;;;;;;;AASI;AACA;;AAKJ;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEA;;AAGF;AAKF;;"}