smarthr-ui 99.3.0 → 99.3.2

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 (218) hide show
  1. package/lib/_virtual/index.cjs +2 -2
  2. package/lib/_virtual/index.js +2 -2
  3. package/lib/_virtual/index2.cjs +2 -2
  4. package/lib/_virtual/index2.js +2 -2
  5. package/lib/components/Checkbox/Checkbox.cjs +1 -1
  6. package/lib/components/Checkbox/Checkbox.cjs.map +1 -1
  7. package/lib/components/Checkbox/Checkbox.js +1 -1
  8. package/lib/components/Checkbox/Checkbox.js.map +1 -1
  9. package/lib/components/Combobox/ItemButton.cjs +11 -2
  10. package/lib/components/Combobox/ItemButton.cjs.map +1 -1
  11. package/lib/components/Combobox/ItemButton.js +11 -2
  12. package/lib/components/Combobox/ItemButton.js.map +1 -1
  13. package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs +2 -2
  14. package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs.map +1 -1
  15. package/lib/components/Combobox/MultiCombobox/MultiCombobox.js +2 -2
  16. package/lib/components/Combobox/MultiCombobox/MultiCombobox.js.map +1 -1
  17. package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs +2 -2
  18. package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs.map +1 -1
  19. package/lib/components/Combobox/SingleCombobox/SingleCombobox.js +2 -2
  20. package/lib/components/Combobox/SingleCombobox/SingleCombobox.js.map +1 -1
  21. package/lib/components/DatePicker/DatePicker.cjs +2 -2
  22. package/lib/components/DatePicker/DatePicker.cjs.map +1 -1
  23. package/lib/components/DatePicker/DatePicker.js +2 -2
  24. package/lib/components/DatePicker/DatePicker.js.map +1 -1
  25. package/lib/components/DatePicker/Portal.cjs +1 -1
  26. package/lib/components/DatePicker/Portal.cjs.map +1 -1
  27. package/lib/components/DatePicker/Portal.js +1 -1
  28. package/lib/components/DatePicker/Portal.js.map +1 -1
  29. package/lib/components/Dialog/FocusTrap.cjs +8 -8
  30. package/lib/components/Dialog/FocusTrap.cjs.map +1 -1
  31. package/lib/components/Dialog/FocusTrap.js +9 -9
  32. package/lib/components/Dialog/FocusTrap.js.map +1 -1
  33. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs +2 -1
  34. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs.map +1 -1
  35. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js +2 -1
  36. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js.map +1 -1
  37. package/lib/components/DropZone/DropZone.cjs +1 -1
  38. package/lib/components/DropZone/DropZone.cjs.map +1 -1
  39. package/lib/components/DropZone/DropZone.js +1 -1
  40. package/lib/components/DropZone/DropZone.js.map +1 -1
  41. package/lib/components/Heading/PageHeading/PageHeading.cjs +1 -1
  42. package/lib/components/Heading/PageHeading/PageHeading.cjs.map +1 -1
  43. package/lib/components/Heading/PageHeading/PageHeading.js +1 -1
  44. package/lib/components/Heading/PageHeading/PageHeading.js.map +1 -1
  45. package/lib/components/Input/CurrencyInput/CurrencyInput.cjs +3 -3
  46. package/lib/components/Input/CurrencyInput/CurrencyInput.cjs.map +1 -1
  47. package/lib/components/Input/CurrencyInput/CurrencyInput.js +3 -3
  48. package/lib/components/Input/CurrencyInput/CurrencyInput.js.map +1 -1
  49. package/lib/components/Input/Input.cjs +15 -17
  50. package/lib/components/Input/Input.cjs.map +1 -1
  51. package/lib/components/Input/Input.js +15 -17
  52. package/lib/components/Input/Input.js.map +1 -1
  53. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +8 -7
  54. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
  55. package/lib/components/InputFile/InputFileMultiplyAppendable.js +8 -7
  56. package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
  57. package/lib/components/InputFile/InputFileNative.cjs +6 -5
  58. package/lib/components/InputFile/InputFileNative.cjs.map +1 -1
  59. package/lib/components/InputFile/InputFileNative.js +6 -5
  60. package/lib/components/InputFile/InputFileNative.js.map +1 -1
  61. package/lib/components/Layout/Container/Container.cjs.map +1 -1
  62. package/lib/components/Layout/Container/Container.js.map +1 -1
  63. package/lib/components/Pagination/Pagination.cjs +3 -2
  64. package/lib/components/Pagination/Pagination.cjs.map +1 -1
  65. package/lib/components/Pagination/Pagination.js +2 -1
  66. package/lib/components/Pagination/Pagination.js.map +1 -1
  67. package/lib/components/Panel/Groupbox/Groupbox.cjs +1 -0
  68. package/lib/components/Panel/Groupbox/Groupbox.cjs.map +1 -1
  69. package/lib/components/Panel/Groupbox/Groupbox.js +1 -0
  70. package/lib/components/Panel/Groupbox/Groupbox.js.map +1 -1
  71. package/lib/components/Scroller/Scroller.cjs +3 -1
  72. package/lib/components/Scroller/Scroller.cjs.map +1 -1
  73. package/lib/components/Scroller/Scroller.js +3 -1
  74. package/lib/components/Scroller/Scroller.js.map +1 -1
  75. package/lib/components/Select/Select.cjs +12 -14
  76. package/lib/components/Select/Select.cjs.map +1 -1
  77. package/lib/components/Select/Select.js +12 -14
  78. package/lib/components/Select/Select.js.map +1 -1
  79. package/lib/components/Table/TableReel.cjs +67 -3
  80. package/lib/components/Table/TableReel.cjs.map +1 -1
  81. package/lib/components/Table/TableReel.js +68 -4
  82. package/lib/components/Table/TableReel.js.map +1 -1
  83. package/lib/components/Table/TableScroller.cjs +13 -15
  84. package/lib/components/Table/TableScroller.cjs.map +1 -1
  85. package/lib/components/Table/TableScroller.js +14 -16
  86. package/lib/components/Table/TableScroller.js.map +1 -1
  87. package/lib/components/Textarea/Textarea.cjs +18 -12
  88. package/lib/components/Textarea/Textarea.cjs.map +1 -1
  89. package/lib/components/Textarea/Textarea.js +19 -13
  90. package/lib/components/Textarea/Textarea.js.map +1 -1
  91. package/lib/components/Tooltip/TooltipPortal.cjs +1 -0
  92. package/lib/components/Tooltip/TooltipPortal.cjs.map +1 -1
  93. package/lib/components/Tooltip/TooltipPortal.js +1 -0
  94. package/lib/components/Tooltip/TooltipPortal.js.map +1 -1
  95. package/lib/hooks/useClick.cjs +7 -4
  96. package/lib/hooks/useClick.cjs.map +1 -1
  97. package/lib/hooks/useClick.js +7 -4
  98. package/lib/hooks/useClick.js.map +1 -1
  99. package/lib/hooks/useOuterClick.cjs +5 -6
  100. package/lib/hooks/useOuterClick.cjs.map +1 -1
  101. package/lib/hooks/useOuterClick.js +6 -7
  102. package/lib/hooks/useOuterClick.js.map +1 -1
  103. package/lib/hooks/usePortal.cjs +24 -17
  104. package/lib/hooks/usePortal.cjs.map +1 -1
  105. package/lib/hooks/usePortal.d.ts +1 -1
  106. package/lib/hooks/usePortal.js +25 -18
  107. package/lib/hooks/usePortal.js.map +1 -1
  108. package/lib/hooks/useResponseStatus.cjs +20 -23
  109. package/lib/hooks/useResponseStatus.cjs.map +1 -1
  110. package/lib/hooks/useResponseStatus.js +20 -23
  111. package/lib/hooks/useResponseStatus.js.map +1 -1
  112. package/lib/intl/locales/en_us.json.cjs +1 -0
  113. package/lib/intl/locales/en_us.json.cjs.map +1 -1
  114. package/lib/intl/locales/en_us.json.js +1 -0
  115. package/lib/intl/locales/en_us.json.js.map +1 -1
  116. package/lib/intl/locales/id_id.json.cjs +0 -8
  117. package/lib/intl/locales/id_id.json.cjs.map +1 -1
  118. package/lib/intl/locales/id_id.json.js +0 -8
  119. package/lib/intl/locales/id_id.json.js.map +1 -1
  120. package/lib/intl/locales/index.d.ts +6 -16
  121. package/lib/intl/locales/ja_easy.json.cjs +1 -0
  122. package/lib/intl/locales/ja_easy.json.cjs.map +1 -1
  123. package/lib/intl/locales/ja_easy.json.js +1 -0
  124. package/lib/intl/locales/ja_easy.json.js.map +1 -1
  125. package/lib/intl/locales/ko_kr.json.cjs +1 -0
  126. package/lib/intl/locales/ko_kr.json.cjs.map +1 -1
  127. package/lib/intl/locales/ko_kr.json.js +1 -0
  128. package/lib/intl/locales/ko_kr.json.js.map +1 -1
  129. package/lib/intl/locales/pt_br.json.cjs +0 -8
  130. package/lib/intl/locales/pt_br.json.cjs.map +1 -1
  131. package/lib/intl/locales/pt_br.json.js +0 -8
  132. package/lib/intl/locales/pt_br.json.js.map +1 -1
  133. package/lib/intl/locales/vi_vn.json.cjs +1 -0
  134. package/lib/intl/locales/vi_vn.json.cjs.map +1 -1
  135. package/lib/intl/locales/vi_vn.json.js +1 -0
  136. package/lib/intl/locales/vi_vn.json.js.map +1 -1
  137. package/lib/intl/locales/zh_hans_cn.json.cjs +1 -0
  138. package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -1
  139. package/lib/intl/locales/zh_hans_cn.json.js +1 -0
  140. package/lib/intl/locales/zh_hans_cn.json.js.map +1 -1
  141. package/lib/intl/locales/zh_hant_tw.json.cjs +1 -0
  142. package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -1
  143. package/lib/intl/locales/zh_hant_tw.json.js +1 -0
  144. package/lib/intl/locales/zh_hant_tw.json.js.map +1 -1
  145. package/lib/intl/useDateFormat.cjs +1 -2
  146. package/lib/intl/useDateFormat.cjs.map +1 -1
  147. package/lib/intl/useDateFormat.js +1 -2
  148. package/lib/intl/useDateFormat.js.map +1 -1
  149. package/lib/intl/useIntl.cjs +2 -4
  150. package/lib/intl/useIntl.cjs.map +1 -1
  151. package/lib/intl/useIntl.js +2 -4
  152. package/lib/intl/useIntl.js.map +1 -1
  153. package/lib/libs/debounce.cjs +10 -2
  154. package/lib/libs/debounce.cjs.map +1 -1
  155. package/lib/libs/debounce.d.ts +4 -2
  156. package/lib/libs/debounce.js +10 -2
  157. package/lib/libs/debounce.js.map +1 -1
  158. package/lib/libs/lodash.cjs +3 -4
  159. package/lib/libs/lodash.cjs.map +1 -1
  160. package/lib/libs/lodash.d.ts +2 -11
  161. package/lib/libs/lodash.js +2 -7
  162. package/lib/libs/lodash.js.map +1 -1
  163. package/lib/libs/map.cjs +1 -5
  164. package/lib/libs/map.cjs.map +1 -1
  165. package/lib/libs/map.js +1 -5
  166. package/lib/libs/map.js.map +1 -1
  167. package/lib/themes/createBackgroundColor.cjs +3 -2
  168. package/lib/themes/createBackgroundColor.cjs.map +1 -1
  169. package/lib/themes/createBackgroundColor.js +2 -1
  170. package/lib/themes/createBackgroundColor.js.map +1 -1
  171. package/lib/themes/createBorder.cjs +3 -2
  172. package/lib/themes/createBorder.cjs.map +1 -1
  173. package/lib/themes/createBorder.js +2 -1
  174. package/lib/themes/createBorder.js.map +1 -1
  175. package/lib/themes/createBreakpoint.cjs +3 -2
  176. package/lib/themes/createBreakpoint.cjs.map +1 -1
  177. package/lib/themes/createBreakpoint.js +2 -1
  178. package/lib/themes/createBreakpoint.js.map +1 -1
  179. package/lib/themes/createColor.cjs +3 -2
  180. package/lib/themes/createColor.cjs.map +1 -1
  181. package/lib/themes/createColor.js +2 -1
  182. package/lib/themes/createColor.js.map +1 -1
  183. package/lib/themes/createFontSize.cjs +3 -2
  184. package/lib/themes/createFontSize.cjs.map +1 -1
  185. package/lib/themes/createFontSize.js +2 -1
  186. package/lib/themes/createFontSize.js.map +1 -1
  187. package/lib/themes/createInteraction.cjs +3 -2
  188. package/lib/themes/createInteraction.cjs.map +1 -1
  189. package/lib/themes/createInteraction.js +2 -1
  190. package/lib/themes/createInteraction.js.map +1 -1
  191. package/lib/themes/createLeading.cjs +3 -2
  192. package/lib/themes/createLeading.cjs.map +1 -1
  193. package/lib/themes/createLeading.js +2 -1
  194. package/lib/themes/createLeading.js.map +1 -1
  195. package/lib/themes/createRadius.cjs +3 -2
  196. package/lib/themes/createRadius.cjs.map +1 -1
  197. package/lib/themes/createRadius.js +2 -1
  198. package/lib/themes/createRadius.js.map +1 -1
  199. package/lib/themes/createShadow/createShadow.cjs +3 -2
  200. package/lib/themes/createShadow/createShadow.cjs.map +1 -1
  201. package/lib/themes/createShadow/createShadow.js +2 -1
  202. package/lib/themes/createShadow/createShadow.js.map +1 -1
  203. package/lib/themes/createTextColor.cjs +3 -2
  204. package/lib/themes/createTextColor.cjs.map +1 -1
  205. package/lib/themes/createTextColor.js +2 -1
  206. package/lib/themes/createTextColor.js.map +1 -1
  207. package/lib/themes/createZIndex.cjs +3 -2
  208. package/lib/themes/createZIndex.cjs.map +1 -1
  209. package/lib/themes/createZIndex.js +2 -1
  210. package/lib/themes/createZIndex.js.map +1 -1
  211. package/metadata.json +1 -1
  212. package/package.json +1 -1
  213. package/smarthr-ui.css +27 -7
  214. package/lib/components/Table/useReelCells.cjs +0 -83
  215. package/lib/components/Table/useReelCells.cjs.map +0 -1
  216. package/lib/components/Table/useReelCells.d.ts +0 -3
  217. package/lib/components/Table/useReelCells.js +0 -81
  218. package/lib/components/Table/useReelCells.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"SingleCombobox.js","sources":["../../../../src/components/Combobox/SingleCombobox/SingleCombobox.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useClick } from '../../../hooks/useClick'\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { Localizer } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { UnstyledButton } from '../../Button'\nimport { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'\nimport { Input } from '../../Input'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useSingleOptions } from '../useOptions'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテム\n */\n selectedItem: ComboboxItem<T> | null\n /**\n * デフォルトで選択されるアイテム\n */\n defaultItem?: ComboboxItem<T>\n /**\n * コンポーネント内の先頭に表示する内容\n */\n prefix?: ReactNode\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n */\n onClear?: () => void\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n * 指定している場合、クリア時にonClickを実行せずにonClearClickのみ実行する\n */\n onClearClick?: (e: MouseEvent) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItem: ComboboxItem<T> | null) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_UP_DOWN_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst classNameGenerator = tv({\n slots: {\n wrapper:\n 'smarthr-ui-SingleCombobox shr-inline-block [&:has(.smarthr-ui-Input-input:disabled)]:shr-cursor-not-allowed',\n input: 'smarthr-ui-SingleCombobox-input shr-w-full',\n caretDownLayout: [\n 'shr-relative -shr-me-0.5 shr-p-0.5',\n 'before:shr-border-0',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n caretDownIcon: 'shr-block',\n clearButton: [\n 'smarthr-ui-SingleCombobox-clearButton',\n 'shr-group/clearButton',\n 'shr-me-0.5',\n 'focus-visible:shr-shadow-none',\n '[[data-clear-button-hidden=true]_&]:shr-hidden',\n ],\n clearButtonIcon: [\n 'shr-block',\n 'group-focus-visible/clearButton:shr-focus-indicator group-focus-visible/clearButton:shr-rounded-full',\n ],\n },\n})\n\ntype SuffixButtonsProps = {\n clearButtonRef: RefObject<HTMLButtonElement>\n caretIconColor: string\n handleClickClear: (e: MouseEvent) => void\n handleClickIcon: (e: MouseEvent) => void\n classNames: {\n clearButton: string\n clearButtonIcon: string\n caretDownLayout: string\n caretDownIcon: string\n }\n}\n\nconst SuffixButtons = memo<SuffixButtonsProps>(\n ({\n clearButtonRef,\n caretIconColor,\n handleClickClear,\n handleClickIcon: handleDelegateClickIcon,\n classNames,\n }) => (\n <>\n <UnstyledButton\n onClick={handleClickClear}\n ref={clearButtonRef}\n className={classNames.clearButton}\n >\n <FaCircleXmarkIcon\n color=\"TEXT_BLACK\"\n alt={\n <Localizer id=\"smarthr-ui/SingleCombobox/destroyButtonIconAlt\" defaultText=\"クリア\" />\n }\n className={classNames.clearButtonIcon}\n />\n </UnstyledButton>\n <span\n role=\"presentation\"\n onClick={handleDelegateClickIcon}\n className={classNames.caretDownLayout}\n >\n <FaCaretDownIcon color={caretIconColor} className={classNames.caretDownIcon} />\n </span>\n </>\n ),\n)\n\nconst ActualSingleCombobox = <T,>(\n {\n items,\n selectedItem,\n defaultItem,\n name,\n disabled,\n readOnly,\n required,\n prefix,\n error,\n creatable,\n placeholder,\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n width,\n dropdownWidth = 'auto',\n className,\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const theme = useTheme()\n const triggerRef = useRef<HTMLDivElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const clearButtonRef = useRef<HTMLButtonElement>(null)\n const [isFocused, setIsFocused] = useState(false)\n const [isExpanded, setIsExpanded] = useState(false)\n const [inputValue, setInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n const [isEditing, setIsEditing] = useState(false)\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n const { options } = useSingleOptions({\n items,\n selected: selectedItem,\n creatable,\n inputValue,\n isFilteringDisabled: !isEditing,\n })\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox<T>(\n {\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n // HINT: memo化していないが、内部でuseLatestでstableにしているため最適化としてそのまま渡している\n onSelect: (selected: ComboboxItem<T>) => {\n onSelect?.(selected)\n onChangeSelected?.(selected)\n\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n setIsExpanded(false)\n // HINT:\n // - 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n // - 対応するdropdownを閉じて以降にonChangeInputを発火する必要がある\n // - 先にclearしてしまうと意図せずこの要素のドロップダウンを閉じる前に他要素の再レンダリングを引き起こす可能性がある\n // - 例えばFilterDropdownなどで当comboboxを使っている場合、レイアウト上comboboxのdropdown以下の要素がクリックされた扱いになってしまい\n // FilterDropdownを意図せず閉じてしまうなどの挙動のバグを引き起こす可能性がある\n onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n })\n\n setIsEditing(false)\n },\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n },\n )\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n handleKeyDownListBox,\n defaultItem,\n selectedItem,\n isFocused,\n isExpanded,\n isComposing,\n isEditing,\n disabled,\n readOnly,\n })\n\n const functions = useMemo(() => {\n const selectDefaultItem = () => {\n if (latest.onSelect && latest.defaultItem) {\n latest.onSelect(latest.defaultItem)\n }\n }\n const unfocus = () => {\n if (!latest.isFocused) return\n\n latest.onBlur?.()\n\n setIsFocused(false)\n setIsExpanded(false)\n setIsEditing(false)\n\n if (latest.selectedItem) {\n setInputValue(innerText(latest.selectedItem.label))\n } else {\n selectDefaultItem()\n }\n }\n\n return {\n selectDefaultItem,\n unfocus,\n handleFocus: () => {\n latest.onFocus?.()\n inputRef.current?.focus()\n setIsFocused(true)\n\n if (!latest.isFocused) {\n setIsExpanded(true)\n }\n },\n handleClickClear: (e: MouseEvent) => {\n e.stopPropagation()\n\n let isExecutedPreventDefault = false\n\n latest.onClearClick?.({\n ...e,\n preventDefault: () => {\n e.preventDefault()\n isExecutedPreventDefault = true\n },\n })\n\n if (!isExecutedPreventDefault) {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n\n inputRef.current?.focus()\n\n setIsFocused(true)\n setIsExpanded(true)\n }\n },\n handleClickInput: (e: MouseEvent) => {\n if (latest.disabled || latest.readOnly) {\n e.stopPropagation()\n\n return\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n latest.onChange?.(e)\n latest.onChangeInput?.(e)\n\n if (!latest.isEditing) setIsEditing(true)\n\n const { value } = e.currentTarget\n\n setInputValue(value)\n\n if (value === '') {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') e.preventDefault()\n\n latest.onKeyPress?.(e)\n },\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (latest.isComposing) {\n return\n }\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n if (latest.isExpanded) {\n e.stopPropagation()\n setIsExpanded(false)\n }\n } else if (e.key === 'Tab') {\n unfocus()\n } else {\n if (ARROW_UP_DOWN_REGEX.test(e.key)) {\n e.preventDefault()\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n }\n\n latest.handleKeyDownListBox(e)\n },\n }\n }, [latest])\n\n const caretIconColor = isFocused\n ? theme.textColor.black\n : disabled || readOnly\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n useClick(\n useMemo(() => [triggerRef, listBoxRef, clearButtonRef], [listBoxRef]),\n isFocused || selectedItem ? NOOP : functions.selectDefaultItem,\n functions.unfocus,\n )\n\n // selectedItem.label はプリミティブ値でないデータ型の可能性があり、そのまま useEffect の依存配列に入れると意図せぬエフェクトの実行を引き起こしてしまう可能性があるので、プリミティブ値である string 型に変換したものを依存配列に入れています。\n const selectedItemLabelText = innerText(selectedItem?.label)\n useEffect(() => {\n setInputValue(selectedItemLabelText)\n }, [selectedItemLabelText])\n\n const classNames = useMemo(() => {\n const { wrapper, input, caretDownLayout, caretDownIcon, clearButton, clearButtonIcon } =\n classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n input: input(),\n caretDownLayout: caretDownLayout(),\n caretDownIcon: caretDownIcon(),\n clearButton: clearButton(),\n clearButtonIcon: clearButtonIcon(),\n }\n }, [className])\n\n return (\n <div\n ref={triggerRef}\n role=\"group\"\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n data-clear-button-hidden={(selectedItem === null || disabled || readOnly || false).toString()}\n >\n <Input\n {...rest}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n name={name}\n value={inputValue}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n autoComplete={autoComplete ?? 'off'}\n aria-haspopup=\"listbox\"\n aria-controls={listBoxId}\n aria-expanded={isFocused}\n aria-activedescendant={activeOption?.id}\n aria-autocomplete=\"list\"\n /* eslint-disable-next-line smarthr/a11y-prohibit-input-placeholder */\n placeholder={placeholder}\n onClick={functions.handleClickInput}\n onChange={functions.handleChangeInput}\n onFocus={isFocused ? undefined : functions.handleFocus}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n onKeyPress={functions.handleKeyPress}\n error={error}\n prefix={prefix}\n suffix={\n <SuffixButtons\n clearButtonRef={clearButtonRef}\n caretIconColor={caretIconColor}\n handleClickClear={functions.handleClickClear}\n handleClickIcon={functions.handleClickInput}\n classNames={classNames}\n />\n }\n className={classNames.input}\n data-smarthr-ui-input=\"true\"\n />\n {!readOnly && <ListBox {...listBoxProps} />}\n </div>\n )\n}\n\nexport const SingleCombobox = genericsForwardRef(ActualSingleCombobox)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA;AAEA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;AAEA;AACA;;;;AAIC;AACD;AACA;;;;;;AAMC;AACD;;;AAGC;AACF;AACF;AAeD;AAiCA;AAmCE;AACA;AACA;AACA;;;;;;;AASA;;AAEE;;;;AAID;AAED;;;;;;AAOI;AACE;AACA;;;;;;;;;;;AAYE;AACF;;;;;;;AAQH;;;;;;;;;;;;;;;;;;;;AAsBF;AAED;;;AAGM;;AAEJ;;;;AAIE;;;;AAMA;;;;AAGE;;AAEJ;;;;;AAMI;AACA;;AAGA;;;;AAIF;;;;AAMI;;;;;AAKD;;AAGC;AACA;AAEA;;;;;AAMJ;;;;;AAOE;AAEA;;;;AAIF;AACE;AACA;;;AAIA;;AAIA;AACE;AACA;;;AAGJ;AACA;;;;AAIA;AACE;;AAEA;;AAEF;AACE;;;;AAKE;;;;;AAIK;AACL;;;;;;AAMA;AAEA;;;;AAKF;;;AAGN;;AAGE;;AAEE;AACA;AAEJ;;;;;AAUA;AAEA;AACE;;AAIE;;;;;;;AAOJ;AAEA;AAMM;AACA;AACD;;;AA6CP;;;"}
