Skip to content

Commit 125d328

Browse files
authored
Core-20: clear selection when user tab-navigates (#2578)
* Core-20: clear selection when user tab-navigates [CORE-20] * Move "HiddenOnMobile" up the hierarchy Update EditCard.spec.tsx.snap * Highlights basically work * CORE_1319 Update CardWrapper.tsx * CORE-1318 * Tests * Use Intl for message * Separate message for create or edit
1 parent a3dd806 commit 125d328

File tree

10 files changed

+243
-175
lines changed

10 files changed

+243
-175
lines changed

src/app/content/highlights/components/CardWrapper.spec.tsx

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('CardWrapper', () => {
138138

139139
// Handle null value
140140
renderer.act(() => {
141-
card1.props.onHeightChange({ current: { offsetHeight: null } });
141+
card1.props.onHeightChange({ current: null });
142142
});
143143
// First card have null height so secondcard starts at the highlight top offset
144144
expect(card1.props.topOffset).toEqual(100);
@@ -332,6 +332,14 @@ describe('CardWrapper', () => {
332332
});
333333
});
334334

335+
renderer.act(() => {
336+
// Simulate pressing Escape to hide card
337+
dispatchKeyDownEvent({
338+
key: 'Escape',
339+
target: highlightElement1,
340+
});
341+
});
342+
335343
// Expect cards to be visible again
336344
renderer.act(() => {
337345
// Simulate pressing Enter to unhide cards
@@ -341,7 +349,6 @@ describe('CardWrapper', () => {
341349
});
342350
});
343351

344-
// Expect all cards to be visible
345352
renderer.act(() => {
346353
// Simulate pressing Tab to unhide all cards
347354
dispatchKeyDownEvent({
@@ -350,11 +357,22 @@ describe('CardWrapper', () => {
350357
});
351358
});
352359

353-
// Set focusedHighlight, and do double=click
354360
renderer.act(() => {
355-
highlightElement1.dispatchEvent(new Event('focus', { bubbles: true }));
356-
highlightElement1.dispatchEvent(new Event('dblclick', { bubbles: true }));
361+
// Simulate pressing Escape to hide card
362+
dispatchKeyDownEvent({
363+
key: 'Escape',
364+
target: highlightElement1,
365+
});
357366
});
367+
368+
// Trigger editOnEnter with no focusedHighlight
369+
renderer.act(() => {
370+
dispatchKeyDownEvent({
371+
key: 'Enter',
372+
});
373+
});
374+
375+
document?.dispatchEvent(new CustomEvent('showCardEvent', { bubbles: true }));
358376
});
359377

360378
it(

src/app/content/highlights/components/CardWrapper.tsx

Lines changed: 29 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import ResizeObserver from 'resize-observer-polyfill';
66
import styled from 'styled-components';
77
import { isHtmlElement } from '../../../guards';
88
import { useFocusLost, useKeyCombination, useFocusHighlight } from '../../../reactUtils';
9-
import { AppState } from '../../../types';
9+
import { AppState, Dispatch } from '../../../types';
1010
import { assertDefined, assertDocument } from '../../../utils';
1111
import * as selectSearch from '../../search/selectors';
1212
import * as contentSelect from '../../selectors';
@@ -23,6 +23,7 @@ export interface WrapperProps {
2323
highlighter: Highlighter;
2424
highlights: Highlight[];
2525
className?: string;
26+
dispatch: Dispatch;
2627
}
2728

2829
function checkIfHiddenByCollapsedAncestor(highlight: Highlight) {
@@ -87,12 +88,6 @@ function useCardsHeights() {
8788
return [cardsHeights, onHeightChange] as const;
8889
}
8990

90-
function rangeString({startOffset, endOffset}: Highlight['range']) {
91-
return `${startOffset}-${endOffset}`;
92-
}
93-
94-
const ELAPSED_LIMIT = 100;
95-
9691
function useFocusedHighlight(
9792
highlights: Highlight[],
9893
element: React.RefObject<HTMLElement>,
@@ -104,47 +99,23 @@ function useFocusedHighlight(
10499
[focusedId, highlights]);
105100
const [shouldFocusCard, setShouldFocusCard] = React.useState(false);
106101
const document = assertDocument();
107-
const previousRange = React.useRef<string>('');
108-
const [dblclickStamp, setDblclickStamp] = React.useState<number>(0);
109102

103+
// catches the "click here" event sent by the EditCard
110104
React.useEffect(() => {
111-
const handler = () => setDblclickStamp(Date.now());
105+
const handler = () => setShouldFocusCard(true);
112106

113-
document.addEventListener('dblclick', handler);
114-
return () => document.removeEventListener('dblclick', handler);
107+
document.addEventListener('showCardEvent', handler);
108+
return () => document.removeEventListener('showCardEvent', handler);
115109
}, [document]);
116110

117-
// Tracking double clicks
118-
// double-click events trigger updates to focusedHighlight, but the order and
119-
// timing of processing can vary, so we check conditions within a short time
120-
// of a double click.
111+
// Ensure focusedHighlight is actually focused
121112
React.useEffect(() => {
122-
if (focusedHighlight) {
123-
const elapsed = Date.now() - dblclickStamp;
124-
const isDoubleClick = elapsed < ELAPSED_LIMIT;
125-
126-
// Existing highlight
127-
if (focusedHighlight.elements.length > 0) {
128-
if (isDoubleClick) {
129-
// Unselect text inside existing highlight.
130-
document.getSelection()?.removeAllRanges();
131-
setShouldFocusCard(true);
132-
}
133-
return;
134-
}
135-
136-
// Text selection that could be a highlight
137-
const newRange = rangeString(focusedHighlight.range);
138-
const isExistingSelection = newRange === previousRange.current;
139-
140-
if (isExistingSelection && isDoubleClick) {
141-
setShouldFocusCard(true);
142-
}
143-
previousRange.current = newRange;
113+
if (focusedHighlight && focusedHighlight.elements.length > 0) {
114+
focusedHighlight?.focus();
144115
}
145-
}, [focusedHighlight, document, dblclickStamp]);
116+
}, [focusedHighlight]);
146117

147-
// Let Enter go from a highlight to the editor
118+
// Pressing Enter moves the users from a highlight to the editor
148119
const editOnEnter = React.useCallback(() => {
149120
if (focusedHighlight) {
150121
setShouldFocusCard(true);
@@ -163,20 +134,23 @@ function useFocusedHighlight(
163134
setShouldFocusCard(!cardIsFocused);
164135
}, [element, focusedHighlight]);
165136

166-
useKeyCombination({key: 'Enter'}, editOnEnter);
137+
useKeyCombination({key: 'Enter'}, editOnEnter, noopKeyCombinationHandler([container, element]));
167138
useKeyCombination(highlightKeyCombination, moveFocus, noopKeyCombinationHandler([container, element]));
168139
// Clear shouldFocusCard when focus is lost from the CardWrapper.
169140
// If we don't do this then card related for the focused highlight will be focused automatically.
170141
useFocusLost(element, shouldFocusCard, React.useCallback(() => setShouldFocusCard(false), []));
171142

172-
return [focusedHighlight, shouldFocusCard] as const;
143+
return [focusedHighlight, shouldFocusCard, setShouldFocusCard] as const;
173144
}
174145

175-
function CardsForHighlights({highlights, container, focusedHighlight, shouldFocusCard, highlighter}: {
146+
function CardsForHighlights({
147+
highlights, container, focusedHighlight, shouldFocusCard, setShouldFocusCard, highlighter,
148+
}: {
176149
highlights: Highlight[];
177150
container: HTMLElement;
178151
focusedHighlight: Highlight | undefined;
179152
shouldFocusCard: boolean;
153+
setShouldFocusCard: (v: boolean) => void;
180154
highlighter: Highlighter;
181155
}) {
182156
const [cardsHeights, onHeightChange] = useCardsHeights();
@@ -190,9 +164,18 @@ function CardsForHighlights({highlights, container, focusedHighlight, shouldFocu
190164
editCardVisibilityHandler,
191165
new Map(highlights.map((highlight) => [highlight.id, false]))
192166
);
167+
168+
// First time, Esc closes it to the instructions; second Esc disappears it
193169
const hideCard = () => {
170+
if (!focusedHighlight?.elements.length) {
171+
return;
172+
}
194173
focusedHighlight?.focus();
195-
dispatch({ type: 'HIDE', id: focusedHighlight?.id });
174+
if (shouldFocusCard) {
175+
setShouldFocusCard(false);
176+
} else {
177+
dispatch({ type: 'HIDE', id: focusedHighlight?.id });
178+
}
196179
};
197180
const showCard = (cardId: string | undefined) => {
198181
dispatch({ type: 'SHOW', id: cardId });
@@ -228,14 +211,15 @@ function CardsForHighlights({highlights, container, focusedHighlight, shouldFocu
228211
// tslint:disable-next-line:variable-name
229212
const Wrapper = ({highlights, className, container, highlighter}: WrapperProps) => {
230213
const element = React.useRef<HTMLElement>(null);
231-
const [focusedHighlight, shouldFocusCard] = useFocusedHighlight(highlights, element, container);
214+
const [focusedHighlight, shouldFocusCard, setShouldFocusCard] = useFocusedHighlight(highlights, element, container);
232215

233216
return <div className={className} ref={element}>
234217
<CardsForHighlights
235218
highlights={highlights}
236219
container={container}
237220
focusedHighlight={focusedHighlight}
238221
shouldFocusCard={shouldFocusCard}
222+
setShouldFocusCard={setShouldFocusCard}
239223
highlighter={highlighter}
240224
/>
241225
</div>;

src/app/content/highlights/components/EditCard.spec.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Highlight } from '@openstax/highlighter';
22
import { HighlightUpdateColorEnum } from '@openstax/highlighter/dist/api';
33
import React, { ReactElement } from 'react';
44
import renderer from 'react-test-renderer';
5+
import ReactTestUtils from 'react-dom/test-utils';
56
import createTestServices from '../../../../test/createTestServices';
67
import createTestStore from '../../../../test/createTestStore';
78
import createMockHighlight from '../../../../test/mocks/highlight';
@@ -101,6 +102,27 @@ describe('EditCard', () => {
101102

102103
const tree = component.toJSON();
103104
expect(tree).toMatchSnapshot();
105+
106+
mockSpyUser.mockClear();
107+
});
108+
109+
it('shows create highlight message for new highlight', () => {
110+
const newHighlight = {...highlight, elements: []};
111+
const mockSpyUser = jest.spyOn(selectAuth, 'user')
112+
.mockReturnValue(formatUser(testAccountsUser));
113+
const component = renderer.create(
114+
<TestContainer services={services} store={store}>
115+
<EditCard
116+
{...{...editCardProps, highlight: newHighlight}}
117+
data={highlightData}
118+
isActive={true}
119+
shouldFocusCard={false}
120+
/>
121+
</TestContainer>
122+
);
123+
124+
const button = component.root.findByType('button');
125+
expect(button.props.children.props.id).toBe('i18n:highlighting:create-instructions');
104126
mockSpyUser.mockClear();
105127
});
106128

@@ -629,7 +651,7 @@ describe('EditCard', () => {
629651
...highlightData,
630652
};
631653

632-
renderToDom(
654+
const component = renderToDom(
633655
<div id={MAIN_CONTENT_ID} tabIndex={-1}>
634656
<TestContainer services={services} store={store}>
635657
<a href='#foo'>text</a>
@@ -650,6 +672,17 @@ describe('EditCard', () => {
650672
document?.querySelector('a')?.focus();
651673
document?.getElementById(MAIN_CONTENT_ID)?.focus();
652674
expect(editCardProps.onBlur).not.toHaveBeenCalled();
675+
const button = component.node.querySelector('button') as HTMLButtonElement;
676+
const preventDefault = jest.fn();
677+
document!.dispatchEvent = jest.fn();
678+
679+
// Two branches of showCard - must be mousedown of button 0
680+
ReactTestUtils.Simulate.mouseDown(button, { preventDefault, button: 1 });
681+
expect(preventDefault).not.toHaveBeenCalled();
682+
ReactTestUtils.Simulate.mouseDown(button, { preventDefault, button: 0 });
683+
expect(preventDefault).toHaveBeenCalled();
684+
expect(document!.dispatchEvent).toHaveBeenCalled();
685+
653686
mockSpyUser.mockClear();
654687
});
655688

src/app/content/highlights/components/EditCard.tsx

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useFocusElement, useOnEsc, useTrapTabNavigation } from '../../../reactU
1212
import theme from '../../../theme';
1313
import { assertDefined, assertWindow, mergeRefs } from '../../../utils';
1414
import { highlightStyles } from '../../constants';
15+
import { cardWidth } from '../constants';
1516
import defer from 'lodash/fp/defer';
1617
import {
1718
clearFocusedHighlight,
@@ -67,6 +68,13 @@ function LoginOrEdit({
6768
const authenticated = !!useSelector(selectAuth.user);
6869
const element = React.useRef<HTMLElement>(null);
6970
const {formatMessage} = useIntl();
71+
const showCard = React.useCallback((event: React.MouseEvent) => {
72+
if (event.button === 0) {
73+
event.preventDefault();
74+
document?.dispatchEvent(new CustomEvent('showCardEvent', { bubbles: true }));
75+
}
76+
}, []);
77+
const isNewSelection = props.highlight.elements.length === 0;
7078

7179
return (
7280
<div
@@ -76,16 +84,26 @@ function LoginOrEdit({
7684
>
7785
{
7886
authenticated ? (
79-
(props.shouldFocusCard || props.data?.annotation) ? (
80-
<form
81-
ref={mergeRefs(fref, element)}
82-
data-analytics-region='edit-note'
83-
data-highlight-card
84-
>
85-
<ActiveEditCard props={props} element={element} />
86-
</form>
87-
) :
88-
<i>Press Enter or double-click highlight to edit highlight</i>
87+
<HiddenOnMobile>
88+
{
89+
(props.shouldFocusCard || props.data?.annotation) ? (
90+
<form
91+
ref={mergeRefs(fref, element)}
92+
data-analytics-region='edit-note'
93+
data-highlight-card
94+
>
95+
<ActiveEditCard props={props} element={element} />
96+
</form>
97+
) :
98+
<button type='button' onMouseDown={showCard}>
99+
<FormattedMessage id={
100+
isNewSelection
101+
? 'i18n:highlighting:create-instructions'
102+
: 'i18n:highlighting:instructions'
103+
} />
104+
</button>
105+
}
106+
</HiddenOnMobile>
89107
) : <LoginConfirmation onBlur={props.onBlur} />
90108
}
91109
</div>
@@ -117,6 +135,7 @@ function LoginConfirmation({
117135

118136
// tslint:disable-next-line:variable-name
119137
const HiddenOnMobile = styled.div`
138+
min-width: ${cardWidth}rem;
120139
${theme.breakpoints.touchDeviceQuery(css`
121140
display: none;
122141
`)}
@@ -205,7 +224,7 @@ function ActiveEditCard({
205224
useTrapTabNavigation(ref, editingAnnotation);
206225

207226
return (
208-
<HiddenOnMobile ref={ref}>
227+
<div ref={ref}>
209228
<ColorPicker
210229
color={props.data?.color}
211230
onChange={onColorChange}
@@ -248,7 +267,7 @@ function ActiveEditCard({
248267
always={() => setConfirmingDelete(false)}
249268
/>
250269
)}
251-
</HiddenOnMobile>
270+
</div>
252271
);
253272
}
254273

0 commit comments

Comments
 (0)