menu-bar-dropdown.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import {
  2. Dropdown,
  3. DropdownMenu,
  4. DropdownToggle,
  5. } from '@/features/ui/components/bootstrap-5/dropdown-menu'
  6. import { FC, forwardRef, useCallback } from 'react'
  7. import classNames from 'classnames'
  8. import { useNestableDropdown } from '@/shared/hooks/use-nestable-dropdown'
  9. import { NestableDropdownContextProvider } from '@/shared/context/nestable-dropdown-context'
  10. import { AnchorProps } from 'react-bootstrap'
  11. import MaterialIcon from '../material-icon'
  12. import { DropdownMenuProps } from '@/features/ui/components/types/dropdown-menu-props'
  13. type MenuBarDropdownProps = {
  14. title: string
  15. id: string
  16. className?: string
  17. align?: 'start' | 'end'
  18. }
  19. export const MenuBarDropdown: FC<
  20. React.PropsWithChildren<MenuBarDropdownProps>
  21. > = ({ title, children, id, className, align = 'start' }) => {
  22. const { menuId, selected, setSelected } = useNestableDropdown()
  23. const onToggle = useCallback(
  24. (show: boolean) => {
  25. setSelected(show ? id : null)
  26. },
  27. [id, setSelected]
  28. )
  29. const onHover = useCallback(() => {
  30. setSelected(prev => {
  31. if (prev === null) {
  32. return null
  33. }
  34. return id
  35. })
  36. }, [id, setSelected])
  37. return (
  38. <Dropdown
  39. show={selected === id}
  40. align={align}
  41. onToggle={onToggle}
  42. autoClose
  43. >
  44. <DropdownToggle
  45. id={`${menuId}-${id}`}
  46. variant="secondary"
  47. className={classNames(className, 'menu-bar-toggle')}
  48. onMouseEnter={onHover}
  49. >
  50. {title}
  51. </DropdownToggle>
  52. <NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
  53. {children}
  54. </NestableDropdownMenu>
  55. </Dropdown>
  56. )
  57. }
  58. const NestableDropdownMenu: FC<
  59. React.PropsWithChildren<DropdownMenuProps & { id: string }>
  60. > = ({ children, id, ...props }) => {
  61. return (
  62. <DropdownMenu {...props}>
  63. <NestableDropdownContextProvider id={id}>
  64. {children}
  65. </NestableDropdownContextProvider>
  66. </DropdownMenu>
  67. )
  68. }
  69. const NestedDropdownToggle: FC<React.PropsWithChildren> = forwardRef<
  70. HTMLAnchorElement,
  71. AnchorProps
  72. >(function NestedDropdownToggle(
  73. { children, className, onMouseEnter, id },
  74. ref
  75. ) {
  76. return (
  77. // eslint-disable-next-line jsx-a11y/anchor-is-valid
  78. <a
  79. id={id}
  80. href="#"
  81. ref={ref}
  82. onMouseEnter={onMouseEnter}
  83. onClick={onMouseEnter}
  84. className={classNames(
  85. className,
  86. 'nested-dropdown-toggle',
  87. 'dropdown-item'
  88. )}
  89. >
  90. {children}
  91. <MaterialIcon type="chevron_right" />
  92. </a>
  93. )
  94. })
  95. export const NestedMenuBarDropdown: FC<
  96. React.PropsWithChildren<{ id: string; title: string }>
  97. > = ({ children, id, title }) => {
  98. const { menuId, selected, setSelected } = useNestableDropdown()
  99. const select = useCallback(() => {
  100. setSelected(id)
  101. }, [id, setSelected])
  102. const onToggle = useCallback(
  103. (show: boolean) => {
  104. setSelected(show ? id : null)
  105. },
  106. [setSelected, id]
  107. )
  108. const active = selected === id
  109. return (
  110. <Dropdown
  111. align="start"
  112. drop="end"
  113. show={active}
  114. autoClose
  115. onToggle={onToggle}
  116. >
  117. <DropdownToggle
  118. id={`${menuId}-${id}`}
  119. onMouseEnter={select}
  120. className={classNames({ 'nested-dropdown-toggle-shown': active })}
  121. as={NestedDropdownToggle}
  122. >
  123. {title}
  124. </DropdownToggle>
  125. <NestableDropdownMenu renderOnMount id={`${menuId}-${id}`}>
  126. {children}
  127. </NestableDropdownMenu>
  128. </Dropdown>
  129. )
  130. }