outline-item.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { useState, useEffect, useRef, memo } from 'react'
  2. import PropTypes from 'prop-types'
  3. import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
  4. import classNames from 'classnames'
  5. import { useTranslation } from 'react-i18next'
  6. import OutlineList from './outline-list'
  7. import Icon from '../../../shared/components/icon'
  8. const OutlineItem = memo(function OutlineItem({
  9. outlineItem,
  10. jumpToLine,
  11. highlightedLine,
  12. matchesHighlightedLine,
  13. containsHighlightedLine,
  14. }) {
  15. const { t } = useTranslation()
  16. const [expanded, setExpanded] = useState(true)
  17. const titleElementRef = useRef()
  18. const isHighlightedRef = useRef(false)
  19. const mainItemClasses = classNames('outline-item', {
  20. 'outline-item-no-children': !outlineItem.children,
  21. })
  22. const hasHighlightedChild = !expanded && containsHighlightedLine
  23. const isHighlighted = matchesHighlightedLine || hasHighlightedChild
  24. const itemLinkClasses = classNames('outline-item-link', {
  25. 'outline-item-link-highlight': isHighlighted,
  26. })
  27. function handleExpandCollapseClick() {
  28. setExpanded(!expanded)
  29. }
  30. function handleOutlineItemLinkClick(event) {
  31. const syncToPdf = event.detail === 2 // double-click = sync to PDF
  32. jumpToLine(outlineItem.line, syncToPdf)
  33. }
  34. useEffect(() => {
  35. const wasHighlighted = isHighlightedRef.current
  36. isHighlightedRef.current = isHighlighted
  37. if (!wasHighlighted && isHighlighted) {
  38. scrollIntoViewIfNeeded(titleElementRef.current, {
  39. scrollMode: 'if-needed',
  40. block: 'center',
  41. })
  42. }
  43. }, [isHighlighted, titleElementRef, isHighlightedRef])
  44. // don't set the aria-expanded attribute when there are no children
  45. const ariaExpandedValue = outlineItem.children ? expanded : undefined
  46. return (
  47. <li
  48. className={mainItemClasses}
  49. aria-expanded={ariaExpandedValue}
  50. // FIXME
  51. // eslint-disable-next-line jsx-a11y/role-has-required-aria-props
  52. role="treeitem"
  53. aria-current={isHighlighted}
  54. aria-label={outlineItem.title}
  55. >
  56. <div className="outline-item-row">
  57. {outlineItem.children ? (
  58. <button
  59. className="outline-item-expand-collapse-btn"
  60. onClick={handleExpandCollapseClick}
  61. aria-label={expanded ? t('collapse') : t('expand')}
  62. >
  63. <Icon
  64. type={expanded ? 'angle-down' : 'angle-right'}
  65. className="outline-caret-icon"
  66. />
  67. </button>
  68. ) : null}
  69. <button
  70. className={itemLinkClasses}
  71. onClick={handleOutlineItemLinkClick}
  72. ref={titleElementRef}
  73. >
  74. {outlineItem.title}
  75. </button>
  76. </div>
  77. {expanded && outlineItem.children ? (
  78. // highlightedLine is only provided to this list if the list contains
  79. // the highlighted line. This means that whenever the list does not
  80. // contain the highlighted line, the props provided to it are the same
  81. // and the component can be memoized.
  82. <OutlineList
  83. outline={outlineItem.children}
  84. jumpToLine={jumpToLine}
  85. isRoot={false}
  86. highlightedLine={containsHighlightedLine ? highlightedLine : null}
  87. containsHighlightedLine={containsHighlightedLine}
  88. />
  89. ) : null}
  90. </li>
  91. )
  92. })
  93. OutlineItem.propTypes = {
  94. outlineItem: PropTypes.exact({
  95. line: PropTypes.number.isRequired,
  96. title: PropTypes.string.isRequired,
  97. level: PropTypes.number,
  98. children: PropTypes.array,
  99. // Used for caching in CM6
  100. from: PropTypes.number,
  101. to: PropTypes.number,
  102. }).isRequired,
  103. jumpToLine: PropTypes.func.isRequired,
  104. highlightedLine: PropTypes.number,
  105. matchesHighlightedLine: PropTypes.bool,
  106. containsHighlightedLine: PropTypes.bool,
  107. }
  108. export default OutlineItem