Commit 61f176d0 authored by Andrey Azov's avatar Andrey Azov
Browse files

Add nuances to behaviour and refactor to useReducer

parent 56652ff1
Pipeline #196246 passed with stages
in 4 minutes and 26 seconds
......@@ -46,6 +46,24 @@ describe('<QuestionButton />', () => {
});
expect(queryByText(helpMessage)).toBeTruthy();
});
it('hides the tooltip on mouseleave', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
) as HTMLElement;
userEvent.hover(questionButton);
act(() => {
jest.advanceTimersByTime(TOOLTIP_TIMEOUT + 10);
});
userEvent.unhover(questionButton);
expect(queryByText(helpMessage)).toBeFalsy();
});
});
describe('on click', () => {
......@@ -87,5 +105,24 @@ describe('<QuestionButton />', () => {
});
expect(queryByText(helpMessage)).toBeTruthy();
});
it('does not hide the tooltip on mouseleave', () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
) as HTMLElement;
userEvent.click(questionButton);
act(() => {
// give the Tooltip component a little time to complete its async tasks
jest.advanceTimersByTime(TOOLTIP_TIMEOUT + 10);
});
userEvent.unhover(questionButton); // <-- should have no effect on the tooltip
expect(queryByText(helpMessage)).toBeTruthy();
});
});
});
......@@ -14,12 +14,10 @@
* limitations under the License.
*/
import React, { useState, useEffect } from 'react';
import React from 'react';
import classNames from 'classnames';
import useHover from 'src/shared/hooks/useHover';
import { TOOLTIP_TIMEOUT } from 'src/shared/components/tooltip/tooltip-constants';
import { useQuestionButtonBehavior } from './useQuestionButtonBehavior';
import Tooltip from 'src/shared/components/tooltip/Tooltip';
import { ReactComponent as QuestionIcon } from './question_circle.svg';
......@@ -39,36 +37,12 @@ type Props = {
};
const QuestionButton = (props: Props) => {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
const [shouldShowTooltip, setShouldShowTooltip] = useState(isHovered);
let timeoutId: number | null = null;
useEffect(() => {
if (isHovered) {
timeoutId = window.setTimeout(() => {
setShouldShowTooltip(isHovered);
}, TOOLTIP_TIMEOUT);
}
return () => {
timeoutId && clearTimeout(timeoutId);
};
}, [isHovered]);
const handleClick = () => {
timeoutId && clearTimeout(timeoutId); // overrides the timer started by hover
timeoutId = null;
setShouldShowTooltip(!shouldShowTooltip);
};
const hideTooltip = () => {
// tooltip will detect when user starts scrolling
// and will send a signal to the parent component so that it can be removed
setTimeout(() => {
// bump this to the next event loop to give the click event time to register and call the click handler
shouldShowTooltip && setShouldShowTooltip(false);
}, 0);
};
const {
questionButtonRef,
onClick,
onTooltipCloseSignal,
shouldShowTooltip
} = useQuestionButtonBehavior();
const className = classNames(
defaultStyles.questionButton,
......@@ -79,13 +53,13 @@ const QuestionButton = (props: Props) => {
);
return (
<div ref={hoverRef} className={className} onClick={handleClick}>
<div ref={questionButtonRef} className={className} onClick={onClick}>
<QuestionIcon />
{shouldShowTooltip && (
<Tooltip
anchor={hoverRef.current}
anchor={questionButtonRef.current}
autoAdjust={true}
onClose={hideTooltip}
onClose={onTooltipCloseSignal}
delay={0}
>
{props.helpText}
......
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useReducer, useEffect } from 'react';
import useHover from 'src/shared/hooks/useHover';
import { TOOLTIP_TIMEOUT } from 'src/shared/components/tooltip/tooltip-constants';
type EventType = 'click' | 'hover';
type State = {
event: EventType | null;
isTooltipShown: boolean;
};
type ShowOnHoverAction = {
type: 'showTooltipOnHover';
};
type ShowOnClickAction = {
type: 'showTooltipOnClick';
};
type HideAction = {
type: 'hideTooltip';
};
type Action = ShowOnHoverAction | ShowOnClickAction | HideAction;
const reducer = (_: State, action: Action): State => {
switch (action.type) {
case 'showTooltipOnHover':
return { event: 'hover', isTooltipShown: true };
case 'showTooltipOnClick':
return { event: 'click', isTooltipShown: true };
case 'hideTooltip':
return initialState;
}
};
const initialState: State = {
event: null,
isTooltipShown: false
};
export const useQuestionButtonBehavior = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
let timeoutId: number | null = null;
useEffect(() => {
if (isHovered) {
timeoutId = window.setTimeout(() => {
dispatch({ type: 'showTooltipOnHover' });
}, TOOLTIP_TIMEOUT);
} else if (state.isTooltipShown && state.event === 'hover') {
// only hide the tooltip on mouseout if the tooltip was shown due to a hover event
dispatch({ type: 'hideTooltip' });
}
return () => {
timeoutId && clearTimeout(timeoutId);
};
}, [isHovered]);
const handleClick = () => {
cancelTimeout(); // cancel the timer started by hover
if (!state.isTooltipShown) {
dispatch({ type: 'showTooltipOnClick' });
} else {
dispatch({ type: 'hideTooltip' });
}
};
const cancelTimeout = () => {
timeoutId && clearTimeout(timeoutId);
timeoutId = null;
};
const onTooltipCloseSignal = () => {
// tooltip will send a signal to hide it when user click outside of the tooltip
// or when they start scrolling
setTimeout(() => {
// bump this to the next event loop to give the click event time to register and call the click handler
dispatch({ type: 'hideTooltip' });
}, 0);
};
return {
questionButtonRef: hoverRef,
onClick: handleClick,
onTooltipCloseSignal,
shouldShowTooltip: state.isTooltipShown
};
};
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment