overflow.tsx 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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 '@/features/ui/components/ol/ol-overlay'
  7. import OLPopover from '@/features/ui/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. }>
  15. > = ({ overflowed, overflowOpen, setOverflowOpen, overflowRef, children }) => {
  16. const { t } = useTranslation()
  17. const buttonRef = useRef<HTMLButtonElement>(null)
  18. const keyboardInputRef = useRef(false)
  19. const view = useCodeMirrorViewContext()
  20. const className = classnames(
  21. 'ol-cm-toolbar-button',
  22. 'ol-cm-toolbar-overflow-toggle',
  23. {
  24. 'ol-cm-toolbar-overflow-toggle-visible': overflowed,
  25. }
  26. )
  27. // A11y - Move the focus inside the popover to the first toolbar button when it opens
  28. const handlePopoverFocus = useCallback(() => {
  29. if (keyboardInputRef.current) {
  30. const firstToolbarItem = document.querySelector(
  31. '#popover-toolbar-overflow .ol-cm-toolbar-overflow button:not([disabled])'
  32. ) as HTMLButtonElement | null
  33. if (firstToolbarItem) {
  34. firstToolbarItem.focus()
  35. }
  36. }
  37. }, [])
  38. const handleKeyDown = useCallback(() => {
  39. keyboardInputRef.current = true
  40. }, [])
  41. const handleMouseDown = useCallback(() => {
  42. keyboardInputRef.current = false
  43. }, [])
  44. useEffect(() => {
  45. document.addEventListener('keydown', handleKeyDown)
  46. document.addEventListener('mousedown', handleMouseDown)
  47. return () => {
  48. document.removeEventListener('keydown', handleKeyDown)
  49. document.removeEventListener('mousedown', handleMouseDown)
  50. }
  51. }, [handleKeyDown, handleMouseDown])
  52. // A11y - Move the focus back to the trigger when the popover is dismissed
  53. const handleCloseAndReturnFocus = useCallback(() => {
  54. setOverflowOpen(false)
  55. if (keyboardInputRef.current && buttonRef.current) {
  56. buttonRef.current.focus()
  57. }
  58. }, [setOverflowOpen])
  59. return (
  60. <>
  61. <button
  62. ref={buttonRef}
  63. type="button"
  64. id="toolbar-more"
  65. className={className}
  66. aria-label={t('more_editor_toolbar_item')}
  67. aria-expanded={overflowOpen}
  68. aria-controls="popover-toolbar-overflow"
  69. onMouseDown={event => {
  70. event.preventDefault()
  71. event.stopPropagation()
  72. }}
  73. onClick={() => {
  74. setOverflowOpen(!overflowOpen)
  75. }}
  76. >
  77. <MaterialIcon type="more_horiz" />
  78. </button>
  79. <OLOverlay
  80. show={overflowOpen}
  81. target={buttonRef.current}
  82. placement="bottom"
  83. container={view.dom}
  84. // containerPadding={0}
  85. transition
  86. rootClose
  87. onHide={handleCloseAndReturnFocus}
  88. onEntered={handlePopoverFocus}
  89. >
  90. <OLPopover
  91. id="popover-toolbar-overflow"
  92. ref={overflowRef}
  93. role="toolbar"
  94. >
  95. <div className="ol-cm-toolbar-overflow">{children}</div>
  96. </OLPopover>
  97. </OLOverlay>
  98. </>
  99. )
  100. }