diff --git a/src/app/components/Dropdown.spec.tsx b/src/app/components/Dropdown.spec.tsx index 2013417131..27a2fc994d 100644 --- a/src/app/components/Dropdown.spec.tsx +++ b/src/app/components/Dropdown.spec.tsx @@ -107,6 +107,7 @@ describe('Dropdown', () => { const useOnEscSpy = jest.spyOn(reactUtils, 'useOnEsc'); const focus = jest.fn(); + const focus2 = jest.fn(); const addEventListener = jest.fn(); const removeEventListener = jest.fn(); const createNodeMock = () => ({focus, addEventListener, removeEventListener}); @@ -126,12 +127,19 @@ describe('Dropdown', () => { expect(() => component.root.findByType(DropdownList)).not.toThrow(); + renderer.act(() => { + const buttons = component.root.findAllByType('button'); + + buttons[1].props.onMouseEnter({target: {focus: focus2}}); + }); + renderer.act(() => { useOnEscSpy.mock.calls[0][1](); }); expect(() => component.root.findByType(DropdownList)).toThrow(); expect(focus).toHaveBeenCalled(); + expect(focus2).toHaveBeenCalled(); useOnEscSpy.mockClear(); }); @@ -149,9 +157,10 @@ describe('Dropdown', () => { ); renderer.act(() => { - const [button1, button2] = component.root.findAllByType('a'); - button1.props.onClick(mockEv); - button2.props.onClick(mockEv); + const items = component.root.findAll((i) => i.props.onClick && i.type === 'button'); + + items.forEach((i) => i.props.onClick(mockEv)); + expect(items.length).toBe(2); }); expect(mockEv.preventDefault).toHaveBeenCalledTimes(2); diff --git a/src/app/components/Dropdown.tsx b/src/app/components/Dropdown.tsx index de52e7a683..e26a59415f 100644 --- a/src/app/components/Dropdown.tsx +++ b/src/app/components/Dropdown.tsx @@ -1,12 +1,12 @@ -import { HTMLElement } from '@openstax/types/lib.dom'; +import { HTMLElement, HTMLMenuElement } from '@openstax/types/lib.dom'; import flow from 'lodash/fp/flow'; import isUndefined from 'lodash/fp/isUndefined'; import omitBy from 'lodash/fp/omitBy'; import React, { ReactNode } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import styled, { css, keyframes } from 'styled-components/macro'; -import { useFocusLost, useTrapTabNavigation } from '../reactUtils'; +import { useFocusLost, useTrapTabNavigation, focusableItemQuery } from '../reactUtils'; import { useOnEsc } from '../reactUtils'; import theme, { defaultFocusOutline } from '../theme'; import { preventDefault } from '../utils'; @@ -167,18 +167,28 @@ const TabTransparentDropdown = styled(( `; function TrappingDropdownList(props: object) { - const ref = React.useRef(null); + const ref = React.useRef(null); useTrapTabNavigation(ref); + React.useEffect( + () => { + if (ref.current?.querySelector) { + ref.current?.querySelector(focusableItemQuery)?.focus(); + } + }, + [] + ); + return ( -
    + ); } // tslint:disable-next-line:variable-name export const DropdownList = styled(TrappingDropdownList)` + list-style: none; margin: 0; padding: 0.6rem 0; background: ${theme.color.neutral.formBackground}; @@ -207,7 +217,6 @@ export const DropdownList = styled(TrappingDropdownList)` font-size: 1.4rem; line-height: 2rem; - &:hover, &:focus { background: ${theme.color.neutral.formBorder}; ${defaultFocusOutline} @@ -235,24 +244,32 @@ const DropdownItemContent = ({ 'data-analytics-label': dataAnalyticsLabel, 'data-analytics-region': dataAnalyticsRegion, }); - return + const focusMe = React.useCallback( + ({target: me}) => me.focus(), + [] + ); + +return {(msg) => href - ? {prefix}{msg} - /* - this should be a button but Safari and firefox don't support focusing buttons - which breaks the tab transparent dropdown - https://bugs.webkit.org/show_bug.cgi?id=22261 - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus - */ - // eslint-disable-next-line jsx-a11y/anchor-is-valid - : {prefix}{msg} + // Safari support tab-navigation of buttons; this operates with space or Enter + : } ; }; @@ -261,9 +278,9 @@ const DropdownItemContent = ({ export const DropdownItem = ({ariaMessage, ...contentProps}: DropdownItemProps) => { const intl = useIntl(); - return ariaMessage - ?
  1. - :
  2. ; + return
  3. + +
  4. ; }; interface CommonDropdownProps { diff --git a/src/app/components/__snapshots__/DotMenu.spec.tsx.snap b/src/app/components/__snapshots__/DotMenu.spec.tsx.snap index b3fb1c0479..f6da4f2c26 100644 --- a/src/app/components/__snapshots__/DotMenu.spec.tsx.snap +++ b/src/app/components/__snapshots__/DotMenu.spec.tsx.snap @@ -136,6 +136,7 @@ exports[`Dropdown matches snapshot 1`] = ` } .c12 { + list-style: none; margin: 0; padding: 0.6rem 0; background: #f5f5f5; @@ -173,8 +174,6 @@ exports[`Dropdown matches snapshot 1`] = ` line-height: 2rem; } -.c12 li button:hover, -.c12 li a:hover, .c12 li button:focus, .c12 li a:focus { background: #d5d5d5; @@ -224,28 +223,31 @@ exports[`Dropdown matches snapshot 1`] = ` -
    1. - Delete - +
    2. Edit
    3. -
    +
    -
    1. - Delete - +
    2. Edit
    3. -
    + -
    1. - Delete - +
    2. Edit
    3. -
    + -
    1. - Delete - +
    2. Edit
    3. -
    + `; diff --git a/src/app/content/highlights/components/DisplayNote.tsx b/src/app/content/highlights/components/DisplayNote.tsx index eb10c014dd..aa1afd5be3 100644 --- a/src/app/content/highlights/components/DisplayNote.tsx +++ b/src/app/content/highlights/components/DisplayNote.tsx @@ -19,7 +19,6 @@ import Confirmation from './Confirmation'; import MenuToggle, { MenuIcon } from './MenuToggle'; import TruncatedText from './TruncatedText'; import { isElementForOnClickOutside, useOnClickOutside } from './utils/onClickOutside'; -import { useIntl } from 'react-intl'; // tslint:disable-next-line:variable-name const CloseIcon = styled((props) =>