button-menu.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { FC, memo, useEffect, useRef } from 'react'
  2. import useDropdown from '../../../../shared/hooks/use-dropdown'
  3. import OLListGroup from '@/shared/components/ol/ol-list-group'
  4. import OLTooltip from '@/shared/components/ol/ol-tooltip'
  5. import OLOverlay from '@/shared/components/ol/ol-overlay'
  6. import OLPopover from '@/shared/components/ol/ol-popover'
  7. import { EditorView } from '@codemirror/view'
  8. import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
  9. import { useCodeMirrorViewContext } from '../codemirror-context'
  10. import classNames from 'classnames'
  11. export const ToolbarButtonMenu: FC<
  12. React.PropsWithChildren<{
  13. id: string
  14. label: string
  15. icon: React.ReactNode
  16. orientation?: 'vertical' | 'horizontal'
  17. className?: string
  18. disabled?: boolean
  19. disablePopover?: boolean
  20. altCommand?: (view: EditorView) => void
  21. onToggle?: (isOpen: boolean) => void
  22. }>
  23. > = memo(function ButtonMenu({
  24. icon,
  25. id,
  26. label,
  27. orientation = 'vertical',
  28. className,
  29. altCommand,
  30. onToggle,
  31. disabled,
  32. disablePopover,
  33. children,
  34. }) {
  35. const target = useRef<any>(null)
  36. const { open, onToggle: handleToggle, ref } = useDropdown()
  37. const view = useCodeMirrorViewContext()
  38. useEffect(() => {
  39. if (!open) return
  40. const onResize = () => {
  41. handleToggle(false)
  42. }
  43. window.addEventListener('resize', onResize)
  44. return () => {
  45. window.removeEventListener('resize', onResize)
  46. }
  47. }, [open, handleToggle])
  48. useEffect(() => {
  49. if (disablePopover && open) {
  50. handleToggle(false)
  51. }
  52. }, [open, disablePopover, handleToggle])
  53. useEffect(() => {
  54. onToggle?.(open)
  55. }, [open, onToggle])
  56. const button = (
  57. <button
  58. type="button"
  59. className={classNames('ol-cm-toolbar-button', className)}
  60. aria-label={label}
  61. aria-disabled={disabled}
  62. aria-haspopup="menu"
  63. aria-expanded={open}
  64. aria-controls={`${id}-menu`}
  65. onMouseDown={event => {
  66. event.preventDefault()
  67. event.stopPropagation()
  68. }}
  69. onClick={event => {
  70. if (disabled) {
  71. event.preventDefault()
  72. return
  73. }
  74. if (event.altKey && altCommand && open === false) {
  75. emitToolbarEvent(view, id)
  76. event.preventDefault()
  77. altCommand(view)
  78. view.focus()
  79. } else {
  80. handleToggle(!open)
  81. }
  82. }}
  83. ref={target}
  84. >
  85. {icon}
  86. </button>
  87. )
  88. const overlay = (
  89. <OLOverlay
  90. show={open && !disablePopover}
  91. target={target.current}
  92. placement="bottom"
  93. container={view.dom}
  94. containerPadding={0}
  95. transition
  96. rootClose
  97. onHide={() => handleToggle(false)}
  98. >
  99. <OLPopover
  100. id={`${id}-menu`}
  101. ref={ref}
  102. className={classNames('ol-cm-toolbar-button-menu-popover', {
  103. 'ol-cm-toolbar-button-menu-popover-horizontal':
  104. orientation === 'horizontal',
  105. })}
  106. >
  107. <OLListGroup
  108. role="menu"
  109. onClick={() => {
  110. handleToggle(false)
  111. }}
  112. >
  113. {children}
  114. </OLListGroup>
  115. </OLPopover>
  116. </OLOverlay>
  117. )
  118. if (!label) {
  119. return (
  120. <>
  121. {button}
  122. {overlay}
  123. </>
  124. )
  125. }
  126. return (
  127. <>
  128. <OLTooltip
  129. hidden={open}
  130. id={id}
  131. description={<div>{label}</div>}
  132. overlayProps={{ placement: 'bottom' }}
  133. >
  134. {button}
  135. </OLTooltip>
  136. {overlay}
  137. </>
  138. )
  139. })