section-heading-dropdown.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import classnames from 'classnames'
  2. import {
  3. useCodeMirrorStateContext,
  4. useCodeMirrorViewContext,
  5. } from '../codemirror-context'
  6. import {
  7. findCurrentSectionHeadingLevel,
  8. setSectionHeadingLevel,
  9. } from '../../extensions/toolbar/sections'
  10. import { useCallback, useMemo, useRef } from 'react'
  11. import OLOverlay from '@/features/ui/components/ol/ol-overlay'
  12. import OLPopover from '@/features/ui/components/ol/ol-popover'
  13. import useEventListener from '../../../../shared/hooks/use-event-listener'
  14. import useDropdown from '../../../../shared/hooks/use-dropdown'
  15. import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
  16. import Icon from '../../../../shared/components/icon'
  17. import { useTranslation } from 'react-i18next'
  18. const levels = new Map([
  19. ['text', 'Normal text'],
  20. ['section', 'Section'],
  21. ['subsection', 'Subsection'],
  22. ['subsubsection', 'Subsubsection'],
  23. ['paragraph', 'Paragraph'],
  24. ['subparagraph', 'Subparagraph'],
  25. ])
  26. const levelsEntries = [...levels.entries()]
  27. export const SectionHeadingDropdown = () => {
  28. const state = useCodeMirrorStateContext()
  29. const view = useCodeMirrorViewContext()
  30. const { t } = useTranslation()
  31. const { open: overflowOpen, onToggle: setOverflowOpen } = useDropdown()
  32. useEventListener(
  33. 'resize',
  34. useCallback(() => {
  35. setOverflowOpen(false)
  36. }, [setOverflowOpen])
  37. )
  38. const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
  39. const currentLevel = useMemo(
  40. () => findCurrentSectionHeadingLevel(state),
  41. [state]
  42. )
  43. const currentLabel = currentLevel
  44. ? (levels.get(currentLevel.level) ?? currentLevel.level)
  45. : '---'
  46. return (
  47. <>
  48. <button
  49. ref={toggleButtonRef}
  50. type="button"
  51. id="section-heading-menu-button"
  52. aria-haspopup="true"
  53. aria-controls="section-heading-menu"
  54. aria-label={t('toolbar_choose_section_heading_level')}
  55. className="ol-cm-toolbar-menu-toggle"
  56. onMouseDown={event => event.preventDefault()}
  57. onClick={() => setOverflowOpen(!overflowOpen)}
  58. >
  59. <span>{currentLabel}</span>
  60. <Icon type="caret-down" fw />
  61. </button>
  62. {overflowOpen && (
  63. <OLOverlay
  64. show
  65. onHide={() => setOverflowOpen(false)}
  66. transition={false}
  67. container={view.dom}
  68. containerPadding={0}
  69. placement="bottom"
  70. rootClose
  71. target={toggleButtonRef.current}
  72. popperConfig={{
  73. modifiers: [
  74. {
  75. name: 'offset',
  76. options: {
  77. offset: [0, 1],
  78. },
  79. },
  80. ],
  81. }}
  82. >
  83. <OLPopover
  84. id="popover-toolbar-section-heading"
  85. className="ol-cm-toolbar-menu-popover"
  86. >
  87. <div
  88. className="ol-cm-toolbar-menu"
  89. id="section-heading-menu"
  90. role="menu"
  91. aria-labelledby="section-heading-menu-button"
  92. >
  93. {levelsEntries.map(([level, label]) => (
  94. <button
  95. type="button"
  96. role="menuitem"
  97. key={level}
  98. onClick={() => {
  99. emitToolbarEvent(view, 'section-level-change')
  100. setSectionHeadingLevel(view, level)
  101. view.focus()
  102. setOverflowOpen(false)
  103. }}
  104. className={classnames(
  105. 'ol-cm-toolbar-menu-item',
  106. `section-level-${level}`,
  107. {
  108. 'ol-cm-toolbar-menu-item-active':
  109. level === currentLevel?.level,
  110. }
  111. )}
  112. >
  113. {label}
  114. </button>
  115. ))}
  116. </div>
  117. </OLPopover>
  118. </OLOverlay>
  119. )}
  120. </>
  121. )
  122. }