1
+ {"version":3,"file":"SingleCombobox.js","sources":["../../../../src/components/Combobox/SingleCombobox/SingleCombobox.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n memo,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useClick } from '../../../hooks/useClick'\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { Localizer } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { UnstyledButton } from '../../Button'\nimport { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'\nimport { Input } from '../../Input'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useSingleOptions } from '../useOptions'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテム\n */\n selectedItem: ComboboxItem<T> | null\n /**\n * デフォルトで選択されるアイテム\n */\n defaultItem?: ComboboxItem<T>\n /**\n * コンポーネント内の先頭に表示する内容\n */\n prefix?: ReactNode\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n */\n onClear?: () => void\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n * 指定している場合、クリア時にonClickを実行せずにonClearClickのみ実行する\n */\n onClearClick?: (e: MouseEvent) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItem: ComboboxItem<T> | null) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_UP_DOWN_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst classNameGenerator = tv({\n slots: {\n wrapper:\n 'smarthr-ui-SingleCombobox shr-inline-block [&:has(.smarthr-ui-Input-input:disabled)]:shr-cursor-not-allowed',\n input: 'smarthr-ui-SingleCombobox-input shr-w-full',\n caretDownLayout: [\n 'shr-relative -shr-me-0.5 shr-p-0.5',\n 'before:shr-border-0',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n caretDownIcon: 'shr-block',\n clearButton: [\n 'smarthr-ui-SingleCombobox-clearButton',\n 'shr-group/clearButton',\n 'shr-me-0.5',\n 'focus-visible:shr-shadow-none',\n '[[data-clear-button-hidden=true]_&]:shr-hidden',\n ],\n clearButtonIcon: [\n 'shr-block',\n 'group-focus-visible/clearButton:shr-focus-indicator group-focus-visible/clearButton:shr-rounded-full',\n ],\n },\n})\n\ntype SuffixButtonsProps = {\n clearButtonRef: RefObject<HTMLButtonElement>\n caretIconColor: string\n handleClickClear: (e: MouseEvent) => void\n handleClickIcon: (e: MouseEvent) => void\n classNames: {\n clearButton: string\n clearButtonIcon: string\n caretDownLayout: string\n caretDownIcon: string\n }\n}\n\nconst SuffixButtons = memo<SuffixButtonsProps>(\n ({\n clearButtonRef,\n caretIconColor,\n handleClickClear,\n handleClickIcon: handleDelegateClickIcon,\n classNames,\n }) => (\n <>\n <UnstyledButton\n onClick={handleClickClear}\n ref={clearButtonRef}\n className={classNames.clearButton}\n >\n <FaCircleXmarkIcon\n color=\"TEXT_BLACK\"\n alt={\n <Localizer id=\"smarthr-ui/SingleCombobox/destroyButtonIconAlt\" defaultText=\"クリア\" />\n }\n className={classNames.clearButtonIcon}\n />\n </UnstyledButton>\n <span\n role=\"presentation\"\n onClick={handleDelegateClickIcon}\n className={classNames.caretDownLayout}\n >\n <FaCaretDownIcon color={caretIconColor} className={classNames.caretDownIcon} />\n </span>\n </>\n ),\n)\n\nconst ActualSingleCombobox = <T,>(\n {\n items,\n selectedItem,\n defaultItem,\n name,\n disabled,\n readOnly,\n required,\n prefix,\n error,\n creatable,\n placeholder,\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n width,\n dropdownWidth = 'auto',\n className,\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const theme = useTheme()\n const triggerRef = useRef<HTMLDivElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const clearButtonRef = useRef<HTMLButtonElement>(null)\n const [isFocused, setIsFocused] = useState(false)\n const [isExpanded, setIsExpanded] = useState(false)\n const [inputValue, setInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n const [isEditing, setIsEditing] = useState(false)\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n ref,\n () => inputRef.current,\n [],\n )\n\n const { options } = useSingleOptions({\n items,\n selected: selectedItem,\n creatable,\n inputValue,\n isFilteringDisabled: !isEditing,\n })\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox<T>(\n {\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n // HINT: memo化していないが、内部でuseLatestでstableにしているため最適化としてそのまま渡している\n onSelect: (selected: ComboboxItem<T>) => {\n onSelect?.(selected)\n onChangeSelected?.(selected)\n\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n setIsExpanded(false)\n // HINT:\n // - 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n // - 対応するdropdownを閉じて以降にonChangeInputを発火する必要がある\n // - 先にclearしてしまうと意図せずこの要素のドロップダウンを閉じる前に他要素の再レンダリングを引き起こす可能性がある\n // - 例えばFilterDropdownなどで当comboboxを使っている場合、レイアウト上comboboxのdropdown以下の要素がクリックされた扱いになってしまい\n // FilterDropdownを意図せず閉じてしまうなどの挙動のバグを引き起こす可能性がある\n onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n })\n\n setIsEditing(false)\n },\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n },\n )\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n handleKeyDownListBox,\n defaultItem,\n selectedItem,\n isFocused,\n isExpanded,\n isComposing,\n isEditing,\n disabled,\n readOnly,\n })\n\n const functions = useMemo(() => {\n const selectDefaultItem = () => {\n if (latest.onSelect && latest.defaultItem) {\n latest.onSelect(latest.defaultItem)\n }\n }\n const unfocus = () => {\n if (!latest.isFocused) return\n\n latest.onBlur?.()\n\n setIsFocused(false)\n setIsExpanded(false)\n setIsEditing(false)\n\n if (latest.selectedItem) {\n setInputValue(innerText(latest.selectedItem.label))\n } else {\n selectDefaultItem()\n }\n }\n\n return {\n selectDefaultItem,\n unfocus,\n handleFocus: () => {\n latest.onFocus?.()\n inputRef.current?.focus()\n setIsFocused(true)\n\n if (!latest.isFocused) {\n setIsExpanded(true)\n }\n },\n handleClickClear: (e: MouseEvent) => {\n e.stopPropagation()\n\n let isExecutedPreventDefault = false\n\n latest.onClearClick?.({\n ...e,\n preventDefault: () => {\n e.preventDefault()\n isExecutedPreventDefault = true\n },\n })\n\n if (!isExecutedPreventDefault) {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n\n inputRef.current?.focus()\n\n setIsFocused(true)\n setIsExpanded(true)\n }\n },\n handleClickInput: (e: MouseEvent) => {\n if (latest.disabled || latest.readOnly) {\n e.stopPropagation()\n\n return\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n latest.onChange?.(e)\n latest.onChangeInput?.(e)\n\n if (!latest.isEditing) setIsEditing(true)\n\n const { value } = e.currentTarget\n\n setInputValue(value)\n\n if (value === '') {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') e.preventDefault()\n\n latest.onKeyPress?.(e)\n },\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (latest.isComposing) {\n return\n }\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n if (latest.isExpanded) {\n e.stopPropagation()\n setIsExpanded(false)\n }\n } else if (e.key === 'Tab') {\n unfocus()\n } else {\n if (ARROW_UP_DOWN_REGEX.test(e.key)) {\n e.preventDefault()\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n }\n\n latest.handleKeyDownListBox(e)\n },\n }\n }, [latest])\n\n const caretIconColor = isFocused\n ? theme.textColor.black\n : disabled || readOnly\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n useClick(\n [triggerRef, listBoxRef, clearButtonRef],\n isFocused || selectedItem ? NOOP : functions.selectDefaultItem,\n functions.unfocus,\n )\n\n // selectedItem.label はプリミティブ値でないデータ型の可能性があり、そのまま useEffect の依存配列に入れると意図せぬエフェクトの実行を引き起こしてしまう可能性があるので、プリミティブ値である string 型に変換したものを依存配列に入れています。\n const selectedItemLabelText = innerText(selectedItem?.label)\n useEffect(() => {\n setInputValue(selectedItemLabelText)\n }, [selectedItemLabelText])\n\n const classNames = useMemo(() => {\n const { wrapper, input, caretDownLayout, caretDownIcon, clearButton, clearButtonIcon } =\n classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n input: input(),\n caretDownLayout: caretDownLayout(),\n caretDownIcon: caretDownIcon(),\n clearButton: clearButton(),\n clearButtonIcon: clearButtonIcon(),\n }\n }, [className])\n\n return (\n <div\n ref={triggerRef}\n role=\"group\"\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n data-clear-button-hidden={(selectedItem === null || disabled || readOnly || false).toString()}\n >\n <Input\n {...rest}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n name={name}\n value={inputValue}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n autoComplete={autoComplete ?? 'off'}\n aria-haspopup=\"listbox\"\n aria-controls={listBoxId}\n aria-expanded={isFocused}\n aria-activedescendant={activeOption?.id}\n aria-autocomplete=\"list\"\n /* eslint-disable-next-line smarthr/a11y-prohibit-input-placeholder */\n placeholder={placeholder}\n onClick={functions.handleClickInput}\n onChange={functions.handleChangeInput}\n onFocus={isFocused ? undefined : functions.handleFocus}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n onKeyPress={functions.handleKeyPress}\n error={error}\n prefix={prefix}\n suffix={\n <SuffixButtons\n clearButtonRef={clearButtonRef}\n caretIconColor={caretIconColor}\n handleClickClear={functions.handleClickClear}\n handleClickIcon={functions.handleClickInput}\n classNames={classNames}\n />\n }\n className={classNames.input}\n data-smarthr-ui-input=\"true\"\n />\n {!readOnly && <ListBox {...listBoxProps} />}\n </div>\n )\n}\n\nexport const SingleCombobox = genericsForwardRef(ActualSingleCombobox)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0EA;AAEA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;AAEA;AACA;;;;AAIC;AACD;AACA;;;;;;AAMC;AACD;;;AAGC;AACF;AACF;AAeD;AAiCA;AAmCE;AACA;AACA;AACA;;;;;;AAOA;AAMA;;AAEE;;;;AAID;AAED;;;;;;AAOI;AACE;AACA;;;;;;;;;;;AAYE;AACF;;;;;;;AAQH;;;;;;;;;;;;;;;;;;;;AAsBF;AAED;;;AAGM;;AAEJ;;;;AAIE;;;;AAMA;;;;AAGE;;AAEJ;;;;;AAMI;AACA;;AAGA;;;;AAIF;;;;AAMI;;;;;AAKD;;AAGC;AACA;AAEA;;;;;AAMJ;;;;;AAOE;AAEA;;;;AAIF;AACE;AACA;;;AAIA;;AAIA;AACE;AACA;;;AAGJ;AACA;;;;AAIA;AACE;;AAEA;;AAEF;AACE;;;;AAKE;;;;;AAIK;AACL;;;;;;AAMA;AAEA;;;;AAKF;;;AAGN;;AAGE;;AAEE;AACA;;;;;;AAYJ;AAEA;AACE;;AAIE;;;;;;;AAOJ;AAEA;AAMM;AACA;AACD;;;AA6CP;;;"}
@@ -161,7 +161,7 @@ const DatePicker = React.forwardRef(({ value, name, from = DEFAULT_FROM, to, dis
161
161
  },
162
162
  };
163
163
  }, [latest]);
