diff --git a/services/web/.babelrc b/services/web/.babelrc index d2b230bb27..d354026e94 100644 --- a/services/web/.babelrc +++ b/services/web/.babelrc @@ -7,14 +7,18 @@ }] ], "plugins": ["angularjs-annotate"], - // Disable core-js polyfilling in test environment, to prevent interference - // with mocking + // Target our current Node version in test environment, to transform and + // polyfill only what's necessary "env": { "test": { "presets": [ "@babel/react", - "@babel/env" + ["@babel/env", { + "targets": {"node": "10.21"}, + "useBuiltIns": "usage", + "corejs": { "version": 3 } + }] ] } } -} \ No newline at end of file +} diff --git a/services/web/frontend/extracted-translation-keys.json b/services/web/frontend/extracted-translation-keys.json index 4faef59296..35f8a2d908 100644 --- a/services/web/frontend/extracted-translation-keys.json +++ b/services/web/frontend/extracted-translation-keys.json @@ -2,5 +2,7 @@ "file_outline", "the_file_outline_is_a_new_feature_click_the_icon_to_learn_more", "we_cant_find_any_sections_or_subsections_in_this_file", - "find_out_more_about_the_file_outline" + "find_out_more_about_the_file_outline", + "expand", + "collapse" ] diff --git a/services/web/frontend/js/features/outline/components/outline-item.js b/services/web/frontend/js/features/outline/components/outline-item.js index fb775b33c9..4749121a8c 100644 --- a/services/web/frontend/js/features/outline/components/outline-item.js +++ b/services/web/frontend/js/features/outline/components/outline-item.js @@ -2,6 +2,7 @@ import React, { useState, useEffect, createRef, useRef } from 'react' import PropTypes from 'prop-types' import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed' import classNames from 'classnames' +import { useTranslation } from 'react-i18next' import OutlineList from './outline-list' function getChildrenLines(children) { @@ -13,6 +14,8 @@ function getChildrenLines(children) { } function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) { + const { t } = useTranslation() + const [expanded, setExpanded] = useState(true) const titleElementRef = createRef() const isHighlightedRef = useRef(false) @@ -30,9 +33,11 @@ function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) { !expanded && getChildrenLines(outlineItem.children).includes(highlightedLine) + const isHighlighted = + highlightedLine === outlineItem.line || hasHighlightedChild + const itemLinkClasses = classNames('outline-item-link', { - 'outline-item-link-highlight': - highlightedLine === outlineItem.line || hasHighlightedChild + 'outline-item-link-highlight': isHighlighted }) function handleExpandCollapseClick() { @@ -64,13 +69,23 @@ function OutlineItem({ outlineItem, jumpToLine, highlightedLine }) { [highlightedLine, titleElementRef, isHighlightedRef] ) + // don't set the aria-expanded attribute when there are no children + const ariaExpandedValue = outlineItem.children ? expanded : undefined + return ( -
  • +
  • {outlineItem.children ? ( @@ -100,7 +115,7 @@ OutlineItem.propTypes = { outlineItem: PropTypes.exact({ line: PropTypes.number.isRequired, title: PropTypes.string.isRequired, - level: PropTypes.number.isRequired, + level: PropTypes.number, children: PropTypes.array }).isRequired, jumpToLine: PropTypes.func.isRequired, diff --git a/services/web/frontend/js/features/outline/components/outline-list.js b/services/web/frontend/js/features/outline/components/outline-list.js index f6f5efa9b0..2380846455 100644 --- a/services/web/frontend/js/features/outline/components/outline-list.js +++ b/services/web/frontend/js/features/outline/components/outline-list.js @@ -8,7 +8,7 @@ function OutlineList({ outline, jumpToLine, isRoot, highlightedLine }) { 'outline-item-list-root': isRoot }) return ( -
      +
        {outline.map((outlineItem, idx) => { return ( ', function() { + before(function() { + this.jumpToLine = sinon.stub() + }) + + afterEach(function() { + this.jumpToLine.reset() + }) + + it('renders basic item', function() { + const outlineItem = { + title: 'Test Title', + line: 1 + } + render( + + ) + + screen.getByRole('treeitem', { current: false }) + screen.getByRole('button', { name: outlineItem.title }) + expect(screen.queryByRole('button', { name: 'Collapse' })).to.not.exist + }) + + it('collapses and expands', function() { + const outlineItem = { + title: 'Parent', + line: 1, + children: [{ title: 'Child', line: 2 }] + } + render( + + ) + + const collapseButton = screen.getByRole('button', { name: 'Collapse' }) + + // test that children are rendered + screen.getByRole('button', { name: 'Child' }) + + fireEvent.click(collapseButton) + + screen.getByRole('button', { name: 'Expand' }) + + expect(screen.queryByRole('button', { name: 'Child' })).to.not.exist + }) + + it('highlights', function() { + const outlineItem = { + title: 'Parent', + line: 1 + } + + render( + + ) + + screen.getByRole('treeitem', { current: true }) + }) + + it('highlights when has collapsed highlighted child', function() { + const outlineItem = { + title: 'Parent', + line: 1, + children: [{ title: 'Child', line: 2 }] + } + render( + + ) + + screen.getByRole('treeitem', { name: 'Parent', current: false }) + screen.getByRole('treeitem', { name: 'Child', current: true }) + + fireEvent.click(screen.getByRole('button', { name: 'Collapse' })) + + screen.getByRole('treeitem', { name: 'Parent', current: true }) + }) + + it('click and double-click jump to location', function() { + const outlineItem = { + title: 'Parent', + line: 1 + } + render( + + ) + + const titleButton = screen.getByRole('button', { name: outlineItem.title }) + + fireEvent.click(titleButton) + sinon.assert.calledOnce(this.jumpToLine) + sinon.assert.calledWith(this.jumpToLine, 1, false) + + this.jumpToLine.reset() + fireEvent.doubleClick(titleButton) + sinon.assert.calledOnce(this.jumpToLine) + sinon.assert.calledWith(this.jumpToLine, 1, true) + }) +})