overflow.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { FC, useCallback, useEffect, useRef } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import classnames from 'classnames'
  4. import MaterialIcon from '@/shared/components/material-icon'
  5. import { useCodeMirrorViewContext } from '../codemirror-context'
  6. import OLOverlay from '@/shared/components/ol/ol-overlay'
  7. import OLPopover from '@/shared/components/ol/ol-popover'
  8. export const ToolbarOverflow: FC<
  9. React.PropsWithChildren<{
  10. overflowed: boolean
  11. overflowOpen: boolean
  12. setOverflowOpen: (open: boolean) => void
  13. overflowRef?: React.Ref<HTMLDivElement>
  14. popoverClassName?: string
  15. }>
  16. > = ({
  17. overflowed,
  18. overflowOpen,
  19. setOverflowOpen,
  20. overflowRef,
  21. popoverClassName,
  22. children,
  23. }) => {
  24. const { t } = useTranslation()
  25. const buttonRef = useRef<HTMLButtonElement>(null)
  26. const keyboardInputRef = useRef(false)
  27. const view = useCodeMirrorViewContext()
  28. const className = classnames(
  29. 'ol-cm-toolbar-button',
  30. 'ol-cm-toolbar-overflow-toggle',
  31. {
  32. 'ol-cm-toolbar-overflow-toggle-visible': overflowed,
  33. }
  34. )
  35. // A11y - Move the focus inside the popover to the first toolbar button when it opens
  36. const handlePopoverFocus = useCallback(() => {
  37. if (keyboardInputRef.current) {
  38. const firstToolbarItem = document.querySelector(
  39. '#popover-toolbar-overflow .ol-cm-toolbar-overflow button:not([disabled])'
  40. ) as HTMLButtonElement | null
  41. if (firstToolbarItem) {
  42. firstToolbarItem.focus()
  43. }
  44. }
  45. }, [])
  46. const handleKeyDown = useCallback(() => {
  47. keyboardInputRef.current = true
  48. }, [])
  49. const handleMouseDown = useCallback(() => {
  50. keyboardInputRef.current = false
  51. }, [])
  52. useEffect(() => {
  53. document.addEventListener('keydown', handleKeyDown)
  54. document.addEventListener('mousedown', handleMouseDown)
  55. return () => {
  56. document.removeEventListener('keydown', handleKeyDown)
  57. document.removeEventListener('mousedown', handleMouseDown)
  58. }
  59. }, [handleKeyDown, handleMouseDown])
  60. // A11y - Move the focus back to the trigger when the popover is dismissed
  61. const handleCloseAndReturnFocus = useCallback(() => {
  62. setOverflowOpen(false)
  63. if (keyboardInputRef.current && buttonRef.current) {
  64. buttonRef.current.focus()
  65. }
  66. }, [setOverflowOpen])
  67. return (
  68. <>
  69. <button
  70. ref={buttonRef}
  71. type="button"
  72. id="toolbar-more"
  73. className={className}
  74. aria-label={t('more_editor_toolbar_item')}
  75. aria-expanded={overflowOpen}
  76. aria-controls="popover-toolbar-overflow"
  77. onMouseDown={event => {
  78. event.preventDefault()
  79. event.stopPropagation()
  80. }}
  81. onClick={() => {
  82. setOverflowOpen(!overflowOpen)
  83. }}
  84. >
  85. <MaterialIcon type="more_horiz" />
  86. </button>
  87. <OLOverlay
  88. show={overflowOpen}
  89. target={buttonRef.current}
  90. placement="bottom"
  91. container={view.dom}
  92. // containerPadding={0}
  93. transition
  94. rootClose
  95. onHide={handleCloseAndReturnFocus}
  96. onEntered={handlePopoverFocus}
  97. >
  98. <OLPopover
  99. id="popover-toolbar-overflow"
  100. ref={overflowRef}
  101. role="toolbar"
  102. >
  103. <div
  104. className={classnames(popoverClassName, 'ol-cm-toolbar-overflow')}
  105. >
  106. {children}
  107. </div>
  108. </OLPopover>
  109. </OLOverlay>
  110. </>
  111. )
  112. }