diff --git a/src/app/components/Modal/__snapshots__/Modal.spec.tsx.snap b/src/app/components/Modal/__snapshots__/Modal.spec.tsx.snap index e66707d889..c18ee8323d 100644 --- a/src/app/components/Modal/__snapshots__/Modal.spec.tsx.snap +++ b/src/app/components/Modal/__snapshots__/Modal.spec.tsx.snap @@ -68,7 +68,6 @@ exports[`Modal matches snapshot 1`] = ` -ms-flex-align: center; align-items: center; margin: 0; - padding: 1.5rem 0; } .c6 { @@ -263,7 +262,6 @@ exports[`Modal matches snapshot with children 1`] = ` -ms-flex-align: center; align-items: center; margin: 0; - padding: 1.5rem 0; } .c6 { diff --git a/src/app/components/Modal/styles.tsx b/src/app/components/Modal/styles.tsx index 9adfa21447..a9468ccaab 100644 --- a/src/app/components/Modal/styles.tsx +++ b/src/app/components/Modal/styles.tsx @@ -37,7 +37,6 @@ export const Heading = styled.h1` display: flex; align-items: center; margin: 0; - padding: ${modalPadding * 0.5}rem 0; `; // tslint:disable-next-line:variable-name diff --git a/src/app/content/__snapshots__/routes.spec.tsx.snap b/src/app/content/__snapshots__/routes.spec.tsx.snap index 933101c580..d394eba369 100644 --- a/src/app/content/__snapshots__/routes.spec.tsx.snap +++ b/src/app/content/__snapshots__/routes.spec.tsx.snap @@ -286,6 +286,10 @@ Array [ .c4 .highlight { position: relative; z-index: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .c4 .MathJax_Display .highlight, diff --git a/src/app/content/components/Page/PageContent.tsx b/src/app/content/components/Page/PageContent.tsx index 30052fb670..fff44152b5 100644 --- a/src/app/content/components/Page/PageContent.tsx +++ b/src/app/content/components/Page/PageContent.tsx @@ -58,6 +58,7 @@ export default styled(MainContent)` .highlight { position: relative; z-index: 1; + user-select: none; } /* stylelint-disable selector-class-pattern */ diff --git a/src/app/content/components/__snapshots__/Content.spec.tsx.snap b/src/app/content/components/__snapshots__/Content.spec.tsx.snap index 7fa7c20754..41a1a88bf2 100644 --- a/src/app/content/components/__snapshots__/Content.spec.tsx.snap +++ b/src/app/content/components/__snapshots__/Content.spec.tsx.snap @@ -711,6 +711,10 @@ Array [ .c74 .highlight { position: relative; z-index: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .c74 .MathJax_Display .highlight, @@ -8072,6 +8076,10 @@ Array [ .c74 .highlight { position: relative; z-index: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .c74 .MathJax_Display .highlight, diff --git a/src/app/content/highlights/components/Card.tsx b/src/app/content/highlights/components/Card.tsx index a737c30135..f67bf6e608 100644 --- a/src/app/content/highlights/components/Card.tsx +++ b/src/app/content/highlights/components/Card.tsx @@ -170,6 +170,9 @@ function Card(props: CardProps) { ); } +type ComputedProps = ReturnType; +type CommonProps = ComputedProps['commonProps']; + function NoteOrCard({ props, setHighlightRemoved, @@ -179,7 +182,7 @@ function NoteOrCard({ props: CardPropsWithBookAndPage; setHighlightRemoved: React.Dispatch>; locationFilterId: string; - computedProps: ReturnType; + computedProps: ComputedProps; }) { const { focusCard, @@ -219,7 +222,7 @@ function NoteOrCard({ /> ) : ( ; type EditCardProps = { - commonProps: object; + commonProps: CommonProps & {onRemove: () => void}; cardProps: CardPropsWithBookAndPage; locationFilterId: string; } & Pick; @@ -291,12 +293,13 @@ const StyledCard = styled(Card)` // Styling is expensive and most Cards don't need to render function PreCard(props: CardProps) { const computedProps = useComputedProps(props); + const hideUnfocusedEditCard = computedProps.annotation ? {} : {isHidden: !props.shouldFocusCard}; if (!computedProps.annotation && (!props.isActive)) { return null; } return ( - + ); } diff --git a/src/app/content/highlights/components/CardWrapper.spec.tsx b/src/app/content/highlights/components/CardWrapper.spec.tsx index 6dcd8f94a4..fafb81044f 100644 --- a/src/app/content/highlights/components/CardWrapper.spec.tsx +++ b/src/app/content/highlights/components/CardWrapper.spec.tsx @@ -303,9 +303,11 @@ describe('CardWrapper', () => { const highlight2 = createMockHighlight('id2'); const highlightElement1 = document.createElement('span'); const highlightElement2 = document.createElement('span'); + highlight1.elements.push(highlightElement1); + highlight2.elements.push(highlightElement2); container.appendChild(highlightElement1); container.appendChild(highlightElement2); - const component = renderer.create( + renderer.create( { ); - const cards = component.root.findAllByType(Card); + renderer.act(() => { + store.dispatch(focusHighlight(highlight1.id)); + }); + + // These tests get code coverage but do not update the highlight structures + // so that we can see that they worked as expected // Expect cards to be hidden renderer.act(() => { @@ -343,8 +350,11 @@ describe('CardWrapper', () => { }); }); - expect(cards[0].props.isHidden).toBe(false); - expect(cards[1].props.isHidden).toBe(false); + // Set focusedHighlight, and do double=click + renderer.act(() => { + highlightElement1.dispatchEvent(new Event('focus', { bubbles: true })); + highlightElement1.dispatchEvent(new Event('dblclick', { bubbles: true })); + }); }); it( diff --git a/src/app/content/highlights/components/CardWrapper.tsx b/src/app/content/highlights/components/CardWrapper.tsx index 70bb987c60..dc5b7e1d44 100644 --- a/src/app/content/highlights/components/CardWrapper.tsx +++ b/src/app/content/highlights/components/CardWrapper.tsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { isHtmlElement } from '../../../guards'; import { useFocusLost, useKeyCombination, useFocusHighlight } from '../../../reactUtils'; import { AppState } from '../../../types'; -import { assertDefined } from '../../../utils'; +import { assertDefined, assertDocument } from '../../../utils'; import * as selectSearch from '../../search/selectors'; import * as contentSelect from '../../selectors'; import { highlightKeyCombination } from '../constants'; @@ -25,141 +25,235 @@ export interface WrapperProps { className?: string; } -// tslint:disable-next-line:variable-name -const Wrapper = ({highlights, className, container, highlighter}: WrapperProps) => { - const element = React.useRef(null); +function checkIfHiddenByCollapsedAncestor(highlight: Highlight) { + const highlightElement = highlight.elements[0] as HTMLElement; + const collapsedAncestor = highlightElement + ? highlightElement.closest('details[data-type="solution"]:not([open])') + : null; + return Boolean(collapsedAncestor); +} + +function useCardPositionObserver( + container: HTMLElement, + focusedHighlight: Highlight | undefined, + highlights: Highlight[], + cardsHeights: Map +) { + const [offsets, setOffsets] = React.useState>(new Map()); const [cardsPositions, setCardsPositions] = React.useState>(new Map()); + const getOffsetsForHighlight = React.useCallback((highlight: Highlight) => { + const newOffsets = assertDefined( + getHighlightOffset(container, highlight), + `Couldn't get offsets for highlight with an id: ${highlight.id}` + ); + + setOffsets((state) => new Map(state).set(highlight.id, newOffsets)); + return newOffsets; + }, [container]); + const updatePositions = React.useCallback(() => updateCardsPositions( + focusedHighlight, + highlights, + cardsHeights, + getOffsetsForHighlight, + checkIfHiddenByCollapsedAncestor + ), [cardsHeights, focusedHighlight, getOffsetsForHighlight, highlights]); + // This creates a function that doesn't require dependency updates, for use by + // the resizeObserver effect. A little nicer than using a ref. + const [, dispatchPositions] = React.useReducer( + () => setCardsPositions(updatePositions()), + undefined + ); + + React.useEffect(() => dispatchPositions(), [updatePositions]); + + React.useEffect(() => { + const resizeObserver = new ResizeObserver(dispatchPositions); + resizeObserver.observe(container); + return () => resizeObserver.disconnect(); + }, [container]); + + return [cardsPositions, offsets]; +} + +function useCardsHeights() { const [cardsHeights, setCardsHeights] = React.useState>(new Map()); - const [offsets, setOffsets] = React.useState>(new Map()); - const [shouldFocusCard, setShouldFocusCard] = React.useState(false); + const onHeightChange = React.useCallback((id: string, ref: React.RefObject) => { + const height = ref.current ? ref.current.offsetHeight : 0; + if (cardsHeights.get(id) !== height) { + setCardsHeights((previous) => new Map(previous).set(id, height)); + } + }, [cardsHeights]); + + return [cardsHeights, onHeightChange] as const; +} + +function rangeString({startOffset, endOffset}: Highlight['range']) { + return `${startOffset}-${endOffset}`; +} + +const ELAPSED_LIMIT = 100; + +function useFocusedHighlight( + highlights: Highlight[], + element: React.RefObject, + container: HTMLElement +) { const focusedId = useSelector(focused); const focusedHighlight = React.useMemo( () => highlights.find((highlight) => highlight.id === focusedId), [focusedId, highlights]); - const setNewCardsPositionsRef = React.useRef<() => void>(); - const [isHiddenByEscape, dispatch] = React.useReducer( - editCardVisibilityHandler, - new Map(highlights.map((highlight) => [highlight.id, false])) - ); + const [shouldFocusCard, setShouldFocusCard] = React.useState(false); + const document = assertDocument(); + const previousRange = React.useRef(''); + const [dblclickStamp, setDblclickStamp] = React.useState(0); - // This function is triggered by keyboard shortcut defined in useKeyCombination(...) - // It moves focus between Card component and highlight in the content. - const moveFocus = React.useCallback((event: KeyboardEvent) => { - const activeElement = isHtmlElement(event.target) ? event.target : null; + React.useEffect(() => { + const handler = () => setDblclickStamp(Date.now()); + + document.addEventListener('dblclick', handler); + return () => document.removeEventListener('dblclick', handler); + }, [document]); + + // Tracking double clicks + // double-click events trigger updates to focusedHighlight, but the order and + // timing of processing can vary, so we check conditions within a short time + // of a double click. + React.useEffect(() => { + if (focusedHighlight) { + const elapsed = Date.now() - dblclickStamp; + const isDoubleClick = elapsed < ELAPSED_LIMIT; + + // Existing highlight + if (focusedHighlight.elements.length > 0) { + if (isDoubleClick) { + // Unselect text inside existing highlight. + document.getSelection()?.removeAllRanges(); + setShouldFocusCard(true); + } + return; + } + + // Text selection that could be a highlight + const newRange = rangeString(focusedHighlight.range); + const isExistingSelection = newRange === previousRange.current; + + if (isExistingSelection && isDoubleClick) { + setShouldFocusCard(true); + } + previousRange.current = newRange; + } + }, [focusedHighlight, document, dblclickStamp]); - if (!focusedHighlight || !activeElement || !element.current) { - return; + // Let Enter go from a highlight to the editor + const editOnEnter = React.useCallback(() => { + if (focusedHighlight) { + setShouldFocusCard(true); } + }, [focusedHighlight]); - if (element.current.contains(activeElement)) { + // This function is triggered by keyboard shortcut defined in useKeyCombination(...) + // It moves focus between Card component and highlight in the content. + const moveFocus = React.useCallback(({target}: KeyboardEvent) => { + const activeElement = isHtmlElement(target) ? target : null; + const cardIsFocused = focusedHighlight && element.current?.contains(activeElement); + + if (cardIsFocused) { focusedHighlight.focus(); - } else { - setShouldFocusCard(focusedId !== undefined); } - }, [focusedHighlight, focusedId]); + setShouldFocusCard(!cardIsFocused); + }, [element, focusedHighlight]); + + useKeyCombination({key: 'Enter'}, editOnEnter); + useKeyCombination(highlightKeyCombination, moveFocus, noopKeyCombinationHandler([container, element])); + // Clear shouldFocusCard when focus is lost from the CardWrapper. + // If we don't do this then card related for the focused highlight will be focused automatically. + useFocusLost(element, shouldFocusCard, React.useCallback(() => setShouldFocusCard(false), [])); + return [focusedHighlight, shouldFocusCard] as const; +} + +function CardsForHighlights({highlights, container, focusedHighlight, shouldFocusCard, highlighter}: { + highlights: Highlight[]; + container: HTMLElement; + focusedHighlight: Highlight | undefined; + shouldFocusCard: boolean; + highlighter: Highlighter; +}) { + const [cardsHeights, onHeightChange] = useCardsHeights(); + const [cardsPositions, offsets] = useCardPositionObserver( + container, + focusedHighlight, + highlights, + cardsHeights + ); + const [isHiddenByEscape, dispatch] = React.useReducer( + editCardVisibilityHandler, + new Map(highlights.map((highlight) => [highlight.id, false])) + ); const hideCard = () => { - dispatch({ type: 'HIDE', id: focusedId }); + focusedHighlight?.focus(); + dispatch({ type: 'HIDE', id: focusedHighlight?.id }); }; - const showCard = (cardId: string | undefined) => { dispatch({ type: 'SHOW', id: cardId }); }; - - useKeyCombination(highlightKeyCombination, moveFocus, noopKeyCombinationHandler([container, element])); - /* * Allow to show EditCard using Enter key * It is important to preserve the default behavior of Enter key */ - useKeyCombination({key: 'Enter'}, () => showCard(focusedId), undefined, false); + useKeyCombination({key: 'Enter'}, () => showCard(focusedHighlight?.id), undefined, false); // Allow to hide EditCard using Escape key useKeyCombination({key: 'Escape'}, hideCard, undefined, false); - - // Clear shouldFocusCard when focus is lost from the CardWrapper. - // If we don't do this then card related for the focused highlight will be focused automatically. - useFocusLost(element, shouldFocusCard, React.useCallback(() => setShouldFocusCard(false), [setShouldFocusCard])); useFocusHighlight(showCard, highlights); + return <> + {highlights.map((highlight, index) => { + const focusThisCard = shouldFocusCard && focusedHighlight === highlight; + return ) => onHeightChange(highlight.id, ref)} + zIndex={highlights.length - index} + shouldFocusCard={focusThisCard} + isHidden={checkIfHiddenByCollapsedAncestor(highlight) || isHiddenByEscape.get(highlight.id)} + />; + })} + ; +} - const onHeightChange = React.useCallback((id: string, ref: React.RefObject) => { - const height = ref.current && ref.current.offsetHeight; - if (cardsHeights.get(id) !== height) { - setCardsHeights((previous) => new Map(previous).set(id, height === null ? 0 : height)); - } - }, [cardsHeights]); - - const getOffsetsForHighlight = React.useCallback((highlight: Highlight) => { - const newOffsets = assertDefined( - getHighlightOffset(container, highlight), - `Couldn't get offsets for highlight with an id: ${highlight.id}` - ); - setOffsets((state) => new Map(state).set(highlight.id, newOffsets)); - return newOffsets; - }, [container]); - - const checkIfHiddenByCollapsedAncestor = (highlight: Highlight) => { - const highlightElement = highlight.elements[0] as HTMLElement; - const collapsedAncestor = highlightElement - ? highlightElement.closest('details[data-type="solution"]:not([open])') - : null; - return Boolean(collapsedAncestor); - }; - - React.useEffect(() => { - setNewCardsPositionsRef.current = () => { - const positions = updateCardsPositions( - focusedHighlight, - highlights, - cardsHeights, - getOffsetsForHighlight, - checkIfHiddenByCollapsedAncestor - ); - setCardsPositions(positions); - }; - setNewCardsPositionsRef.current(); - }, [cardsHeights, focusedHighlight, getOffsetsForHighlight, highlights]); - - React.useEffect(() => { - const resizeObserver = new ResizeObserver(() => { - assertDefined( - setNewCardsPositionsRef.current, - 'setNewCardsPositionsRef should be already defined by useEffect' - )(); - }); - resizeObserver.observe(container); - return () => { - resizeObserver.disconnect(); - }; - }, [container]); +// tslint:disable-next-line:variable-name +const Wrapper = ({highlights, className, container, highlighter}: WrapperProps) => { + const element = React.useRef(null); + const [focusedHighlight, shouldFocusCard] = useFocusedHighlight(highlights, element, container); - return highlights.length - ?
- {highlights.map((highlight, index) => { - const focusThisCard = shouldFocusCard && focusedId === highlight.id; - return ) => onHeightChange(highlight.id, ref)} - zIndex={highlights.length - index} - shouldFocusCard={focusThisCard} - isHidden={checkIfHiddenByCollapsedAncestor(highlight) || isHiddenByEscape.get(highlight.id)} - />; - })} -
- : null; + return
+ +
; }; +function MaybeWrapper(props: WrapperProps) { + if (!props.highlights.length) { + return null; + } + return ; +} + export default connect( (state: AppState) => ({ // These are used in the cardStyles.ts hasQuery: !!selectSearch.query(state), isTocOpen: contentSelect.tocOpen(state), }) -)(styled(Wrapper)` +)(styled(MaybeWrapper)` ${mainWrapperStyles} `); diff --git a/src/app/content/highlights/components/EditCard.tsx b/src/app/content/highlights/components/EditCard.tsx index ad071cf952..469ef328eb 100644 --- a/src/app/content/highlights/components/EditCard.tsx +++ b/src/app/content/highlights/components/EditCard.tsx @@ -454,24 +454,22 @@ function useOnColorChange(props: EditCardProps) { } function useSaveAnnotation( - props: EditCardProps, + { data, pageId, locationFilterId, highlight, onCancel }: EditCardProps, element: React.RefObject, pendingAnnotation: string ) { const dispatch = useDispatch(); const trackEditAnnotation = useAnalyticsEvent('editAnnotation'); - const { pageId, locationFilterId, highlight } = props; - const onCancel = props.onCancel; return React.useCallback( (toSave: HighlightData) => { - const data = assertDefined( - props.data, + const definedData = assertDefined( + data, 'Can\'t update highlight that doesn\'t exist' ); - const addedNote = data.annotation === undefined; - const { updatePayload, preUpdateData } = generateUpdatePayload(data, { + const addedNote = definedData.annotation === undefined; + const { updatePayload, preUpdateData } = generateUpdatePayload(definedData, { id: toSave.id, annotation: pendingAnnotation, }); @@ -490,6 +488,7 @@ function useSaveAnnotation( highlight.focus(); }, [ + data, dispatch, element, highlight, @@ -497,7 +496,6 @@ function useSaveAnnotation( onCancel, pageId, pendingAnnotation, - props.data, trackEditAnnotation, ] ); diff --git a/src/app/content/highlights/components/HighlightsPopUp.tsx b/src/app/content/highlights/components/HighlightsPopUp.tsx index 1a70cd4805..ead84fafc2 100644 --- a/src/app/content/highlights/components/HighlightsPopUp.tsx +++ b/src/app/content/highlights/components/HighlightsPopUp.tsx @@ -94,59 +94,63 @@ interface Props { loggedOut: boolean; } +function useCloseAndTrack(closeFn: () => void) { + const trackOpenCloseMH = useAnalyticsEvent('openCloseMH'); + + return React.useCallback((method: string) => () => { + closeFn(); + trackOpenCloseMH(method); + }, [closeFn, trackOpenCloseMH]); +} + // tslint:disable-next-line: variable-name -const HighlightsPopUp = ({ closeMyHighlights, ...props }: Props) => { +const HighlightsPopUp = ({ closeMyHighlights, ...props }: Omit) => { const popUpRef = React.useRef(null); const intl = useIntl(); - const trackOpenCloseMH = useAnalyticsEvent('openCloseMH'); + const closeAndTrack = useCloseAndTrack(closeMyHighlights); - const closeAndTrack = React.useCallback((method: string) => () => { - closeMyHighlights(); - trackOpenCloseMH(method); - }, [closeMyHighlights, trackOpenCloseMH]); + useOnEsc(true, closeAndTrack('esc')); - useOnEsc(props.myHighlightsOpen, closeAndTrack('esc')); + React.useEffect(() => popUpRef.current?.focus(), []); - React.useEffect(() => { - const popUp = popUpRef.current; + return +
+

+ + {(msg) => msg} + +

+ + + +
+ {props.user ? : } + +
; +}; - if (popUp && props.myHighlightsOpen) { - popUp.focus(); - } - }, [props.myHighlightsOpen]); +function MaybeHighlightsPopUp({myHighlightsOpen, ...props}: Props) { + if (!myHighlightsOpen) { + return null; + } - return props.myHighlightsOpen ? - -
-

- - {(msg) => msg} - -

- - - -
- {props.user ? : } - -
- : null; -}; + return ; +} export default connect( (state: AppState) => ({ @@ -158,4 +162,4 @@ export default connect( (dispatch: Dispatch) => ({ closeMyHighlights: () => dispatch(closeMyHighlightsAction()), }) -)(HighlightsPopUp); +)(MaybeHighlightsPopUp); diff --git a/src/app/content/highlights/components/__snapshots__/Card.spec.tsx.snap b/src/app/content/highlights/components/__snapshots__/Card.spec.tsx.snap index 4b1662d209..51cc57f7c1 100644 --- a/src/app/content/highlights/components/__snapshots__/Card.spec.tsx.snap +++ b/src/app/content/highlights/components/__snapshots__/Card.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`Card matches snapshot when focused without note 1`] = ` .c0 { - visibility: visible; + visibility: hidden; -webkit-animation: 600ms bcCCNc ease-out; animation: 600ms bcCCNc ease-out; display: block; @@ -121,7 +121,7 @@ exports[`Card matches snapshot when focused without note 1`] = ` exports[`Card matches snapshot when passed data without note 1`] = ` .c0 { - visibility: visible; + visibility: hidden; -webkit-animation: 600ms bcCCNc ease-out; animation: 600ms bcCCNc ease-out; display: block; @@ -215,7 +215,7 @@ exports[`Card matches snapshot when passed data without note 1`] = ` exports[`Card matches snapshot without data 1`] = ` .c0 { - visibility: visible; + visibility: hidden; -webkit-animation: 600ms bcCCNc ease-out; animation: 600ms bcCCNc ease-out; display: block; diff --git a/src/app/content/highlights/components/__snapshots__/ConfirmationModal.spec.tsx.snap b/src/app/content/highlights/components/__snapshots__/ConfirmationModal.spec.tsx.snap index e156bf36d5..a1252f3c76 100644 --- a/src/app/content/highlights/components/__snapshots__/ConfirmationModal.spec.tsx.snap +++ b/src/app/content/highlights/components/__snapshots__/ConfirmationModal.spec.tsx.snap @@ -68,7 +68,6 @@ exports[`ConfirmationModal matches snapshot 1`] = ` -ms-flex-align: center; align-items: center; margin: 0; - padding: 1.5rem 0; } .c7 { diff --git a/src/app/errors/components/__snapshots__/ErrorModal.spec.tsx.snap b/src/app/errors/components/__snapshots__/ErrorModal.spec.tsx.snap index b69a359458..39fb8e0785 100644 --- a/src/app/errors/components/__snapshots__/ErrorModal.spec.tsx.snap +++ b/src/app/errors/components/__snapshots__/ErrorModal.spec.tsx.snap @@ -68,7 +68,6 @@ exports[`ErrorModal matches snapshot 1`] = ` -ms-flex-align: center; align-items: center; margin: 0; - padding: 1.5rem 0; } .c7 { @@ -391,7 +390,6 @@ exports[`ErrorModal matches snapshots with recorded error ids 1`] = ` -ms-flex-align: center; align-items: center; margin: 0; - padding: 1.5rem 0; } .c7 { diff --git a/src/app/reactUtils.ts b/src/app/reactUtils.ts index 017c8fdf91..ce6d7de777 100644 --- a/src/app/reactUtils.ts +++ b/src/app/reactUtils.ts @@ -142,13 +142,11 @@ export const onFocusInOrOutHandler = ( }; export const useFocusLost = (ref: React.RefObject, isEnabled: boolean, cb: () => void) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - React.useEffect(onFocusInOrOutHandler(ref, isEnabled, cb, 'focusout'), [ref, isEnabled, cb]); + React.useEffect(() => onFocusInOrOutHandler(ref, isEnabled, cb, 'focusout')(), [ref, isEnabled, cb]); }; export const useFocusIn = (ref: React.RefObject, isEnabled: boolean, cb: () => void) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - React.useEffect(onFocusInOrOutHandler(ref, isEnabled, cb, 'focusin'), [ref, isEnabled, cb]); + React.useEffect(() => onFocusInOrOutHandler(ref, isEnabled, cb, 'focusin')(), [ref, isEnabled, cb]); }; export const onDOMEventHandler = ( @@ -184,23 +182,21 @@ export const useTimeout = (delay: number, callback: () => void) => { const timeout = React.useRef(); const timeoutHandler = () => savedCallback.current && savedCallback.current(); - const reset = () => { + const reset = React.useCallback(() => { if (timeout.current) { clearTimeout(timeout.current); } timeout.current = setTimeout(timeoutHandler, delay); - }; + }, [delay]); React.useEffect(() => { savedCallback.current = callback; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [callback]); React.useEffect(() => { reset(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [delay]); + }, [reset]); React.useEffect(() => () => clearTimeout(assertDefined(timeout.current, 'timeout ID can\'t be undefined')), []); @@ -260,8 +256,7 @@ export const useOnKey = ( isEnabled: boolean, cb: () => void ) => { - // eslint-disable-next-line react-hooks/exhaustive-deps - React.useEffect(onKeyHandler(config, element, isEnabled, cb), [config, element, isEnabled, cb]); + React.useEffect(() => onKeyHandler(config, element, isEnabled, cb)(), [config, element, isEnabled, cb]); }; export const onEscCallbacks: Array<() => void> = []; @@ -463,8 +458,7 @@ export const useKeyCombination = ( if (preventDefault) event.preventDefault(); callback(event); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [callback, options]); + }, [callback, noopHandler, options, preventDefault]); React.useEffect(() => { document.addEventListener('keydown', handler); @@ -492,16 +486,15 @@ export const useFocusHighlight = (showCard: (id: string) => void, highlights: Hi When clicking on a highlight, the target is a mark element and we need to find the first span inside it to get the highlight as expected */ - target = event.target.querySelectorAll('span')[0]; + target = event.target.querySelector('span'); } } else { target = event.target; } const highlight = highlights.find(h => - h.elements && h.elements.some(el => + h.elements && (h.elements as Element[]).some(el => el === target || - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (!!el && typeof (el as any).contains === 'function' && (el as any).contains(target)) + (!!el && typeof el.contains === 'function' && el.contains(target as Element)) ) );