164
- React.useImperativeHandle(ref, () => inputRef.current);
164
+ React.useImperativeHandle(ref, () => inputRef.current, []);
165
165
  React.useEffect(() => {
166
166
  if (value === undefined || !inputRef.current) {
167
167
  return;
@@ -183,7 +183,7 @@ const DatePicker = React.forwardRef(({ value, name, from = DEFAULT_FROM, to, dis
183
183
  }
184
184
  inputRef.current.value = value || '';
185
185
  }, [value, isInputFocused, functions]);
186
- hooks_useOuterClick.useOuterClick(React.useMemo(() => [inputWrapperRef, calendarPortalRef], [inputWrapperRef, calendarPortalRef]), functions.closeCalendar);
186
+ hooks_useOuterClick.useOuterClick([inputWrapperRef, calendarPortalRef], functions.closeCalendar);
187
187
  React.useEffect(() => {
188
188
  const handleKeyDown = (e) => {
189
189
  if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {
@@ -1 +1 @@
1
- {"version":3,"file":"DatePicker.cjs","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport dayjs from 'dayjs'\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type MouseEvent,\n type ReactNode,\n forwardRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useOuterClick } from '../../hooks/useOuterClick'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Calendar } from '../Calendar'\nimport { FaCalendarDaysIcon } from '../Icon'\nimport { Input } from '../Input'\n\nimport { Portal } from './Portal'\nimport { parseJpnDateString } from './datePickerHelper'\n\ntype ChangeLikeEvent = ChangeEvent | React.KeyboardEvent | MouseEvent\ntype BaseProps = {\n /** input 要素の `value` 属性の値 */\n value?: string | null\n /** input 要素の `name` 属性の値 */\n name?: string\n /** 選択可能な期間の開始日 */\n from?: Date\n /** 選択可能な期間の終了日 */\n to?: Date\n /** フォームを無効にするかどうか */\n disabled?: boolean\n /**\n * placeholder属性は非推奨です。別途ヒント用要素を設置するか、それらの領域を確保出来ない場合はTooltipコンポーネントの利用を検討してください。\n */\n placeholder?: string\n /** フォームにエラーがあるかどうか */\n error?: boolean\n /** コンポーネントの幅 */\n width?: number | string\n /** 入力を独自にパースする場合に、パース処理を記述する関数 */\n parseInput?: (input: string) => Date | null\n /** 表示する日付を独自にフォーマットする場合に、フォーマット処理を記述する関数 */\n formatDate?: (date: Date | null) => string\n /** 入出力用文字列と併記する別フォーマット処理を記述する関数 */\n showAlternative?: (date: Date | null) => ReactNode\n /** @deprecated onChangeDate は非推奨です。onChange を使ってください。 */\n onChangeDate?: (date: Date | null, value: string, other: { errors: string[] }) => void\n /** 選択された日付が変わった時に発火するコールバック関数 */\n onChange?: (\n e: ChangeEvent<HTMLInputElement>,\n other: { date: Date | null; formatValue: string; errors: string[] },\n ) => void\n}\ntype Props = BaseProps &\n Omit<\n ComponentPropsWithRef<'input'>,\n | keyof BaseProps\n | 'type'\n | 'onChange'\n | 'onKeyPress'\n | 'onFocus'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-haspopup'\n >\n\nexport const DEFAULT_FROM = new Date(1900, 0, 1)\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-DatePicker shr-inline-block',\n inputSuffixLayout: 'shr-box-border shr-h-full shr-py-0.5',\n inputSuffixWrapper:\n 'shr-border-l-shorthand shr-box-border shr-flex shr-h-full shr-items-center shr-justify-center shr-ps-0.5 shr-text-base',\n inputSuffixText: 'shr-text-gray shr-me-0.5 shr-text-sm',\n },\n})\n\nconst DEFAULT_DATE_TO_STRING_FORMAT = 'YYYY/MM/DD'\nconst DEFAULT_DATE_TO_STRING = (d: Date | null) =>\n d ? dayjs(d).format(DEFAULT_DATE_TO_STRING_FORMAT) : ''\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\n\nconst parseStringDate = (\n str: string | null | undefined,\n parseInputFn: ((input: string) => Date | null) | undefined,\n) => {\n if (!str) return null\n return parseInputFn ? parseInputFn(str) : parseJpnDateString(str)\n}\n\n/** @deprecated DatePicker は非推奨です。Input[type=date] を使ってください。 */\nexport const DatePicker = forwardRef<HTMLInputElement, Props>(\n (\n {\n value,\n name,\n from = DEFAULT_FROM,\n to,\n disabled,\n width,\n error,\n className,\n parseInput,\n formatDate,\n showAlternative,\n onChangeDate,\n onChange,\n onBlur,\n ...rest\n },\n ref,\n ) => {\n const theme = useTheme()\n const classNames = useMemo(() => {\n const { container, inputSuffixLayout, inputSuffixWrapper, inputSuffixText } =\n classNameGenerator()\n\n return {\n container: container({ className }),\n inputSuffixLayout: inputSuffixLayout(),\n inputSuffixWrapper: inputSuffixWrapper(),\n inputSuffixText: inputSuffixText(),\n }\n }, [className])\n\n const [isInputFocused, setIsInputFocused] = useState(false)\n const inputRef = useRef<HTMLInputElement>(null)\n const inputWrapperRef = useRef<HTMLDivElement>(null)\n const calendarPortalRef = useRef<HTMLDivElement>(null)\n const [inputRect, setInputRect] = useState<DOMRect | null>(null)\n const [isCalendarShown, setIsCalendarShown] = useState(false)\n const [alternativeFormat, setAlternativeFormat] = useState<null | ReactNode>(null)\n const calenderId = useId()\n\n const [selectedDate, setSelectedDate] = useState<Date | null>(() =>\n parseStringDate(value, parseInput),\n )\n\n const latest = useLatest({\n onChange,\n onChangeDate,\n parseInput,\n formatDate,\n showAlternative,\n onBlur,\n isInputFocused,\n selectedDate,\n })\n\n const functions = useMemo(() => {\n const dateToString = (date: Date | null) =>\n latest.formatDate ? latest.formatDate(date) : DEFAULT_DATE_TO_STRING(date)\n\n const dateToAlternativeFormat = (d: Date | null) => {\n if (!latest.showAlternative) return null\n return d ? latest.showAlternative(d) : null\n }\n\n const stringToDate = (str: string | null | undefined) =>\n parseStringDate(str, latest.parseInput)\n\n const updateDate = (e: ChangeLikeEvent, newDate: Date | null) => {\n if (\n !inputRef.current ||\n newDate === latest.selectedDate ||\n (newDate && latest.selectedDate && newDate.getTime() === latest.selectedDate.getTime())\n ) {\n // Do not update date if the new date is same with the old one.\n return\n }\n\n const isValid = !newDate || dayjs(newDate).isValid()\n const errors: string[] = []\n\n if (!isValid) {\n errors.push('INVALID_DATE')\n }\n\n const nextDate = isValid ? newDate : null\n const formatValue = dateToString(nextDate)\n\n inputRef.current.value = formatValue\n setAlternativeFormat(dateToAlternativeFormat(nextDate))\n setSelectedDate(nextDate)\n\n if (latest.onChange) {\n e.preventDefault()\n e.stopPropagation()\n\n const event = new Event('change', { bubbles: true })\n const input = inputRef.current\n\n input.dispatchEvent(event)\n latest.onChange(\n // HINT: 型問題のため別途オブジェクトをイベントに見立てる\n {\n stopPropagation: () => {\n event.stopPropagation()\n },\n preventDefault: () => {\n event.preventDefault()\n },\n target: input,\n currentTarget: input,\n } as ChangeEvent<HTMLInputElement>,\n { date: nextDate, formatValue, errors },\n )\n } else if (latest.onChangeDate) {\n latest.onChangeDate(nextDate, formatValue, { errors })\n }\n }\n\n const closeCalendar = () => setIsCalendarShown(false)\n\n const openCalendar = () => {\n if (inputWrapperRef.current) {\n setIsCalendarShown(true)\n setInputRect(inputWrapperRef.current.getBoundingClientRect())\n }\n }\n\n return {\n stringToDate,\n dateToString,\n dateToAlternativeFormat,\n closeCalendar,\n openCalendar,\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => {\n setIsInputFocused(false)\n updateDate(e, e.target.value ? stringToDate(e.target.value) : null)\n latest.onBlur?.(e)\n },\n handleDelegateKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n }\n },\n handleKeyPressInput: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n const isExpanded = e.currentTarget.getAttribute('aria-expanded') === 'true'\n ;(isExpanded ? closeCalendar : openCalendar)()\n updateDate(e, stringToDate(e.currentTarget.value))\n }\n },\n handleFocusInput: () => {\n setIsInputFocused(true)\n openCalendar()\n },\n handleSelectDateCalendar: (e: ChangeLikeEvent, selected: Date | null) => {\n updateDate(e, selected)\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n },\n }\n }, [latest])\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n ref,\n () => inputRef.current,\n )\n\n useEffect(() => {\n if (value === undefined || !inputRef.current) {\n return\n }\n\n /**\n * Do not format the given value in the following cases\n * - while input element is focused.\n * - if the given value is not date formattable.\n */\n if (!isInputFocused) {\n const newDate = functions.stringToDate(value)\n\n if (newDate && dayjs(newDate).isValid()) {\n inputRef.current.value = functions.dateToString(newDate)\n setAlternativeFormat(functions.dateToAlternativeFormat(newDate))\n setSelectedDate(newDate)\n\n return\n }\n\n setSelectedDate(null)\n }\n\n inputRef.current.value = value || ''\n }, [value, isInputFocused, functions])\n\n useOuterClick(\n useMemo(() => [inputWrapperRef, calendarPortalRef], [inputWrapperRef, calendarPortalRef]),\n functions.closeCalendar,\n )\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {\n return\n }\n\n const calendarButtons = calendarPortalRef.current.querySelectorAll('button')\n\n if (calendarButtons.length === 0) {\n return\n }\n\n const firstCalendarButton = calendarButtons[0]\n\n if (latest.isInputFocused) {\n if (e.shiftKey) {\n // move focus from Input to previous elements of DatePicker\n functions.closeCalendar()\n\n return\n }\n\n // move focus from Input to Calendar\n e.preventDefault()\n firstCalendarButton.focus()\n\n return\n }\n\n const calendarButtonAry = Array.from(calendarButtons)\n const currentFocused = calendarButtonAry.find((button) => button === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstCalendarButton) {\n // move focus from Calendar to Input\n inputRef.current.focus()\n e.preventDefault()\n }\n } else if (currentFocused === calendarButtonAry.at(-1)) {\n // move focus from Calendar to next elements of DatePicker\n inputRef.current.focus()\n functions.closeCalendar()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n }\n }, [functions, latest])\n\n const caretIconColor =\n isInputFocused || isCalendarShown\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n <div\n onClick={!isCalendarShown && !disabled ? functions.openCalendar : undefined}\n onKeyDown={isCalendarShown ? functions.handleDelegateKeyDown : undefined}\n role=\"presentation\"\n className={classNames.container}\n style={{\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <div ref={inputWrapperRef}>\n <Input\n {...rest}\n data-smarthr-ui-input=\"true\"\n width=\"100%\"\n name={name}\n onChange={isCalendarShown ? functions.closeCalendar : undefined}\n onKeyPress={functions.handleKeyPressInput}\n onFocus={functions.handleFocusInput}\n onBlur={functions.handleBlur}\n suffix={\n <InputSuffixIcon\n alternativeFormat={showAlternative ? alternativeFormat : null}\n caretIconColor={caretIconColor}\n classNames={classNames}\n />\n }\n disabled={disabled}\n error={error}\n ref={inputRef}\n className=\"smarthr-ui-DatePicker-inputContainer\"\n aria-expanded={isCalendarShown}\n aria-controls={calenderId}\n aria-haspopup={true}\n />\n </div>\n {isCalendarShown && inputRect && (\n <Portal inputRect={inputRect} ref={calendarPortalRef}>\n <Calendar\n id={calenderId}\n value={selectedDate || undefined}\n from={from}\n to={to}\n onSelectDate={functions.handleSelectDateCalendar}\n />\n </Portal>\n )}\n </div>\n )\n },\n)\n\nconst InputSuffixIcon = memo<{\n classNames: { inputSuffixLayout: string; inputSuffixWrapper: string; inputSuffixText: string }\n alternativeFormat: null | ReactNode\n caretIconColor: ComponentProps<typeof FaCalendarDaysIcon>['color']\n}>(({ classNames, alternativeFormat, caretIconColor }) => (\n <span className={classNames.inputSuffixLayout}>\n <span className={classNames.inputSuffixWrapper}>\n {alternativeFormat && <span className={classNames.inputSuffixText}>{alternativeFormat}</span>}\n <FaCalendarDaysIcon color={caretIconColor} />\n </span>\n </span>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA6EO;AAEP;AACE;AACE;AACA;AACA;AAEA;AACD;AACF;AAED;AACA;AAEA;AAEA;AAIE;AAAU;AACV;AACF;AAEA;;AAsBI;AACA;AACE;;AAIE;;;;;AAKJ;;AAGA;AACA;AACA;;;;AAIA;AAEA;;;;;;;;;;AAaC;AAED;;AAIE;;AAC+B;AAC7B;AACF;AAEA;AAGA;;;AAII;;;;AAMF;;;AAIE;;;AAIF;AAEA;AACA;;AAGA;;;AAIE;AACA;AAEA;AACA;;AAEE;;;;;;;AAOE;AACA;;;AAIC;;;AAGT;;;AAKE;;;;AAIF;;;;;;;AAQE;;;AAGE;;AAEF;;;;;;AAM0B;;;AAG1B;AACE;AACE;;AAEA;;;;;AAKF;;AAEF;AACE;;;;AAIsB;;;AAG5B;;;;;;AAYE;;;;AAIG;;;;;;;;;;;;;;;AAwBH;AACE;;;;AAMA;;;AAIA;AAEA;AACE;;;;;;;;;;;AAeF;AAEA;AACE;;AAEE;;;;;;AAKF;;;AAGJ;AAEA;AAEA;AACE;AACF;AACF;AAEA;AAEI;AACA;AACE;AACA;;;AAIJ;AAMI;AACD;AAyCP;AAGF;;;"}
1
+ {"version":3,"file":"DatePicker.cjs","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport dayjs from 'dayjs'\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type MouseEvent,\n type ReactNode,\n forwardRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useOuterClick } from '../../hooks/useOuterClick'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Calendar } from '../Calendar'\nimport { FaCalendarDaysIcon } from '../Icon'\nimport { Input } from '../Input'\n\nimport { Portal } from './Portal'\nimport { parseJpnDateString } from './datePickerHelper'\n\ntype ChangeLikeEvent = ChangeEvent | React.KeyboardEvent | MouseEvent\ntype BaseProps = {\n /** input 要素の `value` 属性の値 */\n value?: string | null\n /** input 要素の `name` 属性の値 */\n name?: string\n /** 選択可能な期間の開始日 */\n from?: Date\n /** 選択可能な期間の終了日 */\n to?: Date\n /** フォームを無効にするかどうか */\n disabled?: boolean\n /**\n * placeholder属性は非推奨です。別途ヒント用要素を設置するか、それらの領域を確保出来ない場合はTooltipコンポーネントの利用を検討してください。\n */\n placeholder?: string\n /** フォームにエラーがあるかどうか */\n error?: boolean\n /** コンポーネントの幅 */\n width?: number | string\n /** 入力を独自にパースする場合に、パース処理を記述する関数 */\n parseInput?: (input: string) => Date | null\n /** 表示する日付を独自にフォーマットする場合に、フォーマット処理を記述する関数 */\n formatDate?: (date: Date | null) => string\n /** 入出力用文字列と併記する別フォーマット処理を記述する関数 */\n showAlternative?: (date: Date | null) => ReactNode\n /** @deprecated onChangeDate は非推奨です。onChange を使ってください。 */\n onChangeDate?: (date: Date | null, value: string, other: { errors: string[] }) => void\n /** 選択された日付が変わった時に発火するコールバック関数 */\n onChange?: (\n e: ChangeEvent<HTMLInputElement>,\n other: { date: Date | null; formatValue: string; errors: string[] },\n ) => void\n}\ntype Props = BaseProps &\n Omit<\n ComponentPropsWithRef<'input'>,\n | keyof BaseProps\n | 'type'\n | 'onChange'\n | 'onKeyPress'\n | 'onFocus'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-haspopup'\n >\n\nexport const DEFAULT_FROM = new Date(1900, 0, 1)\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-DatePicker shr-inline-block',\n inputSuffixLayout: 'shr-box-border shr-h-full shr-py-0.5',\n inputSuffixWrapper:\n 'shr-border-l-shorthand shr-box-border shr-flex shr-h-full shr-items-center shr-justify-center shr-ps-0.5 shr-text-base',\n inputSuffixText: 'shr-text-gray shr-me-0.5 shr-text-sm',\n },\n})\n\nconst DEFAULT_DATE_TO_STRING_FORMAT = 'YYYY/MM/DD'\nconst DEFAULT_DATE_TO_STRING = (d: Date | null) =>\n d ? dayjs(d).format(DEFAULT_DATE_TO_STRING_FORMAT) : ''\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\n\nconst parseStringDate = (\n str: string | null | undefined,\n parseInputFn: ((input: string) => Date | null) | undefined,\n) => {\n if (!str) return null\n return parseInputFn ? parseInputFn(str) : parseJpnDateString(str)\n}\n\n/** @deprecated DatePicker は非推奨です。Input[type=date] を使ってください。 */\nexport const DatePicker = forwardRef<HTMLInputElement, Props>(\n (\n {\n value,\n name,\n from = DEFAULT_FROM,\n to,\n disabled,\n width,\n error,\n className,\n parseInput,\n formatDate,\n showAlternative,\n onChangeDate,\n onChange,\n onBlur,\n ...rest\n },\n ref,\n ) => {\n const theme = useTheme()\n const classNames = useMemo(() => {\n const { container, inputSuffixLayout, inputSuffixWrapper, inputSuffixText } =\n classNameGenerator()\n\n return {\n container: container({ className }),\n inputSuffixLayout: inputSuffixLayout(),\n inputSuffixWrapper: inputSuffixWrapper(),\n inputSuffixText: inputSuffixText(),\n }\n }, [className])\n\n const [isInputFocused, setIsInputFocused] = useState(false)\n const inputRef = useRef<HTMLInputElement>(null)\n const inputWrapperRef = useRef<HTMLDivElement>(null)\n const calendarPortalRef = useRef<HTMLDivElement>(null)\n const [inputRect, setInputRect] = useState<DOMRect | null>(null)\n const [isCalendarShown, setIsCalendarShown] = useState(false)\n const [alternativeFormat, setAlternativeFormat] = useState<null | ReactNode>(null)\n const calenderId = useId()\n\n const [selectedDate, setSelectedDate] = useState<Date | null>(() =>\n parseStringDate(value, parseInput),\n )\n\n const latest = useLatest({\n onChange,\n onChangeDate,\n parseInput,\n formatDate,\n showAlternative,\n onBlur,\n isInputFocused,\n selectedDate,\n })\n\n const functions = useMemo(() => {\n const dateToString = (date: Date | null) =>\n latest.formatDate ? latest.formatDate(date) : DEFAULT_DATE_TO_STRING(date)\n\n const dateToAlternativeFormat = (d: Date | null) => {\n if (!latest.showAlternative) return null\n return d ? latest.showAlternative(d) : null\n }\n\n const stringToDate = (str: string | null | undefined) =>\n parseStringDate(str, latest.parseInput)\n\n const updateDate = (e: ChangeLikeEvent, newDate: Date | null) => {\n if (\n !inputRef.current ||\n newDate === latest.selectedDate ||\n (newDate && latest.selectedDate && newDate.getTime() === latest.selectedDate.getTime())\n ) {\n // Do not update date if the new date is same with the old one.\n return\n }\n\n const isValid = !newDate || dayjs(newDate).isValid()\n const errors: string[] = []\n\n if (!isValid) {\n errors.push('INVALID_DATE')\n }\n\n const nextDate = isValid ? newDate : null\n const formatValue = dateToString(nextDate)\n\n inputRef.current.value = formatValue\n setAlternativeFormat(dateToAlternativeFormat(nextDate))\n setSelectedDate(nextDate)\n\n if (latest.onChange) {\n e.preventDefault()\n e.stopPropagation()\n\n const event = new Event('change', { bubbles: true })\n const input = inputRef.current\n\n input.dispatchEvent(event)\n latest.onChange(\n // HINT: 型問題のため別途オブジェクトをイベントに見立てる\n {\n stopPropagation: () => {\n event.stopPropagation()\n },\n preventDefault: () => {\n event.preventDefault()\n },\n target: input,\n currentTarget: input,\n } as ChangeEvent<HTMLInputElement>,\n { date: nextDate, formatValue, errors },\n )\n } else if (latest.onChangeDate) {\n latest.onChangeDate(nextDate, formatValue, { errors })\n }\n }\n\n const closeCalendar = () => setIsCalendarShown(false)\n\n const openCalendar = () => {\n if (inputWrapperRef.current) {\n setIsCalendarShown(true)\n setInputRect(inputWrapperRef.current.getBoundingClientRect())\n }\n }\n\n return {\n stringToDate,\n dateToString,\n dateToAlternativeFormat,\n closeCalendar,\n openCalendar,\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => {\n setIsInputFocused(false)\n updateDate(e, e.target.value ? stringToDate(e.target.value) : null)\n latest.onBlur?.(e)\n },\n handleDelegateKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n }\n },\n handleKeyPressInput: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n const isExpanded = e.currentTarget.getAttribute('aria-expanded') === 'true'\n ;(isExpanded ? closeCalendar : openCalendar)()\n updateDate(e, stringToDate(e.currentTarget.value))\n }\n },\n handleFocusInput: () => {\n setIsInputFocused(true)\n openCalendar()\n },\n handleSelectDateCalendar: (e: ChangeLikeEvent, selected: Date | null) => {\n updateDate(e, selected)\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n },\n }\n }, [latest])\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n ref,\n () => inputRef.current,\n [],\n )\n\n useEffect(() => {\n if (value === undefined || !inputRef.current) {\n return\n }\n\n /**\n * Do not format the given value in the following cases\n * - while input element is focused.\n * - if the given value is not date formattable.\n */\n if (!isInputFocused) {\n const newDate = functions.stringToDate(value)\n\n if (newDate && dayjs(newDate).isValid()) {\n inputRef.current.value = functions.dateToString(newDate)\n setAlternativeFormat(functions.dateToAlternativeFormat(newDate))\n setSelectedDate(newDate)\n\n return\n }\n\n setSelectedDate(null)\n }\n\n inputRef.current.value = value || ''\n }, [value, isInputFocused, functions])\n\n useOuterClick([inputWrapperRef, calendarPortalRef], functions.closeCalendar)\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {\n return\n }\n\n const calendarButtons = calendarPortalRef.current.querySelectorAll('button')\n\n if (calendarButtons.length === 0) {\n return\n }\n\n const firstCalendarButton = calendarButtons[0]\n\n if (latest.isInputFocused) {\n if (e.shiftKey) {\n // move focus from Input to previous elements of DatePicker\n functions.closeCalendar()\n\n return\n }\n\n // move focus from Input to Calendar\n e.preventDefault()\n firstCalendarButton.focus()\n\n return\n }\n\n const calendarButtonAry = Array.from(calendarButtons)\n const currentFocused = calendarButtonAry.find((button) => button === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstCalendarButton) {\n // move focus from Calendar to Input\n inputRef.current.focus()\n e.preventDefault()\n }\n } else if (currentFocused === calendarButtonAry.at(-1)) {\n // move focus from Calendar to next elements of DatePicker\n inputRef.current.focus()\n functions.closeCalendar()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n }\n }, [functions, latest])\n\n const caretIconColor =\n isInputFocused || isCalendarShown\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n <div\n onClick={!isCalendarShown && !disabled ? functions.openCalendar : undefined}\n onKeyDown={isCalendarShown ? functions.handleDelegateKeyDown : undefined}\n role=\"presentation\"\n className={classNames.container}\n style={{\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <div ref={inputWrapperRef}>\n <Input\n {...rest}\n data-smarthr-ui-input=\"true\"\n width=\"100%\"\n name={name}\n onChange={isCalendarShown ? functions.closeCalendar : undefined}\n onKeyPress={functions.handleKeyPressInput}\n onFocus={functions.handleFocusInput}\n onBlur={functions.handleBlur}\n suffix={\n <InputSuffixIcon\n alternativeFormat={showAlternative ? alternativeFormat : null}\n caretIconColor={caretIconColor}\n classNames={classNames}\n />\n }\n disabled={disabled}\n error={error}\n ref={inputRef}\n className=\"smarthr-ui-DatePicker-inputContainer\"\n aria-expanded={isCalendarShown}\n aria-controls={calenderId}\n aria-haspopup={true}\n />\n </div>\n {isCalendarShown && inputRect && (\n <Portal inputRect={inputRect} ref={calendarPortalRef}>\n <Calendar\n id={calenderId}\n value={selectedDate || undefined}\n from={from}\n to={to}\n onSelectDate={functions.handleSelectDateCalendar}\n />\n </Portal>\n )}\n </div>\n )\n },\n)\n\nconst InputSuffixIcon = memo<{\n classNames: { inputSuffixLayout: string; inputSuffixWrapper: string; inputSuffixText: string }\n alternativeFormat: null | ReactNode\n caretIconColor: ComponentProps<typeof FaCalendarDaysIcon>['color']\n}>(({ classNames, alternativeFormat, caretIconColor }) => (\n <span className={classNames.inputSuffixLayout}>\n <span className={classNames.inputSuffixWrapper}>\n {alternativeFormat && <span className={classNames.inputSuffixText}>{alternativeFormat}</span>}\n <FaCalendarDaysIcon color={caretIconColor} />\n </span>\n </span>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA6EO;AAEP;AACE;AACE;AACA;AACA;AAEA;AACD;AACF;AAED;AACA;AAEA;AAEA;AAIE;AAAU;AACV;AACF;AAEA;;AAsBI;AACA;AACE;;AAIE;;;;;AAKJ;;AAGA;AACA;AACA;;;;AAIA;AAEA;;;;;;;;;;AAaC;AAED;;AAIE;;AAC+B;AAC7B;AACF;AAEA;AAGA;;;AAII;;;;AAMF;;;AAIE;;;AAIF;AAEA;AACA;;AAGA;;;AAIE;AACA;AAEA;AACA;;AAEE;;;;;;;AAOE;AACA;;;AAIC;;;AAGT;;;AAKE;;;;AAIF;;;;;;;AAQE;;;AAGE;;AAEF;;;;;;AAM0B;;;AAG1B;AACE;AACE;;AAEA;;;;;AAKF;;AAEF;AACE;;;;AAIsB;;;AAG5B;AAEA;;;;;AAWE;;;;AAIG;;;;;;;;;;;;;;;AAqBH;AACE;;;;AAMA;;;AAIA;AAEA;AACE;;;;;;;;;;;AAeF;AAEA;AACE;;AAEE;;;;;;AAKF;;;AAGJ;AAEA;AAEA;AACE;AACF;AACF;AAEA;AAEI;AACA;AACE;AACA;;;AAIJ;AAMI;AACD;AAyCP;AAGF;;;"}
@@ -159,7 +159,7 @@ const DatePicker = forwardRef(({ value, name, from = DEFAULT_FROM, to, disabled,
159
159
  },
160
160
  };
161
161
  }, [latest]);
162
- useImperativeHandle(ref, () => inputRef.current);
162
+ useImperativeHandle(ref, () => inputRef.current, []);
163
163
  useEffect(() => {
164
164
  if (value === undefined || !inputRef.current) {
165
165
  return;
@@ -181,7 +181,7 @@ const DatePicker = forwardRef(({ value, name, from = DEFAULT_FROM, to, disabled,
181
181
  }
182
182
  inputRef.current.value = value || '';
183
183
  }, [value, isInputFocused, functions]);
184
- useOuterClick(useMemo(() => [inputWrapperRef, calendarPortalRef], [inputWrapperRef, calendarPortalRef]), functions.closeCalendar);
184
+ useOuterClick([inputWrapperRef, calendarPortalRef], functions.closeCalendar);
185
185
  useEffect(() => {
186
186
  const handleKeyDown = (e) => {
187
187
  if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {
@@ -1 +1 @@
1
- {"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport dayjs from 'dayjs'\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type MouseEvent,\n type ReactNode,\n forwardRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useOuterClick } from '../../hooks/useOuterClick'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Calendar } from '../Calendar'\nimport { FaCalendarDaysIcon } from '../Icon'\nimport { Input } from '../Input'\n\nimport { Portal } from './Portal'\nimport { parseJpnDateString } from './datePickerHelper'\n\ntype ChangeLikeEvent = ChangeEvent | React.KeyboardEvent | MouseEvent\ntype BaseProps = {\n /** input 要素の `value` 属性の値 */\n value?: string | null\n /** input 要素の `name` 属性の値 */\n name?: string\n /** 選択可能な期間の開始日 */\n from?: Date\n /** 選択可能な期間の終了日 */\n to?: Date\n /** フォームを無効にするかどうか */\n disabled?: boolean\n /**\n * placeholder属性は非推奨です。別途ヒント用要素を設置するか、それらの領域を確保出来ない場合はTooltipコンポーネントの利用を検討してください。\n */\n placeholder?: string\n /** フォームにエラーがあるかどうか */\n error?: boolean\n /** コンポーネントの幅 */\n width?: number | string\n /** 入力を独自にパースする場合に、パース処理を記述する関数 */\n parseInput?: (input: string) => Date | null\n /** 表示する日付を独自にフォーマットする場合に、フォーマット処理を記述する関数 */\n formatDate?: (date: Date | null) => string\n /** 入出力用文字列と併記する別フォーマット処理を記述する関数 */\n showAlternative?: (date: Date | null) => ReactNode\n /** @deprecated onChangeDate は非推奨です。onChange を使ってください。 */\n onChangeDate?: (date: Date | null, value: string, other: { errors: string[] }) => void\n /** 選択された日付が変わった時に発火するコールバック関数 */\n onChange?: (\n e: ChangeEvent<HTMLInputElement>,\n other: { date: Date | null; formatValue: string; errors: string[] },\n ) => void\n}\ntype Props = BaseProps &\n Omit<\n ComponentPropsWithRef<'input'>,\n | keyof BaseProps\n | 'type'\n | 'onChange'\n | 'onKeyPress'\n | 'onFocus'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-haspopup'\n >\n\nexport const DEFAULT_FROM = new Date(1900, 0, 1)\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-DatePicker shr-inline-block',\n inputSuffixLayout: 'shr-box-border shr-h-full shr-py-0.5',\n inputSuffixWrapper:\n 'shr-border-l-shorthand shr-box-border shr-flex shr-h-full shr-items-center shr-justify-center shr-ps-0.5 shr-text-base',\n inputSuffixText: 'shr-text-gray shr-me-0.5 shr-text-sm',\n },\n})\n\nconst DEFAULT_DATE_TO_STRING_FORMAT = 'YYYY/MM/DD'\nconst DEFAULT_DATE_TO_STRING = (d: Date | null) =>\n d ? dayjs(d).format(DEFAULT_DATE_TO_STRING_FORMAT) : ''\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\n\nconst parseStringDate = (\n str: string | null | undefined,\n parseInputFn: ((input: string) => Date | null) | undefined,\n) => {\n if (!str) return null\n return parseInputFn ? parseInputFn(str) : parseJpnDateString(str)\n}\n\n/** @deprecated DatePicker は非推奨です。Input[type=date] を使ってください。 */\nexport const DatePicker = forwardRef<HTMLInputElement, Props>(\n (\n {\n value,\n name,\n from = DEFAULT_FROM,\n to,\n disabled,\n width,\n error,\n className,\n parseInput,\n formatDate,\n showAlternative,\n onChangeDate,\n onChange,\n onBlur,\n ...rest\n },\n ref,\n ) => {\n const theme = useTheme()\n const classNames = useMemo(() => {\n const { container, inputSuffixLayout, inputSuffixWrapper, inputSuffixText } =\n classNameGenerator()\n\n return {\n container: container({ className }),\n inputSuffixLayout: inputSuffixLayout(),\n inputSuffixWrapper: inputSuffixWrapper(),\n inputSuffixText: inputSuffixText(),\n }\n }, [className])\n\n const [isInputFocused, setIsInputFocused] = useState(false)\n const inputRef = useRef<HTMLInputElement>(null)\n const inputWrapperRef = useRef<HTMLDivElement>(null)\n const calendarPortalRef = useRef<HTMLDivElement>(null)\n const [inputRect, setInputRect] = useState<DOMRect | null>(null)\n const [isCalendarShown, setIsCalendarShown] = useState(false)\n const [alternativeFormat, setAlternativeFormat] = useState<null | ReactNode>(null)\n const calenderId = useId()\n\n const [selectedDate, setSelectedDate] = useState<Date | null>(() =>\n parseStringDate(value, parseInput),\n )\n\n const latest = useLatest({\n onChange,\n onChangeDate,\n parseInput,\n formatDate,\n showAlternative,\n onBlur,\n isInputFocused,\n selectedDate,\n })\n\n const functions = useMemo(() => {\n const dateToString = (date: Date | null) =>\n latest.formatDate ? latest.formatDate(date) : DEFAULT_DATE_TO_STRING(date)\n\n const dateToAlternativeFormat = (d: Date | null) => {\n if (!latest.showAlternative) return null\n return d ? latest.showAlternative(d) : null\n }\n\n const stringToDate = (str: string | null | undefined) =>\n parseStringDate(str, latest.parseInput)\n\n const updateDate = (e: ChangeLikeEvent, newDate: Date | null) => {\n if (\n !inputRef.current ||\n newDate === latest.selectedDate ||\n (newDate && latest.selectedDate && newDate.getTime() === latest.selectedDate.getTime())\n ) {\n // Do not update date if the new date is same with the old one.\n return\n }\n\n const isValid = !newDate || dayjs(newDate).isValid()\n const errors: string[] = []\n\n if (!isValid) {\n errors.push('INVALID_DATE')\n }\n\n const nextDate = isValid ? newDate : null\n const formatValue = dateToString(nextDate)\n\n inputRef.current.value = formatValue\n setAlternativeFormat(dateToAlternativeFormat(nextDate))\n setSelectedDate(nextDate)\n\n if (latest.onChange) {\n e.preventDefault()\n e.stopPropagation()\n\n const event = new Event('change', { bubbles: true })\n const input = inputRef.current\n\n input.dispatchEvent(event)\n latest.onChange(\n // HINT: 型問題のため別途オブジェクトをイベントに見立てる\n {\n stopPropagation: () => {\n event.stopPropagation()\n },\n preventDefault: () => {\n event.preventDefault()\n },\n target: input,\n currentTarget: input,\n } as ChangeEvent<HTMLInputElement>,\n { date: nextDate, formatValue, errors },\n )\n } else if (latest.onChangeDate) {\n latest.onChangeDate(nextDate, formatValue, { errors })\n }\n }\n\n const closeCalendar = () => setIsCalendarShown(false)\n\n const openCalendar = () => {\n if (inputWrapperRef.current) {\n setIsCalendarShown(true)\n setInputRect(inputWrapperRef.current.getBoundingClientRect())\n }\n }\n\n return {\n stringToDate,\n dateToString,\n dateToAlternativeFormat,\n closeCalendar,\n openCalendar,\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => {\n setIsInputFocused(false)\n updateDate(e, e.target.value ? stringToDate(e.target.value) : null)\n latest.onBlur?.(e)\n },\n handleDelegateKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n }\n },\n handleKeyPressInput: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n const isExpanded = e.currentTarget.getAttribute('aria-expanded') === 'true'\n ;(isExpanded ? closeCalendar : openCalendar)()\n updateDate(e, stringToDate(e.currentTarget.value))\n }\n },\n handleFocusInput: () => {\n setIsInputFocused(true)\n openCalendar()\n },\n handleSelectDateCalendar: (e: ChangeLikeEvent, selected: Date | null) => {\n updateDate(e, selected)\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n },\n }\n }, [latest])\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n ref,\n () => inputRef.current,\n )\n\n useEffect(() => {\n if (value === undefined || !inputRef.current) {\n return\n }\n\n /**\n * Do not format the given value in the following cases\n * - while input element is focused.\n * - if the given value is not date formattable.\n */\n if (!isInputFocused) {\n const newDate = functions.stringToDate(value)\n\n if (newDate && dayjs(newDate).isValid()) {\n inputRef.current.value = functions.dateToString(newDate)\n setAlternativeFormat(functions.dateToAlternativeFormat(newDate))\n setSelectedDate(newDate)\n\n return\n }\n\n setSelectedDate(null)\n }\n\n inputRef.current.value = value || ''\n }, [value, isInputFocused, functions])\n\n useOuterClick(\n useMemo(() => [inputWrapperRef, calendarPortalRef], [inputWrapperRef, calendarPortalRef]),\n functions.closeCalendar,\n )\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {\n return\n }\n\n const calendarButtons = calendarPortalRef.current.querySelectorAll('button')\n\n if (calendarButtons.length === 0) {\n return\n }\n\n const firstCalendarButton = calendarButtons[0]\n\n if (latest.isInputFocused) {\n if (e.shiftKey) {\n // move focus from Input to previous elements of DatePicker\n functions.closeCalendar()\n\n return\n }\n\n // move focus from Input to Calendar\n e.preventDefault()\n firstCalendarButton.focus()\n\n return\n }\n\n const calendarButtonAry = Array.from(calendarButtons)\n const currentFocused = calendarButtonAry.find((button) => button === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstCalendarButton) {\n // move focus from Calendar to Input\n inputRef.current.focus()\n e.preventDefault()\n }\n } else if (currentFocused === calendarButtonAry.at(-1)) {\n // move focus from Calendar to next elements of DatePicker\n inputRef.current.focus()\n functions.closeCalendar()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n }\n }, [functions, latest])\n\n const caretIconColor =\n isInputFocused || isCalendarShown\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n <div\n onClick={!isCalendarShown && !disabled ? functions.openCalendar : undefined}\n onKeyDown={isCalendarShown ? functions.handleDelegateKeyDown : undefined}\n role=\"presentation\"\n className={classNames.container}\n style={{\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <div ref={inputWrapperRef}>\n <Input\n {...rest}\n data-smarthr-ui-input=\"true\"\n width=\"100%\"\n name={name}\n onChange={isCalendarShown ? functions.closeCalendar : undefined}\n onKeyPress={functions.handleKeyPressInput}\n onFocus={functions.handleFocusInput}\n onBlur={functions.handleBlur}\n suffix={\n <InputSuffixIcon\n alternativeFormat={showAlternative ? alternativeFormat : null}\n caretIconColor={caretIconColor}\n classNames={classNames}\n />\n }\n disabled={disabled}\n error={error}\n ref={inputRef}\n className=\"smarthr-ui-DatePicker-inputContainer\"\n aria-expanded={isCalendarShown}\n aria-controls={calenderId}\n aria-haspopup={true}\n />\n </div>\n {isCalendarShown && inputRect && (\n <Portal inputRect={inputRect} ref={calendarPortalRef}>\n <Calendar\n id={calenderId}\n value={selectedDate || undefined}\n from={from}\n to={to}\n onSelectDate={functions.handleSelectDateCalendar}\n />\n </Portal>\n )}\n </div>\n )\n },\n)\n\nconst InputSuffixIcon = memo<{\n classNames: { inputSuffixLayout: string; inputSuffixWrapper: string; inputSuffixText: string }\n alternativeFormat: null | ReactNode\n caretIconColor: ComponentProps<typeof FaCalendarDaysIcon>['color']\n}>(({ classNames, alternativeFormat, caretIconColor }) => (\n <span className={classNames.inputSuffixLayout}>\n <span className={classNames.inputSuffixWrapper}>\n {alternativeFormat && <span className={classNames.inputSuffixText}>{alternativeFormat}</span>}\n <FaCalendarDaysIcon color={caretIconColor} />\n </span>\n </span>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AA6EO;AAEP;AACE;AACE;AACA;AACA;AAEA;AACD;AACF;AAED;AACA;AAEA;AAEA;AAIE;AAAU;AACV;AACF;AAEA;;AAsBI;AACA;AACE;;AAIE;;;;;AAKJ;;AAGA;AACA;AACA;;;;AAIA;AAEA;;;;;;;;;;AAaC;AAED;;AAIE;;AAC+B;AAC7B;AACF;AAEA;AAGA;;;AAII;;;;AAMF;;;AAIE;;;AAIF;AAEA;AACA;;AAGA;;;AAIE;AACA;AAEA;AACA;;AAEE;;;;;;;AAOE;AACA;;;AAIC;;;AAGT;;;AAKE;;;;AAIF;;;;;;;AAQE;;;AAGE;;AAEF;;;;;;AAM0B;;;AAG1B;AACE;AACE;;AAEA;;;;;AAKF;;AAEF;AACE;;;;AAIsB;;;AAG5B;;;;;;AAYE;;;;AAIG;;;;;;;;;;;;;;;AAwBH;AACE;;;;AAMA;;;AAIA;AAEA;AACE;;;;;;;;;;;AAeF;AAEA;AACE;;AAEE;;;;;;AAKF;;;AAGJ;AAEA;AAEA;AACE;AACF;AACF;AAEA;AAEI;AACA;AACE;AACA;;;AAIJ;AAMI;AACD;AAyCP;AAGF;;"}
1
+ {"version":3,"file":"DatePicker.js","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport dayjs from 'dayjs'\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type MouseEvent,\n type ReactNode,\n forwardRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useOuterClick } from '../../hooks/useOuterClick'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Calendar } from '../Calendar'\nimport { FaCalendarDaysIcon } from '../Icon'\nimport { Input } from '../Input'\n\nimport { Portal } from './Portal'\nimport { parseJpnDateString } from './datePickerHelper'\n\ntype ChangeLikeEvent = ChangeEvent | React.KeyboardEvent | MouseEvent\ntype BaseProps = {\n /** input 要素の `value` 属性の値 */\n value?: string | null\n /** input 要素の `name` 属性の値 */\n name?: string\n /** 選択可能な期間の開始日 */\n from?: Date\n /** 選択可能な期間の終了日 */\n to?: Date\n /** フォームを無効にするかどうか */\n disabled?: boolean\n /**\n * placeholder属性は非推奨です。別途ヒント用要素を設置するか、それらの領域を確保出来ない場合はTooltipコンポーネントの利用を検討してください。\n */\n placeholder?: string\n /** フォームにエラーがあるかどうか */\n error?: boolean\n /** コンポーネントの幅 */\n width?: number | string\n /** 入力を独自にパースする場合に、パース処理を記述する関数 */\n parseInput?: (input: string) => Date | null\n /** 表示する日付を独自にフォーマットする場合に、フォーマット処理を記述する関数 */\n formatDate?: (date: Date | null) => string\n /** 入出力用文字列と併記する別フォーマット処理を記述する関数 */\n showAlternative?: (date: Date | null) => ReactNode\n /** @deprecated onChangeDate は非推奨です。onChange を使ってください。 */\n onChangeDate?: (date: Date | null, value: string, other: { errors: string[] }) => void\n /** 選択された日付が変わった時に発火するコールバック関数 */\n onChange?: (\n e: ChangeEvent<HTMLInputElement>,\n other: { date: Date | null; formatValue: string; errors: string[] },\n ) => void\n}\ntype Props = BaseProps &\n Omit<\n ComponentPropsWithRef<'input'>,\n | keyof BaseProps\n | 'type'\n | 'onChange'\n | 'onKeyPress'\n | 'onFocus'\n | 'aria-expanded'\n | 'aria-controls'\n | 'aria-haspopup'\n >\n\nexport const DEFAULT_FROM = new Date(1900, 0, 1)\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-DatePicker shr-inline-block',\n inputSuffixLayout: 'shr-box-border shr-h-full shr-py-0.5',\n inputSuffixWrapper:\n 'shr-border-l-shorthand shr-box-border shr-flex shr-h-full shr-items-center shr-justify-center shr-ps-0.5 shr-text-base',\n inputSuffixText: 'shr-text-gray shr-me-0.5 shr-text-sm',\n },\n})\n\nconst DEFAULT_DATE_TO_STRING_FORMAT = 'YYYY/MM/DD'\nconst DEFAULT_DATE_TO_STRING = (d: Date | null) =>\n d ? dayjs(d).format(DEFAULT_DATE_TO_STRING_FORMAT) : ''\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\n\nconst parseStringDate = (\n str: string | null | undefined,\n parseInputFn: ((input: string) => Date | null) | undefined,\n) => {\n if (!str) return null\n return parseInputFn ? parseInputFn(str) : parseJpnDateString(str)\n}\n\n/** @deprecated DatePicker は非推奨です。Input[type=date] を使ってください。 */\nexport const DatePicker = forwardRef<HTMLInputElement, Props>(\n (\n {\n value,\n name,\n from = DEFAULT_FROM,\n to,\n disabled,\n width,\n error,\n className,\n parseInput,\n formatDate,\n showAlternative,\n onChangeDate,\n onChange,\n onBlur,\n ...rest\n },\n ref,\n ) => {\n const theme = useTheme()\n const classNames = useMemo(() => {\n const { container, inputSuffixLayout, inputSuffixWrapper, inputSuffixText } =\n classNameGenerator()\n\n return {\n container: container({ className }),\n inputSuffixLayout: inputSuffixLayout(),\n inputSuffixWrapper: inputSuffixWrapper(),\n inputSuffixText: inputSuffixText(),\n }\n }, [className])\n\n const [isInputFocused, setIsInputFocused] = useState(false)\n const inputRef = useRef<HTMLInputElement>(null)\n const inputWrapperRef = useRef<HTMLDivElement>(null)\n const calendarPortalRef = useRef<HTMLDivElement>(null)\n const [inputRect, setInputRect] = useState<DOMRect | null>(null)\n const [isCalendarShown, setIsCalendarShown] = useState(false)\n const [alternativeFormat, setAlternativeFormat] = useState<null | ReactNode>(null)\n const calenderId = useId()\n\n const [selectedDate, setSelectedDate] = useState<Date | null>(() =>\n parseStringDate(value, parseInput),\n )\n\n const latest = useLatest({\n onChange,\n onChangeDate,\n parseInput,\n formatDate,\n showAlternative,\n onBlur,\n isInputFocused,\n selectedDate,\n })\n\n const functions = useMemo(() => {\n const dateToString = (date: Date | null) =>\n latest.formatDate ? latest.formatDate(date) : DEFAULT_DATE_TO_STRING(date)\n\n const dateToAlternativeFormat = (d: Date | null) => {\n if (!latest.showAlternative) return null\n return d ? latest.showAlternative(d) : null\n }\n\n const stringToDate = (str: string | null | undefined) =>\n parseStringDate(str, latest.parseInput)\n\n const updateDate = (e: ChangeLikeEvent, newDate: Date | null) => {\n if (\n !inputRef.current ||\n newDate === latest.selectedDate ||\n (newDate && latest.selectedDate && newDate.getTime() === latest.selectedDate.getTime())\n ) {\n // Do not update date if the new date is same with the old one.\n return\n }\n\n const isValid = !newDate || dayjs(newDate).isValid()\n const errors: string[] = []\n\n if (!isValid) {\n errors.push('INVALID_DATE')\n }\n\n const nextDate = isValid ? newDate : null\n const formatValue = dateToString(nextDate)\n\n inputRef.current.value = formatValue\n setAlternativeFormat(dateToAlternativeFormat(nextDate))\n setSelectedDate(nextDate)\n\n if (latest.onChange) {\n e.preventDefault()\n e.stopPropagation()\n\n const event = new Event('change', { bubbles: true })\n const input = inputRef.current\n\n input.dispatchEvent(event)\n latest.onChange(\n // HINT: 型問題のため別途オブジェクトをイベントに見立てる\n {\n stopPropagation: () => {\n event.stopPropagation()\n },\n preventDefault: () => {\n event.preventDefault()\n },\n target: input,\n currentTarget: input,\n } as ChangeEvent<HTMLInputElement>,\n { date: nextDate, formatValue, errors },\n )\n } else if (latest.onChangeDate) {\n latest.onChangeDate(nextDate, formatValue, { errors })\n }\n }\n\n const closeCalendar = () => setIsCalendarShown(false)\n\n const openCalendar = () => {\n if (inputWrapperRef.current) {\n setIsCalendarShown(true)\n setInputRect(inputWrapperRef.current.getBoundingClientRect())\n }\n }\n\n return {\n stringToDate,\n dateToString,\n dateToAlternativeFormat,\n closeCalendar,\n openCalendar,\n handleBlur: (e: React.FocusEvent<HTMLInputElement>) => {\n setIsInputFocused(false)\n updateDate(e, e.target.value ? stringToDate(e.target.value) : null)\n latest.onBlur?.(e)\n },\n handleDelegateKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n }\n },\n handleKeyPressInput: (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n const isExpanded = e.currentTarget.getAttribute('aria-expanded') === 'true'\n ;(isExpanded ? closeCalendar : openCalendar)()\n updateDate(e, stringToDate(e.currentTarget.value))\n }\n },\n handleFocusInput: () => {\n setIsInputFocused(true)\n openCalendar()\n },\n handleSelectDateCalendar: (e: ChangeLikeEvent, selected: Date | null) => {\n updateDate(e, selected)\n // delay hiding calendar because calendar will be displayed when input is focused\n requestAnimationFrame(closeCalendar)\n\n if (inputRef.current) inputRef.current.focus()\n },\n }\n }, [latest])\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n ref,\n () => inputRef.current,\n [],\n )\n\n useEffect(() => {\n if (value === undefined || !inputRef.current) {\n return\n }\n\n /**\n * Do not format the given value in the following cases\n * - while input element is focused.\n * - if the given value is not date formattable.\n */\n if (!isInputFocused) {\n const newDate = functions.stringToDate(value)\n\n if (newDate && dayjs(newDate).isValid()) {\n inputRef.current.value = functions.dateToString(newDate)\n setAlternativeFormat(functions.dateToAlternativeFormat(newDate))\n setSelectedDate(newDate)\n\n return\n }\n\n setSelectedDate(null)\n }\n\n inputRef.current.value = value || ''\n }, [value, isInputFocused, functions])\n\n useOuterClick([inputWrapperRef, calendarPortalRef], functions.closeCalendar)\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {\n return\n }\n\n const calendarButtons = calendarPortalRef.current.querySelectorAll('button')\n\n if (calendarButtons.length === 0) {\n return\n }\n\n const firstCalendarButton = calendarButtons[0]\n\n if (latest.isInputFocused) {\n if (e.shiftKey) {\n // move focus from Input to previous elements of DatePicker\n functions.closeCalendar()\n\n return\n }\n\n // move focus from Input to Calendar\n e.preventDefault()\n firstCalendarButton.focus()\n\n return\n }\n\n const calendarButtonAry = Array.from(calendarButtons)\n const currentFocused = calendarButtonAry.find((button) => button === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstCalendarButton) {\n // move focus from Calendar to Input\n inputRef.current.focus()\n e.preventDefault()\n }\n } else if (currentFocused === calendarButtonAry.at(-1)) {\n // move focus from Calendar to next elements of DatePicker\n inputRef.current.focus()\n functions.closeCalendar()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n }\n }, [functions, latest])\n\n const caretIconColor =\n isInputFocused || isCalendarShown\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n // eslint-disable-next-line smarthr/best-practice-for-interactive-element\n <div\n onClick={!isCalendarShown && !disabled ? functions.openCalendar : undefined}\n onKeyDown={isCalendarShown ? functions.handleDelegateKeyDown : undefined}\n role=\"presentation\"\n className={classNames.container}\n style={{\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <div ref={inputWrapperRef}>\n <Input\n {...rest}\n data-smarthr-ui-input=\"true\"\n width=\"100%\"\n name={name}\n onChange={isCalendarShown ? functions.closeCalendar : undefined}\n onKeyPress={functions.handleKeyPressInput}\n onFocus={functions.handleFocusInput}\n onBlur={functions.handleBlur}\n suffix={\n <InputSuffixIcon\n alternativeFormat={showAlternative ? alternativeFormat : null}\n caretIconColor={caretIconColor}\n classNames={classNames}\n />\n }\n disabled={disabled}\n error={error}\n ref={inputRef}\n className=\"smarthr-ui-DatePicker-inputContainer\"\n aria-expanded={isCalendarShown}\n aria-controls={calenderId}\n aria-haspopup={true}\n />\n </div>\n {isCalendarShown && inputRect && (\n <Portal inputRect={inputRect} ref={calendarPortalRef}>\n <Calendar\n id={calenderId}\n value={selectedDate || undefined}\n from={from}\n to={to}\n onSelectDate={functions.handleSelectDateCalendar}\n />\n </Portal>\n )}\n </div>\n )\n },\n)\n\nconst InputSuffixIcon = memo<{\n classNames: { inputSuffixLayout: string; inputSuffixWrapper: string; inputSuffixText: string }\n alternativeFormat: null | ReactNode\n caretIconColor: ComponentProps<typeof FaCalendarDaysIcon>['color']\n}>(({ classNames, alternativeFormat, caretIconColor }) => (\n <span className={classNames.inputSuffixLayout}>\n <span className={classNames.inputSuffixWrapper}>\n {alternativeFormat && <span className={classNames.inputSuffixText}>{alternativeFormat}</span>}\n <FaCalendarDaysIcon color={caretIconColor} />\n </span>\n </span>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;AA6EO;AAEP;AACE;AACE;AACA;AACA;AAEA;AACD;AACF;AAED;AACA;AAEA;AAEA;AAIE;AAAU;AACV;AACF;AAEA;;AAsBI;AACA;AACE;;AAIE;;;;;AAKJ;;AAGA;AACA;AACA;;;;AAIA;AAEA;;;;;;;;;;AAaC;AAED;;AAIE;;AAC+B;AAC7B;AACF;AAEA;AAGA;;;AAII;;;;AAMF;;;AAIE;;;AAIF;AAEA;AACA;;AAGA;;;AAIE;AACA;AAEA;AACA;;AAEE;;;;;;;AAOE;AACA;;;AAIC;;;AAGT;;;AAKE;;;;AAIF;;;;;;;AAQE;;;AAGE;;AAEF;;;;;;AAM0B;;;AAG1B;AACE;AACE;;AAEA;;;;;AAKF;;AAEF;AACE;;;;AAIsB;;;AAG5B;AAEA;;;;;AAWE;;;;AAIG;;;;;;;;;;;;;;;AAqBH;AACE;;;;AAMA;;;AAIA;AAEA;AACE;;;;;;;;;;;AAeF;AAEA;AACE;;AAEE;;;;;;AAKF;;;AAGJ;AAEA;AAEA;AACE;AACF;AACF;AAEA;AAEI;AACA;AACE;AACA;;;AAIJ;AAMI;AACD;AAyCP;AAGF;;"}
@@ -13,7 +13,7 @@ const initialPosition = {
13
13
  const Portal = React.forwardRef(({ inputRect, ...rest }, ref) => {
14
14
  const { portalRoot, createPortal } = hooks_usePortal.usePortal();
15
15
  const containerRef = React.useRef(null);
16
- React.useImperativeHandle(ref, () => containerRef.current);
16
+ React.useImperativeHandle(ref, () => containerRef.current, [portalRoot]);
17
17
  const [style, setStyle] = React.useState(initialPosition);
18
18
  hooks_useEnhancedEffect.useEnhancedEffect(() => {
19
19
  if (containerRef.current) {
@@ -1 +1 @@
1
- {"version":3,"file":"Portal.cjs","sources":["../../../src/components/DatePicker/Portal.tsx"],"sourcesContent":["import { type PropsWithChildren, forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nimport { useEnhancedEffect } from '../../hooks/useEnhancedEffect'\nimport { usePortal } from '../../hooks/usePortal'\n\nimport { getPortalPosition } from './datePickerHelper'\n\ntype Props = PropsWithChildren<{\n inputRect: DOMRect\n}>\n\nconst initialPosition = {\n top: '0px',\n left: '0px',\n}\n\nexport const Portal = forwardRef<HTMLDivElement, Props>(({ inputRect, ...rest }, ref) => {\n const { portalRoot, createPortal } = usePortal()\n const containerRef = useRef<HTMLDivElement>(null)\n\n useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => containerRef.current)\n\n const [style, setStyle] = useState(initialPosition)\n\n useEnhancedEffect(() => {\n if (containerRef.current) {\n const position = getPortalPosition(inputRect, containerRef.current.offsetHeight)\n\n setStyle({\n top: `${position.top}px`,\n left: `${position.left}px`,\n })\n }\n }, [inputRect, portalRoot])\n\n return createPortal(\n <div\n {...rest}\n ref={containerRef}\n className=\"smarthr-ui-DatePicker-calendarContainer shr-absolute shr-z-overlap shr-leading-none\"\n style={style}\n />,\n )\n})\n"],"names":["forwardRef","usePortal","useRef","useImperativeHandle","useState","useEnhancedEffect","getPortalPosition","_jsx"],"mappings":";;;;;;;;AAWA,MAAM,eAAe,GAAG;AACtB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,IAAI,EAAE,KAAK;CACZ;AAEM,MAAM,MAAM,GAAGA,gBAAU,CAAwB,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,KAAI;IACtF,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAGC,yBAAS,EAAE;AAChD,IAAA,MAAM,YAAY,GAAGC,YAAM,CAAiB,IAAI,CAAC;IAEjDC,yBAAmB,CAA+C,GAAG,EAAE,MAAM,YAAY,CAAC,OAAO,CAAC;IAElG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAGC,cAAQ,CAAC,eAAe,CAAC;IAEnDC,yCAAiB,CAAC,MAAK;AACrB,QAAA,IAAI,YAAY,CAAC,OAAO,EAAE;AACxB,YAAA,MAAM,QAAQ,GAAGC,wDAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC;AAEhF,YAAA,QAAQ,CAAC;AACP,gBAAA,GAAG,EAAE,CAAA,EAAG,QAAQ,CAAC,GAAG,CAAA,EAAA,CAAI;AACxB,gBAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,IAAI,CAAA,EAAA,CAAI;AAC3B,aAAA,CAAC;QACJ;AACF,IAAA,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAE3B,IAAA,OAAO,YAAY,CACjBC,cAAA,CAAA,KAAA,EAAA,EAAA,GACM,IAAI,EACR,GAAG,EAAE,YAAY,EACjB,SAAS,EAAC,qFAAqF,EAC/F,KAAK,EAAE,KAAK,EAAA,CACZ,CACH;AACH,CAAC;;;;"}
1
+ {"version":3,"file":"Portal.cjs","sources":["../../../src/components/DatePicker/Portal.tsx"],"sourcesContent":["import { type PropsWithChildren, forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nimport { useEnhancedEffect } from '../../hooks/useEnhancedEffect'\nimport { usePortal } from '../../hooks/usePortal'\n\nimport { getPortalPosition } from './datePickerHelper'\n\ntype Props = PropsWithChildren<{\n inputRect: DOMRect\n}>\n\nconst initialPosition = {\n top: '0px',\n left: '0px',\n}\n\nexport const Portal = forwardRef<HTMLDivElement, Props>(({ inputRect, ...rest }, ref) => {\n const { portalRoot, createPortal } = usePortal()\n const containerRef = useRef<HTMLDivElement>(null)\n\n useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(\n ref,\n () => containerRef.current,\n [portalRoot], // eslint-disable-line react-hooks/exhaustive-deps\n )\n\n const [style, setStyle] = useState(initialPosition)\n\n useEnhancedEffect(() => {\n if (containerRef.current) {\n const position = getPortalPosition(inputRect, containerRef.current.offsetHeight)\n\n setStyle({\n top: `${position.top}px`,\n left: `${position.left}px`,\n })\n }\n }, [inputRect, portalRoot])\n\n return createPortal(\n <div\n {...rest}\n ref={containerRef}\n className=\"smarthr-ui-DatePicker-calendarContainer shr-absolute shr-z-overlap shr-leading-none\"\n style={style}\n />,\n )\n})\n"],"names":["forwardRef","usePortal","useRef","useImperativeHandle","useState","useEnhancedEffect","getPortalPosition","_jsx"],"mappings":";;;;;;;;AAWA,MAAM,eAAe,GAAG;AACtB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,IAAI,EAAE,KAAK;CACZ;AAEM,MAAM,MAAM,GAAGA,gBAAU,CAAwB,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,KAAI;IACtF,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAGC,yBAAS,EAAE;AAChD,IAAA,MAAM,YAAY,GAAGC,YAAM,CAAiB,IAAI,CAAC;AAEjD,IAAAC,yBAAmB,CACjB,GAAG,EACH,MAAM,YAAY,CAAC,OAAO,EAC1B,CAAC,UAAU,CAAC,CACb;IAED,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAGC,cAAQ,CAAC,eAAe,CAAC;IAEnDC,yCAAiB,CAAC,MAAK;AACrB,QAAA,IAAI,YAAY,CAAC,OAAO,EAAE;AACxB,YAAA,MAAM,QAAQ,GAAGC,wDAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC;AAEhF,YAAA,QAAQ,CAAC;AACP,gBAAA,GAAG,EAAE,CAAA,EAAG,QAAQ,CAAC,GAAG,CAAA,EAAA,CAAI;AACxB,gBAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,IAAI,CAAA,EAAA,CAAI;AAC3B,aAAA,CAAC;QACJ;AACF,IAAA,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAE3B,IAAA,OAAO,YAAY,CACjBC,cAAA,CAAA,KAAA,EAAA,EAAA,GACM,IAAI,EACR,GAAG,EAAE,YAAY,EACjB,SAAS,EAAC,qFAAqF,EAC/F,KAAK,EAAE,KAAK,EAAA,CACZ,CACH;AACH,CAAC;;;;"}
@@ -11,7 +11,7 @@ const initialPosition = {
11
11
  const Portal = forwardRef(({ inputRect, ...rest }, ref) => {
12
12
  const { portalRoot, createPortal } = usePortal();
13
13
  const containerRef = useRef(null);
14
- useImperativeHandle(ref, () => containerRef.current);
14
+ useImperativeHandle(ref, () => containerRef.current, [portalRoot]);
15
15
  const [style, setStyle] = useState(initialPosition);
16
16
  useEnhancedEffect(() => {
17
17
  if (containerRef.current) {
@@ -1 +1 @@
1
- {"version":3,"file":"Portal.js","sources":["../../../src/components/DatePicker/Portal.tsx"],"sourcesContent":["import { type PropsWithChildren, forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nimport { useEnhancedEffect } from '../../hooks/useEnhancedEffect'\nimport { usePortal } from '../../hooks/usePortal'\n\nimport { getPortalPosition } from './datePickerHelper'\n\ntype Props = PropsWithChildren<{\n inputRect: DOMRect\n}>\n\nconst initialPosition = {\n top: '0px',\n left: '0px',\n}\n\nexport const Portal = forwardRef<HTMLDivElement, Props>(({ inputRect, ...rest }, ref) => {\n const { portalRoot, createPortal } = usePortal()\n const containerRef = useRef<HTMLDivElement>(null)\n\n useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(ref, () => containerRef.current)\n\n const [style, setStyle] = useState(initialPosition)\n\n useEnhancedEffect(() => {\n if (containerRef.current) {\n const position = getPortalPosition(inputRect, containerRef.current.offsetHeight)\n\n setStyle({\n top: `${position.top}px`,\n left: `${position.left}px`,\n })\n }\n }, [inputRect, portalRoot])\n\n return createPortal(\n <div\n {...rest}\n ref={containerRef}\n className=\"smarthr-ui-DatePicker-calendarContainer shr-absolute shr-z-overlap shr-leading-none\"\n style={style}\n />,\n )\n})\n"],"names":["_jsx"],"mappings":";;;;;;AAWA,MAAM,eAAe,GAAG;AACtB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,IAAI,EAAE,KAAK;CACZ;AAEM,MAAM,MAAM,GAAG,UAAU,CAAwB,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,KAAI;IACtF,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE;AAChD,IAAA,MAAM,YAAY,GAAG,MAAM,CAAiB,IAAI,CAAC;IAEjD,mBAAmB,CAA+C,GAAG,EAAE,MAAM,YAAY,CAAC,OAAO,CAAC;IAElG,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,eAAe,CAAC;IAEnD,iBAAiB,CAAC,MAAK;AACrB,QAAA,IAAI,YAAY,CAAC,OAAO,EAAE;AACxB,YAAA,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC;AAEhF,YAAA,QAAQ,CAAC;AACP,gBAAA,GAAG,EAAE,CAAA,EAAG,QAAQ,CAAC,GAAG,CAAA,EAAA,CAAI;AACxB,gBAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,IAAI,CAAA,EAAA,CAAI;AAC3B,aAAA,CAAC;QACJ;AACF,IAAA,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAE3B,IAAA,OAAO,YAAY,CACjBA,GAAA,CAAA,KAAA,EAAA,EAAA,GACM,IAAI,EACR,GAAG,EAAE,YAAY,EACjB,SAAS,EAAC,qFAAqF,EAC/F,KAAK,EAAE,KAAK,EAAA,CACZ,CACH;AACH,CAAC;;;;"}
1
+ {"version":3,"file":"Portal.js","sources":["../../../src/components/DatePicker/Portal.tsx"],"sourcesContent":["import { type PropsWithChildren, forwardRef, useImperativeHandle, useRef, useState } from 'react'\n\nimport { useEnhancedEffect } from '../../hooks/useEnhancedEffect'\nimport { usePortal } from '../../hooks/usePortal'\n\nimport { getPortalPosition } from './datePickerHelper'\n\ntype Props = PropsWithChildren<{\n inputRect: DOMRect\n}>\n\nconst initialPosition = {\n top: '0px',\n left: '0px',\n}\n\nexport const Portal = forwardRef<HTMLDivElement, Props>(({ inputRect, ...rest }, ref) => {\n const { portalRoot, createPortal } = usePortal()\n const containerRef = useRef<HTMLDivElement>(null)\n\n useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(\n ref,\n () => containerRef.current,\n [portalRoot], // eslint-disable-line react-hooks/exhaustive-deps\n )\n\n const [style, setStyle] = useState(initialPosition)\n\n useEnhancedEffect(() => {\n if (containerRef.current) {\n const position = getPortalPosition(inputRect, containerRef.current.offsetHeight)\n\n setStyle({\n top: `${position.top}px`,\n left: `${position.left}px`,\n })\n }\n }, [inputRect, portalRoot])\n\n return createPortal(\n <div\n {...rest}\n ref={containerRef}\n className=\"smarthr-ui-DatePicker-calendarContainer shr-absolute shr-z-overlap shr-leading-none\"\n style={style}\n />,\n )\n})\n"],"names":["_jsx"],"mappings":";;;;;;AAWA,MAAM,eAAe,GAAG;AACtB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,IAAI,EAAE,KAAK;CACZ;AAEM,MAAM,MAAM,GAAG,UAAU,CAAwB,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,KAAI;IACtF,MAAM,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,SAAS,EAAE;AAChD,IAAA,MAAM,YAAY,GAAG,MAAM,CAAiB,IAAI,CAAC;AAEjD,IAAA,mBAAmB,CACjB,GAAG,EACH,MAAM,YAAY,CAAC,OAAO,EAC1B,CAAC,UAAU,CAAC,CACb;IAED,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,eAAe,CAAC;IAEnD,iBAAiB,CAAC,MAAK;AACrB,QAAA,IAAI,YAAY,CAAC,OAAO,EAAE;AACxB,YAAA,MAAM,QAAQ,GAAG,iBAAiB,CAAC,SAAS,EAAE,YAAY,CAAC,OAAO,CAAC,YAAY,CAAC;AAEhF,YAAA,QAAQ,CAAC;AACP,gBAAA,GAAG,EAAE,CAAA,EAAG,QAAQ,CAAC,GAAG,CAAA,EAAA,CAAI;AACxB,gBAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,IAAI,CAAA,EAAA,CAAI;AAC3B,aAAA,CAAC;QACJ;AACF,IAAA,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAE3B,IAAA,OAAO,YAAY,CACjBA,GAAA,CAAA,KAAA,EAAA,EAAA,GACM,IAAI,EACR,GAAG,EAAE,YAAY,EACjB,SAAS,EAAC,qFAAqF,EAC/F,KAAK,EAAE,KAAK,EAAA,CACZ,CACH;AACH,CAAC;;;;"}
@@ -7,16 +7,16 @@ var libs_tabbable = require('../../libs/tabbable.cjs');
7
7
  const FocusTrap = React.forwardRef(({ firstFocusTarget, children }, ref) => {
8
8
  const innerRef = React.useRef(null);
9
9
  const dummyFocusRef = React.useRef(null);
10
- const focus = React.useCallback(() => {
11
- (firstFocusTarget?.current || dummyFocusRef.current)?.focus();
12
- }, [firstFocusTarget]);
13
- React.useImperativeHandle(ref, () => ({
14
- focus,
15
- }));
10
+ const functions = React.useMemo(() => ({
11
+ focus: () => {
12
+ (firstFocusTarget?.current || dummyFocusRef.current)?.focus();
13
+ },
14
+ }), [firstFocusTarget]);
15
+ React.useImperativeHandle(ref, () => functions, [functions]);
16
16
  React.useEffect(() => {
17
17
  // FocusTrap がマウントされた時点のフォーカス要素を保存
18
18
  const triggerElement = document.activeElement;
19
- focus();
19
+ functions.focus();
20
20
  const handleKeyDown = (e) => {
21
21
  // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。
22
22
  // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても
@@ -50,7 +50,7 @@ const FocusTrap = React.forwardRef(({ firstFocusTarget, children }, ref) => {
50
50
  triggerElement.focus();
51
51
  }
52
52
  };
53
- }, [focus]);
53
+ }, [functions]);
54
54
  return (jsxRuntime.jsxs("div", { ref: innerRef, children: [jsxRuntime.jsx("div", { ref: dummyFocusRef, tabIndex: -1 }), children] }));
55
55
  });
56
56
 
@@ -1 +1 @@
1
- {"version":3,"file":"FocusTrap.cjs","sources":["../../../src/components/Dialog/FocusTrap.tsx"],"sourcesContent":["import {\n type PropsWithChildren,\n type RefObject,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react'\n\nimport { tabbable } from '../../libs/tabbable'\n\ntype Props = PropsWithChildren<{\n firstFocusTarget?: RefObject<HTMLElement>\n}>\n\nexport type FocusTrapRef = {\n focus: () => void\n}\n\nexport const FocusTrap = forwardRef<FocusTrapRef, Props>(({ firstFocusTarget, children }, ref) => {\n const innerRef = useRef<HTMLDivElement | null>(null)\n const dummyFocusRef = useRef<HTMLDivElement>(null)\n\n const focus = useCallback(() => {\n ;(firstFocusTarget?.current || dummyFocusRef.current)?.focus()\n }, [firstFocusTarget])\n\n useImperativeHandle(ref, () => ({\n focus,\n }))\n\n useEffect(() => {\n // FocusTrap がマウントされた時点のフォーカス要素を保存\n const triggerElement = document.activeElement\n\n focus()\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。\n // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても\n // 変換候補が確定されず、未確定文字列がそのまま入力されてしまう。\n if (e.key !== 'Tab' || e.isComposing || innerRef.current === null) {\n return\n }\n\n const tabbables = tabbable(innerRef.current).filter((elm) => elm.tabIndex >= 0)\n\n if (tabbables.length === 0) {\n return\n }\n\n const firstTabbable = tabbables[0]\n const lastTabbable = tabbables[tabbables.length - 1]\n const currentFocused = tabbables.find((elm) => elm === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstTabbable || document.activeElement === dummyFocusRef.current) {\n e.preventDefault()\n lastTabbable.focus()\n }\n } else if (currentFocused === lastTabbable) {\n e.preventDefault()\n firstTabbable.focus()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n // フォーカストラップ終了時にトリガにフォーカスを戻す\n if (triggerElement instanceof HTMLElement) {\n triggerElement.focus()\n }\n }\n }, [focus])\n\n return (\n <div ref={innerRef}>\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div ref={dummyFocusRef} tabIndex={-1} />\n {children}\n </div>\n )\n})\n"],"names":["forwardRef","useRef","useCallback","useImperativeHandle","useEffect","tabbable","_jsxs","_jsx"],"mappings":";;;;;;AAoBO,MAAM,SAAS,GAAGA,gBAAU,CAAsB,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAI;AAC/F,IAAA,MAAM,QAAQ,GAAGC,YAAM,CAAwB,IAAI,CAAC;AACpD,IAAA,MAAM,aAAa,GAAGA,YAAM,CAAiB,IAAI,CAAC;AAElD,IAAA,MAAM,KAAK,GAAGC,iBAAW,CAAC,MAAK;QAC5B,CAAC,gBAAgB,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,GAAG,KAAK,EAAE;AAChE,IAAA,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;AAEtB,IAAAC,yBAAmB,CAAC,GAAG,EAAE,OAAO;QAC9B,KAAK;AACN,KAAA,CAAC,CAAC;IAEHC,eAAS,CAAC,MAAK;;AAEb,QAAA,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa;AAE7C,QAAA,KAAK,EAAE;AAEP,QAAA,MAAM,aAAa,GAAG,CAAC,CAAgB,KAAI;;;;AAIzC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE;gBACjE;YACF;YAEA,MAAM,SAAS,GAAGC,sBAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC;AAE/E,YAAA,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE;gBAC1B;YACF;AAEA,YAAA,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;YAClC,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;AACpD,YAAA,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;AAEhE,YAAA,IAAI,CAAC,CAAC,QAAQ,EAAE;AACd,gBAAA,IAAI,cAAc,KAAK,aAAa,IAAI,QAAQ,CAAC,aAAa,KAAK,aAAa,CAAC,OAAO,EAAE;oBACxF,CAAC,CAAC,cAAc,EAAE;oBAClB,YAAY,CAAC,KAAK,EAAE;gBACtB;YACF;AAAO,iBAAA,IAAI,cAAc,KAAK,YAAY,EAAE;gBAC1C,CAAC,CAAC,cAAc,EAAE;gBAClB,aAAa,CAAC,KAAK,EAAE;YACvB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC;AAEjD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC;;AAEpD,YAAA,IAAI,cAAc,YAAY,WAAW,EAAE;gBACzC,cAAc,CAAC,KAAK,EAAE;YACxB;AACF,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAEX,QACEC,yBAAK,GAAG,EAAE,QAAQ,EAAA,QAAA,EAAA,CAEhBC,cAAA,CAAA,KAAA,EAAA,EAAK,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAA,CAAI,EACxC,QAAQ,CAAA,EAAA,CACL;AAEV,CAAC;;;;"}
1
+ {"version":3,"file":"FocusTrap.cjs","sources":["../../../src/components/Dialog/FocusTrap.tsx"],"sourcesContent":["import {\n type PropsWithChildren,\n type RefObject,\n forwardRef,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n} from 'react'\n\nimport { tabbable } from '../../libs/tabbable'\n\ntype Props = PropsWithChildren<{\n firstFocusTarget?: RefObject<HTMLElement>\n}>\n\nexport type FocusTrapRef = {\n focus: () => void\n}\n\nexport const FocusTrap = forwardRef<FocusTrapRef, Props>(({ firstFocusTarget, children }, ref) => {\n const innerRef = useRef<HTMLDivElement | null>(null)\n const dummyFocusRef = useRef<HTMLDivElement>(null)\n\n const functions = useMemo(\n () => ({\n focus: () => {\n ;(firstFocusTarget?.current || dummyFocusRef.current)?.focus()\n },\n }),\n [firstFocusTarget],\n )\n\n useImperativeHandle(ref, () => functions, [functions])\n\n useEffect(() => {\n // FocusTrap がマウントされた時点のフォーカス要素を保存\n const triggerElement = document.activeElement\n\n functions.focus()\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。\n // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても\n // 変換候補が確定されず、未確定文字列がそのまま入力されてしまう。\n if (e.key !== 'Tab' || e.isComposing || innerRef.current === null) {\n return\n }\n\n const tabbables = tabbable(innerRef.current).filter((elm) => elm.tabIndex >= 0)\n\n if (tabbables.length === 0) {\n return\n }\n\n const firstTabbable = tabbables[0]\n const lastTabbable = tabbables[tabbables.length - 1]\n const currentFocused = tabbables.find((elm) => elm === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstTabbable || document.activeElement === dummyFocusRef.current) {\n e.preventDefault()\n lastTabbable.focus()\n }\n } else if (currentFocused === lastTabbable) {\n e.preventDefault()\n firstTabbable.focus()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n // フォーカストラップ終了時にトリガにフォーカスを戻す\n if (triggerElement instanceof HTMLElement) {\n triggerElement.focus()\n }\n }\n }, [functions])\n\n return (\n <div ref={innerRef}>\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div ref={dummyFocusRef} tabIndex={-1} />\n {children}\n </div>\n )\n})\n"],"names":["forwardRef","useRef","useMemo","useImperativeHandle","useEffect","tabbable","_jsxs","_jsx"],"mappings":";;;;;;AAoBO,MAAM,SAAS,GAAGA,gBAAU,CAAsB,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAI;AAC/F,IAAA,MAAM,QAAQ,GAAGC,YAAM,CAAwB,IAAI,CAAC;AACpD,IAAA,MAAM,aAAa,GAAGA,YAAM,CAAiB,IAAI,CAAC;AAElD,IAAA,MAAM,SAAS,GAAGC,aAAO,CACvB,OAAO;QACL,KAAK,EAAE,MAAK;YACT,CAAC,gBAAgB,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,GAAG,KAAK,EAAE;QAChE,CAAC;AACF,KAAA,CAAC,EACF,CAAC,gBAAgB,CAAC,CACnB;AAED,IAAAC,yBAAmB,CAAC,GAAG,EAAE,MAAM,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC;IAEtDC,eAAS,CAAC,MAAK;;AAEb,QAAA,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa;QAE7C,SAAS,CAAC,KAAK,EAAE;AAEjB,QAAA,MAAM,aAAa,GAAG,CAAC,CAAgB,KAAI;;;;AAIzC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE;gBACjE;YACF;YAEA,MAAM,SAAS,GAAGC,sBAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC;AAE/E,YAAA,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE;gBAC1B;YACF;AAEA,YAAA,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;YAClC,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;AACpD,YAAA,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;AAEhE,YAAA,IAAI,CAAC,CAAC,QAAQ,EAAE;AACd,gBAAA,IAAI,cAAc,KAAK,aAAa,IAAI,QAAQ,CAAC,aAAa,KAAK,aAAa,CAAC,OAAO,EAAE;oBACxF,CAAC,CAAC,cAAc,EAAE;oBAClB,YAAY,CAAC,KAAK,EAAE;gBACtB;YACF;AAAO,iBAAA,IAAI,cAAc,KAAK,YAAY,EAAE;gBAC1C,CAAC,CAAC,cAAc,EAAE;gBAClB,aAAa,CAAC,KAAK,EAAE;YACvB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC;AAEjD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC;;AAEpD,YAAA,IAAI,cAAc,YAAY,WAAW,EAAE;gBACzC,cAAc,CAAC,KAAK,EAAE;YACxB;AACF,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;IAEf,QACEC,yBAAK,GAAG,EAAE,QAAQ,EAAA,QAAA,EAAA,CAEhBC,cAAA,CAAA,KAAA,EAAA,EAAK,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAA,CAAI,EACxC,QAAQ,CAAA,EAAA,CACL;AAEV,CAAC;;;;"}
@@ -1,20 +1,20 @@
1
1
  import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { forwardRef, useRef, useCallback, useImperativeHandle, useEffect } from 'react';
2
+ import { forwardRef, useRef, useMemo, useImperativeHandle, useEffect } from 'react';
3
3
  import { tabbable } from '../../libs/tabbable.js';
4
4
 
5
5
  const FocusTrap = forwardRef(({ firstFocusTarget, children }, ref) => {
6
6
  const innerRef = useRef(null);
7
7
  const dummyFocusRef = useRef(null);
8
- const focus = useCallback(() => {
9
- (firstFocusTarget?.current || dummyFocusRef.current)?.focus();
10
- }, [firstFocusTarget]);
11
- useImperativeHandle(ref, () => ({
12
- focus,
13
- }));
8
+ const functions = useMemo(() => ({
9
+ focus: () => {
10
+ (firstFocusTarget?.current || dummyFocusRef.current)?.focus();
11
+ },
12
+ }), [firstFocusTarget]);
13
+ useImperativeHandle(ref, () => functions, [functions]);
14
14
  useEffect(() => {
15
15
  // FocusTrap がマウントされた時点のフォーカス要素を保存
16
16
  const triggerElement = document.activeElement;
17
- focus();
17
+ functions.focus();
18
18
  const handleKeyDown = (e) => {
19
19
  // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。
20
20
  // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても
@@ -48,7 +48,7 @@ const FocusTrap = forwardRef(({ firstFocusTarget, children }, ref) => {
48
48
  triggerElement.focus();
49
49
  }
50
50
  };
51
- }, [focus]);
51
+ }, [functions]);
52
52
  return (jsxs("div", { ref: innerRef, children: [jsx("div", { ref: dummyFocusRef, tabIndex: -1 }), children] }));
53
53
  });
54
54
 
@@ -1 +1 @@
1
- {"version":3,"file":"FocusTrap.js","sources":["../../../src/components/Dialog/FocusTrap.tsx"],"sourcesContent":["import {\n type PropsWithChildren,\n type RefObject,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react'\n\nimport { tabbable } from '../../libs/tabbable'\n\ntype Props = PropsWithChildren<{\n firstFocusTarget?: RefObject<HTMLElement>\n}>\n\nexport type FocusTrapRef = {\n focus: () => void\n}\n\nexport const FocusTrap = forwardRef<FocusTrapRef, Props>(({ firstFocusTarget, children }, ref) => {\n const innerRef = useRef<HTMLDivElement | null>(null)\n const dummyFocusRef = useRef<HTMLDivElement>(null)\n\n const focus = useCallback(() => {\n ;(firstFocusTarget?.current || dummyFocusRef.current)?.focus()\n }, [firstFocusTarget])\n\n useImperativeHandle(ref, () => ({\n focus,\n }))\n\n useEffect(() => {\n // FocusTrap がマウントされた時点のフォーカス要素を保存\n const triggerElement = document.activeElement\n\n focus()\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。\n // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても\n // 変換候補が確定されず、未確定文字列がそのまま入力されてしまう。\n if (e.key !== 'Tab' || e.isComposing || innerRef.current === null) {\n return\n }\n\n const tabbables = tabbable(innerRef.current).filter((elm) => elm.tabIndex >= 0)\n\n if (tabbables.length === 0) {\n return\n }\n\n const firstTabbable = tabbables[0]\n const lastTabbable = tabbables[tabbables.length - 1]\n const currentFocused = tabbables.find((elm) => elm === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstTabbable || document.activeElement === dummyFocusRef.current) {\n e.preventDefault()\n lastTabbable.focus()\n }\n } else if (currentFocused === lastTabbable) {\n e.preventDefault()\n firstTabbable.focus()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n // フォーカストラップ終了時にトリガにフォーカスを戻す\n if (triggerElement instanceof HTMLElement) {\n triggerElement.focus()\n }\n }\n }, [focus])\n\n return (\n <div ref={innerRef}>\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div ref={dummyFocusRef} tabIndex={-1} />\n {children}\n </div>\n )\n})\n"],"names":["_jsxs","_jsx"],"mappings":";;;;AAoBO,MAAM,SAAS,GAAG,UAAU,CAAsB,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAI;AAC/F,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAwB,IAAI,CAAC;AACpD,IAAA,MAAM,aAAa,GAAG,MAAM,CAAiB,IAAI,CAAC;AAElD,IAAA,MAAM,KAAK,GAAG,WAAW,CAAC,MAAK;QAC5B,CAAC,gBAAgB,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,GAAG,KAAK,EAAE;AAChE,IAAA,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC;AAEtB,IAAA,mBAAmB,CAAC,GAAG,EAAE,OAAO;QAC9B,KAAK;AACN,KAAA,CAAC,CAAC;IAEH,SAAS,CAAC,MAAK;;AAEb,QAAA,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa;AAE7C,QAAA,KAAK,EAAE;AAEP,QAAA,MAAM,aAAa,GAAG,CAAC,CAAgB,KAAI;;;;AAIzC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE;gBACjE;YACF;YAEA,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC;AAE/E,YAAA,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE;gBAC1B;YACF;AAEA,YAAA,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;YAClC,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;AACpD,YAAA,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;AAEhE,YAAA,IAAI,CAAC,CAAC,QAAQ,EAAE;AACd,gBAAA,IAAI,cAAc,KAAK,aAAa,IAAI,QAAQ,CAAC,aAAa,KAAK,aAAa,CAAC,OAAO,EAAE;oBACxF,CAAC,CAAC,cAAc,EAAE;oBAClB,YAAY,CAAC,KAAK,EAAE;gBACtB;YACF;AAAO,iBAAA,IAAI,cAAc,KAAK,YAAY,EAAE;gBAC1C,CAAC,CAAC,cAAc,EAAE;gBAClB,aAAa,CAAC,KAAK,EAAE;YACvB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC;AAEjD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC;;AAEpD,YAAA,IAAI,cAAc,YAAY,WAAW,EAAE;gBACzC,cAAc,CAAC,KAAK,EAAE;YACxB;AACF,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IAEX,QACEA,cAAK,GAAG,EAAE,QAAQ,EAAA,QAAA,EAAA,CAEhBC,GAAA,CAAA,KAAA,EAAA,EAAK,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAA,CAAI,EACxC,QAAQ,CAAA,EAAA,CACL;AAEV,CAAC;;;;"}
1
+ {"version":3,"file":"FocusTrap.js","sources":["../../../src/components/Dialog/FocusTrap.tsx"],"sourcesContent":["import {\n type PropsWithChildren,\n type RefObject,\n forwardRef,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n} from 'react'\n\nimport { tabbable } from '../../libs/tabbable'\n\ntype Props = PropsWithChildren<{\n firstFocusTarget?: RefObject<HTMLElement>\n}>\n\nexport type FocusTrapRef = {\n focus: () => void\n}\n\nexport const FocusTrap = forwardRef<FocusTrapRef, Props>(({ firstFocusTarget, children }, ref) => {\n const innerRef = useRef<HTMLDivElement | null>(null)\n const dummyFocusRef = useRef<HTMLDivElement>(null)\n\n const functions = useMemo(\n () => ({\n focus: () => {\n ;(firstFocusTarget?.current || dummyFocusRef.current)?.focus()\n },\n }),\n [firstFocusTarget],\n )\n\n useImperativeHandle(ref, () => functions, [functions])\n\n useEffect(() => {\n // FocusTrap がマウントされた時点のフォーカス要素を保存\n const triggerElement = document.activeElement\n\n functions.focus()\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // IME 変換中の Tab は変換候補の選択に使われるため、フォーカストラップの対象外にする。\n // ここで preventDefault してしまうと、Dialog 内で日本語入力中に Tab を押しても\n // 変換候補が確定されず、未確定文字列がそのまま入力されてしまう。\n if (e.key !== 'Tab' || e.isComposing || innerRef.current === null) {\n return\n }\n\n const tabbables = tabbable(innerRef.current).filter((elm) => elm.tabIndex >= 0)\n\n if (tabbables.length === 0) {\n return\n }\n\n const firstTabbable = tabbables[0]\n const lastTabbable = tabbables[tabbables.length - 1]\n const currentFocused = tabbables.find((elm) => elm === e.target)\n\n if (e.shiftKey) {\n if (currentFocused === firstTabbable || document.activeElement === dummyFocusRef.current) {\n e.preventDefault()\n lastTabbable.focus()\n }\n } else if (currentFocused === lastTabbable) {\n e.preventDefault()\n firstTabbable.focus()\n }\n }\n\n window.addEventListener('keydown', handleKeyDown)\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown)\n // フォーカストラップ終了時にトリガにフォーカスを戻す\n if (triggerElement instanceof HTMLElement) {\n triggerElement.focus()\n }\n }\n }, [functions])\n\n return (\n <div ref={innerRef}>\n {/* eslint-disable-next-line smarthr/a11y-scroller-has-tabindex -- dummy element for focus management. */}\n <div ref={dummyFocusRef} tabIndex={-1} />\n {children}\n </div>\n )\n})\n"],"names":["_jsxs","_jsx"],"mappings":";;;;AAoBO,MAAM,SAAS,GAAG,UAAU,CAAsB,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAI;AAC/F,IAAA,MAAM,QAAQ,GAAG,MAAM,CAAwB,IAAI,CAAC;AACpD,IAAA,MAAM,aAAa,GAAG,MAAM,CAAiB,IAAI,CAAC;AAElD,IAAA,MAAM,SAAS,GAAG,OAAO,CACvB,OAAO;QACL,KAAK,EAAE,MAAK;YACT,CAAC,gBAAgB,EAAE,OAAO,IAAI,aAAa,CAAC,OAAO,GAAG,KAAK,EAAE;QAChE,CAAC;AACF,KAAA,CAAC,EACF,CAAC,gBAAgB,CAAC,CACnB;AAED,IAAA,mBAAmB,CAAC,GAAG,EAAE,MAAM,SAAS,EAAE,CAAC,SAAS,CAAC,CAAC;IAEtD,SAAS,CAAC,MAAK;;AAEb,QAAA,MAAM,cAAc,GAAG,QAAQ,CAAC,aAAa;QAE7C,SAAS,CAAC,KAAK,EAAE;AAEjB,QAAA,MAAM,aAAa,GAAG,CAAC,CAAgB,KAAI;;;;AAIzC,YAAA,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,QAAQ,CAAC,OAAO,KAAK,IAAI,EAAE;gBACjE;YACF;YAEA,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC;AAE/E,YAAA,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE;gBAC1B;YACF;AAEA,YAAA,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC;YAClC,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;AACpD,YAAA,MAAM,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;AAEhE,YAAA,IAAI,CAAC,CAAC,QAAQ,EAAE;AACd,gBAAA,IAAI,cAAc,KAAK,aAAa,IAAI,QAAQ,CAAC,aAAa,KAAK,aAAa,CAAC,OAAO,EAAE;oBACxF,CAAC,CAAC,cAAc,EAAE;oBAClB,YAAY,CAAC,KAAK,EAAE;gBACtB;YACF;AAAO,iBAAA,IAAI,cAAc,KAAK,YAAY,EAAE;gBAC1C,CAAC,CAAC,cAAc,EAAE;gBAClB,aAAa,CAAC,KAAK,EAAE;YACvB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,aAAa,CAAC;AAEjD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,aAAa,CAAC;;AAEpD,YAAA,IAAI,cAAc,YAAY,WAAW,EAAE;gBACzC,cAAc,CAAC,KAAK,EAAE;YACxB;AACF,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC;IAEf,QACEA,cAAK,GAAG,EAAE,QAAQ,EAAA,QAAA,EAAA,CAEhBC,GAAA,CAAA,KAAA,EAAA,EAAK,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAE,EAAA,CAAI,EACxC,QAAQ,CAAA,EAAA,CACL;AAEV,CAAC;;;;"}
@@ -171,7 +171,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
171
171
  React.useEffect(() => {
172
172
  if (!wrapperPosition) {
173
173
  setDebouncedLiveRegionText('');
174
- return;
174
+ return functions.debounceLiveRegionText.cancel;
175
175
  }
176
176
  const txt = localize({
177
177
  id: 'smarthr-ui/ModelessDialog/dialogHandlerLiveRegionText',
@@ -181,6 +181,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
181
181
  left: Math.trunc(wrapperPosition.left).toString(),
182
182
  });
183
183
  functions.debounceLiveRegionText(txt);
184
+ return functions.debounceLiveRegionText.cancel;
184
185
  }, [localize, wrapperPosition, functions]);
185
186
  React.useEffect(() => {
186
187
  if (wrapperRef.current instanceof Element) {
@@ -1 +1 @@
1
- {"version":3,"file":"ModelessDialog.cjs","sources":["../../../../src/components/Dialog/ModelessDialog/ModelessDialog.tsx"],"sourcesContent":["'use client'\n\nimport {\n type FC,\n type KeyboardEvent,\n type MouseEvent,\n type PropsWithChildren,\n type ReactNode,\n type RefObject,\n memo,\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 { useLatest } from '../../../hooks/useLatest'\nimport { Localizer, useIntl } from '../../../intl'\nimport { debounce } from '../../../libs/debounce'\nimport { dialogSize } from '../../../tailwind'\nimport { Button } from '../../Button'\nimport { Heading } from '../../Heading'\nimport { FaGripIcon, FaXmarkIcon } from '../../Icon'\nimport { Panel, type PanelElementProps } from '../../Panel'\nimport { DialogBody, type Props as DialogBodyProps } from '../DialogBody'\nimport { DialogOverlap } from '../DialogOverlap'\nimport { useDialogPortal } from '../useDialogPortal'\n\nimport type { DialogSize } from '../types'\n\ntype BaseProps = 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 = BaseProps &\n Omit<DialogBodyProps, keyof BaseProps> &\n Omit<PanelElementProps, keyof BaseProps> &\n Omit<VariantProps<typeof classNameGenerator>, keyof BaseProps>\n\nconst classNameGenerator = tv({\n slots: {\n overlap: 'shr-inset-[unset]',\n wrapper:\n 'smarthr-ui-ModelessDialog shr-fixed shr-flex shr-max-h-[calc(100svh-theme(spacing[0.5]))] shr-max-w-[calc(100vw-theme(spacing[0.5]))] 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] = useState(Draggable.defaultProps.bounds)\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 latest = useLatest({ isOpen, onClickClose, onPressEscape })\n\n const functions = useMemo(\n () => ({\n debounceLiveRegionText: debounce(setDebouncedLiveRegionText, 600),\n handleArrowKeyDown: (e: KeyboardEvent) => {\n if (!latest.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 handleClickClose: (e: MouseEvent<HTMLButtonElement>) => {\n lastFocusElementRef.current?.focus()\n latest.onClickClose?.(e)\n },\n handlePressEscape: () => {\n lastFocusElementRef.current?.focus()\n latest.onPressEscape?.()\n },\n handleDragStart: (_: any, data: { x: number; y: number }) => setPosition(data),\n handleDrag: (_: 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 [latest],\n )\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 functions.debounceLiveRegionText(txt)\n }, [localize, wrapperPosition, functions])\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 ? Math.max(0, window.innerHeight / 2 - rect.height / 2) : undefined,\n left: isXCenter ? Math.max(0, 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 useHandleEscape(isOpen ? functions.handlePressEscape : 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 return createPortal(\n <DialogOverlap isOpen={isOpen} className={classNames.overlap} as=\"section\">\n <Draggable\n {...Draggable.defaultProps}\n handle=\".smarthr-ui-ModelessDialog-handle\"\n onStart={functions.handleDragStart}\n onDrag={functions.handleDrag}\n position={position}\n bounds={draggableBounds ?? false}\n nodeRef={wrapperRef}\n >\n <Panel\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\n handleArrowKeyDown={functions.handleArrowKeyDown}\n className={classNames.dialogHandler}\n />\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\n handleClick={functions.handleClickClose}\n className={classNames.closeButtonLayout}\n />\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 </Panel>\n </Draggable>\n </DialogOverlap>,\n )\n}\n\nconst Handler = memo<{\n className: string\n handleArrowKeyDown: (e: KeyboardEvent) => void\n}>(({ handleArrowKeyDown, ...rest }) => {\n const { localize } = useIntl()\n\n return (\n <>\n <button\n {...rest}\n type=\"button\"\n aria-label={localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaLabel',\n defaultText: 'ダイアログの位置',\n })}\n aria-roledescription={localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaRoleDescription',\n defaultText: 'ドラッグ可能',\n })}\n aria-describedby=\"handler-description\"\n onKeyDown={handleArrowKeyDown}\n >\n <FaGripIcon />\n </button>\n <div className=\"shr-hidden\" id=\"handler-description\">\n {localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerDescription',\n defaultText: '矢印キーを押して上下左右に移動できます',\n })}\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 handleClick: (e: MouseEvent<HTMLButtonElement>) => void\n}>(({ handleClick, className }) => (\n <div className={className}>\n <Button\n type=\"button\"\n size=\"S\"\n onClick={handleClick}\n className=\"smarthr-ui-ModelessDialog-closeButton\"\n >\n <FaXmarkIcon\n alt={<Localizer id=\"smarthr-ui/ModelessDialog/closeButtonIconAlt\" defaultText=\"閉じる\" />}\n />\n </Button>\n </div>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FA;AACE;AACE;AACA;AAEA;;;;;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;AACD;AAEA;AAEI;AACA;;;;;AAKD;AAIH;AAEA;AAEI;AACA;AACE;;;;AAMA;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;;AAKP;AACE;AACA;;;AAGA;AACA;;;AAGF;AACE;AACE;AACA;AACD;;AAEJ;;;;;;;AAYG;AACA;;;;AAKD;AAGH;;;AAIA;;;AAGF;;;;;;;;AAWE;;AAGE;;;AAGC;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;;AAGE;;AAEE;AACE;;AAEJ;;AAIA;;;AAsDJ;AAEA;AAIE;;AAQQ;AACA;;AAGA;AACA;;AASA;AACA;;AAKV;AAEA;AASA;;"}
1
+ {"version":3,"file":"ModelessDialog.cjs","sources":["../../../../src/components/Dialog/ModelessDialog/ModelessDialog.tsx"],"sourcesContent":["'use client'\n\nimport {\n type FC,\n type KeyboardEvent,\n type MouseEvent,\n type PropsWithChildren,\n type ReactNode,\n type RefObject,\n memo,\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 { useLatest } from '../../../hooks/useLatest'\nimport { Localizer, useIntl } from '../../../intl'\nimport { debounce } from '../../../libs/debounce'\nimport { dialogSize } from '../../../tailwind'\nimport { Button } from '../../Button'\nimport { Heading } from '../../Heading'\nimport { FaGripIcon, FaXmarkIcon } from '../../Icon'\nimport { Panel, type PanelElementProps } from '../../Panel'\nimport { DialogBody, type Props as DialogBodyProps } from '../DialogBody'\nimport { DialogOverlap } from '../DialogOverlap'\nimport { useDialogPortal } from '../useDialogPortal'\n\nimport type { DialogSize } from '../types'\n\ntype BaseProps = 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 = BaseProps &\n Omit<DialogBodyProps, keyof BaseProps> &\n Omit<PanelElementProps, keyof BaseProps> &\n Omit<VariantProps<typeof classNameGenerator>, keyof BaseProps>\n\nconst classNameGenerator = tv({\n slots: {\n overlap: 'shr-inset-[unset]',\n wrapper:\n 'smarthr-ui-ModelessDialog shr-fixed shr-flex shr-max-h-[calc(100svh-theme(spacing[0.5]))] shr-max-w-[calc(100vw-theme(spacing[0.5]))] 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] = useState(Draggable.defaultProps.bounds)\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 latest = useLatest({ isOpen, onClickClose, onPressEscape })\n\n const functions = useMemo(\n () => ({\n debounceLiveRegionText: debounce(setDebouncedLiveRegionText, 600),\n handleArrowKeyDown: (e: KeyboardEvent) => {\n if (!latest.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 handleClickClose: (e: MouseEvent<HTMLButtonElement>) => {\n lastFocusElementRef.current?.focus()\n latest.onClickClose?.(e)\n },\n handlePressEscape: () => {\n lastFocusElementRef.current?.focus()\n latest.onPressEscape?.()\n },\n handleDragStart: (_: any, data: { x: number; y: number }) => setPosition(data),\n handleDrag: (_: 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 [latest],\n )\n\n useEffect(() => {\n if (!wrapperPosition) {\n setDebouncedLiveRegionText('')\n return functions.debounceLiveRegionText.cancel\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 functions.debounceLiveRegionText(txt)\n\n return functions.debounceLiveRegionText.cancel\n }, [localize, wrapperPosition, functions])\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 ? Math.max(0, window.innerHeight / 2 - rect.height / 2) : undefined,\n left: isXCenter ? Math.max(0, 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 useHandleEscape(isOpen ? functions.handlePressEscape : 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 return createPortal(\n <DialogOverlap isOpen={isOpen} className={classNames.overlap} as=\"section\">\n <Draggable\n {...Draggable.defaultProps}\n handle=\".smarthr-ui-ModelessDialog-handle\"\n onStart={functions.handleDragStart}\n onDrag={functions.handleDrag}\n position={position}\n bounds={draggableBounds ?? false}\n nodeRef={wrapperRef}\n >\n <Panel\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\n handleArrowKeyDown={functions.handleArrowKeyDown}\n className={classNames.dialogHandler}\n />\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\n handleClick={functions.handleClickClose}\n className={classNames.closeButtonLayout}\n />\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 </Panel>\n </Draggable>\n </DialogOverlap>,\n )\n}\n\nconst Handler = memo<{\n className: string\n handleArrowKeyDown: (e: KeyboardEvent) => void\n}>(({ handleArrowKeyDown, ...rest }) => {\n const { localize } = useIntl()\n\n return (\n <>\n <button\n {...rest}\n type=\"button\"\n aria-label={localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaLabel',\n defaultText: 'ダイアログの位置',\n })}\n aria-roledescription={localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerAriaRoleDescription',\n defaultText: 'ドラッグ可能',\n })}\n aria-describedby=\"handler-description\"\n onKeyDown={handleArrowKeyDown}\n >\n <FaGripIcon />\n </button>\n <div className=\"shr-hidden\" id=\"handler-description\">\n {localize({\n id: 'smarthr-ui/ModelessDialog/dialogHandlerDescription',\n defaultText: '矢印キーを押して上下左右に移動できます',\n })}\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 handleClick: (e: MouseEvent<HTMLButtonElement>) => void\n}>(({ handleClick, className }) => (\n <div className={className}>\n <Button\n type=\"button\"\n size=\"S\"\n onClick={handleClick}\n className=\"smarthr-ui-ModelessDialog-closeButton\"\n >\n <FaXmarkIcon\n alt={<Localizer id=\"smarthr-ui/ModelessDialog/closeButtonIconAlt\" defaultText=\"閉じる\" />}\n />\n </Button>\n </div>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FA;AACE;AACE;AACA;AAEA;;;;;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;AACD;AAEA;AAEI;AACA;;;;;AAKD;AAIH;AAEA;AAEI;AACA;AACE;;;;AAMA;AACE;AACE;;AAEE;AACD;;;AAGH;AACE;;AAEE;AACD;;;AAGH;AACE;AACE;;AAED;;;AAGH;AACE;AACE;;AAED;;;;;AAKP;AACE;AACA;;;AAGA;AACA;;;AAGF;AACE;AACE;AACA;AACD;;AAEJ;;;;AAOC;;;AAKE;AACA;;;;AAKD;AAGH;AAEA;;;AAIA;;;AAGF;;;;;;;;AAWE;;AAGE;;;AAGC;;AAEL;;AAGE;;AAEA;AACE;;;AAKF;;AAGE;;;;;;AAOA;;AAEJ;AAEA;;AAGE;;AAEE;AACE;;AAEJ;;AAIA;;;AAsDJ;AAEA;AAIE;;AAQQ;AACA;;AAGA;AACA;;AASA;AACA;;AAKV;AAEA;AASA;;"}
@@ -169,7 +169,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
169
169
  useEffect(() => {
170
170
  if (!wrapperPosition) {
171
171
  setDebouncedLiveRegionText('');
172
- return;
172
+ return functions.debounceLiveRegionText.cancel;
173
173
  }
174
174
  const txt = localize({
175
175
  id: 'smarthr-ui/ModelessDialog/dialogHandlerLiveRegionText',
@@ -179,6 +179,7 @@ const ModelessDialog = ({ heading, children, contentBgColor, contentPadding, foo
179
179
  left: Math.trunc(wrapperPosition.left).toString(),
180
180
  });
181
181
  functions.debounceLiveRegionText(txt);
182
+ return functions.debounceLiveRegionText.cancel;
182
183
  }, [localize, wrapperPosition, functions]);
183
184
  useEffect(() => {
184
185
  if (wrapperRef.current instanceof Element) {