Unverified Commit 0f53e582 authored by Ridwan Amode's avatar Ridwan Amode Committed by GitHub
Browse files

Added alert button in storybook (#751)

* icon can be either an error icon (red) or warning icon (orange) or just an icon on its own with no tooltip

* move hook to shared/hooks and create Test for hook

* replace alert icon with shared component in unsupported-browser page
parent 40577fb0
Pipeline #281156 passed with stages
in 5 minutes and 3 seconds
@import 'src/styles/common';
.alertButton {
padding: 2px;
user-select: none;
> svg {
width: var(--alert-icon-width, 18px);
height: var(--alert-icon-height, 18px);
cursor: pointer;
}
}
.alertButtonRed svg {
fill: $red;
}
.alertButtonAmber svg {
fill: $orange;
}
.noTooltip svg {
cursor: auto;
}
/**
* 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 React from 'react';
import classNames from 'classnames';
import AlertIcon from 'static/icons/icon_alert_circle.svg';
import { useShowTooltip } from 'src/shared/hooks/useShowTooltip';
import Tooltip from 'src/shared/components/tooltip/Tooltip';
import styles from './AlertButton.scss';
type Props = {
tooltipContent?: React.ReactNode;
level?: 'red' | 'amber';
className?: string;
};
const AlertButton = (props: Props) => {
const { level: alertLevel = 'red', tooltipContent } = props;
const { elementRef, onClick, onTooltipCloseSignal, shouldShowTooltip } =
useShowTooltip();
const alertButtonClass = classNames(
styles.alertButton,
props.className,
{ [styles.alertButtonRed]: alertLevel === 'red' },
{ [styles.alertButtonAmber]: alertLevel === 'amber' },
{ [styles.noTooltip]: !props.tooltipContent }
);
return (
<div ref={elementRef} className={alertButtonClass} onClick={onClick}>
<AlertIcon />
{tooltipContent && shouldShowTooltip && (
<Tooltip
anchor={elementRef.current}
autoAdjust={true}
onClose={onTooltipCloseSignal}
delay={0}
>
{tooltipContent}
</Tooltip>
)}
</div>
);
};
export default AlertButton;
......@@ -46,7 +46,8 @@
font-size: 12px;
}
.reloadButton, .homeButton {
.reloadButton,
.homeButton {
padding-top: 40px;
}
......@@ -91,21 +92,19 @@
margin-left: 25px;
}
}
.vennIntersection{
.vennIntersection {
padding-left: 19px;
.infoText{
.infoText {
font-size: 18px;
font-weight: bold;
}
.infoIcon {
svg {
fill: $red;
width: 24px;
height: 24px;
--alert-icon-width: 20px;
--alert-icon-height: 20px;
}
}
}
}
......@@ -25,7 +25,7 @@ import { Topbar } from 'src/header/Header';
import ShowHide from '../show-hide/ShowHide';
import { PrimaryButton } from '../button/Button';
import InfoIcon from 'static/icons/icon_alert_circle.svg';
import AlertButton from 'src/shared/components/alert-button/AlertButton';
import styles from './ErrorScreen.scss';
......@@ -60,9 +60,7 @@ const GeneralErrorScreen = () => {
className={classNames(styles.vennCircle, styles.vennCircleRight)}
>
<div className={styles.vennIntersection}>
<div className={styles.infoIcon}>
<InfoIcon />
</div>
<AlertButton className={styles.infoIcon} />
</div>
<span>...now we need you to do something</span>
</div>
......
......@@ -17,7 +17,7 @@
import React from 'react';
import classNames from 'classnames';
import { useQuestionButtonBehaviour } from './useQuestionButtonBehaviour';
import { useShowTooltip } from 'src/shared/hooks/useShowTooltip';
import Tooltip from 'src/shared/components/tooltip/Tooltip';
......@@ -38,12 +38,8 @@ type Props = {
};
const QuestionButton = (props: Props) => {
const {
questionButtonRef,
onClick,
onTooltipCloseSignal,
shouldShowTooltip
} = useQuestionButtonBehaviour();
const { elementRef, onClick, onTooltipCloseSignal, shouldShowTooltip } =
useShowTooltip();
const className = classNames(
defaultStyles.questionButton,
......@@ -54,11 +50,11 @@ const QuestionButton = (props: Props) => {
);
return (
<div ref={questionButtonRef} className={className} onClick={onClick}>
<div ref={elementRef} className={className} onClick={onClick}>
<QuestionIcon />
{shouldShowTooltip && (
<Tooltip
anchor={questionButtonRef.current}
anchor={elementRef.current}
autoAdjust={true}
onClose={onTooltipCloseSignal}
delay={0}
......
......@@ -18,7 +18,9 @@ import React from 'react';
import { render, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import QuestionButton from './QuestionButton';
import { useShowTooltip } from '../useShowTooltip';
import Tooltip from 'src/shared/components/tooltip/Tooltip';
import { TOOLTIP_TIMEOUT } from 'src/shared/components/tooltip/tooltip-constants';
......@@ -30,110 +32,121 @@ const userEventWithoutDelay = userEvent.setup({
delay: null
});
describe('<QuestionButton />', () => {
const helpMessage = 'I am a helpful message that will appear in the tooltip';
const helpText = <span>{helpMessage}</span>;
const TestComponent = () => {
const { elementRef, onClick, onTooltipCloseSignal, shouldShowTooltip } =
useShowTooltip();
return (
<div className="test-element" ref={elementRef} onClick={onClick}>
Element with tooltip
{shouldShowTooltip && (
<Tooltip
anchor={elementRef.current}
autoAdjust={true}
onClose={onTooltipCloseSignal}
delay={0}
>
TooltipText
</Tooltip>
)}
</div>
);
};
describe('useShowTooltip', () => {
afterEach(() => {
jest.resetAllMocks();
});
beforeAll(() => {
jest.useFakeTimers();
});
describe('on hover', () => {
it('shows the tooltip after a delay', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
describe('useShowTooltip with on click', () => {
it('toggles the tooltip', async () => {
const { container, queryByText } = render(<TestComponent />);
const testElement = container.querySelector(
'.test-element'
) as HTMLElement;
await userEventWithoutDelay.hover(questionButton);
expect(questionButton.querySelector('.pointerBox')).toBeFalsy();
// moving the timer to simulate the delay before the tooltip is shown
act(() => {
jest.advanceTimersByTime(TOOLTIP_TIMEOUT);
});
await userEventWithoutDelay.click(testElement);
// moving the timer to allow the tooltip to appear
act(() => {
jest.advanceTimersByTime(0);
});
expect(queryByText(helpMessage)).toBeTruthy();
expect(queryByText('TooltipText')).toBeTruthy();
});
it('hides the tooltip on mouseleave', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
it('takes precedence over hover', async () => {
const { container, queryByText } = render(<TestComponent />);
const testElement = container.querySelector(
'.test-element'
) as HTMLElement;
await userEventWithoutDelay.hover(questionButton);
await userEventWithoutDelay.hover(testElement); // would start the timer
await userEventWithoutDelay.click(testElement); // would clear the timer and toggle the state to show the tooltip
// making sure that the tooltip got shown
await waitFor(() => {
expect(queryByText(helpMessage)).toBeTruthy();
act(() => {
jest.advanceTimersByTime(0); // the tooltip should appear instantaneously
});
await userEvent.unhover(questionButton);
expect(queryByText(helpMessage)).toBeFalsy();
expect(queryByText('TooltipText')).toBeTruthy();
});
});
describe('on click', () => {
it('toggles the tooltip', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
it('does not hide the tooltip on mouseleave', async () => {
const { container, queryByText } = render(<TestComponent />);
const testElement = container.querySelector(
'.test-element'
) as HTMLElement;
await userEventWithoutDelay.click(questionButton);
await userEventWithoutDelay.click(testElement);
act(() => {
jest.advanceTimersByTime(0);
});
expect(queryByText(helpMessage)).toBeTruthy();
await userEventWithoutDelay.unhover(testElement); // <-- should have no effect on the tooltip
expect(queryByText('TooltipText')).toBeTruthy();
});
});
it('takes precedence over hover', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
describe('useShowTooltip with on hover', () => {
it('shows the tooltip after a delay', async () => {
const { container, queryByText } = render(<TestComponent />);
const testElement = container.querySelector(
'.test-element'
) as HTMLElement;
await userEventWithoutDelay.hover(questionButton); // would start the timer
await userEventWithoutDelay.click(questionButton); // would clear the timer and toggle the state to show the tooltip
await userEventWithoutDelay.hover(testElement);
expect(testElement.querySelector('.pointerBox')).toBeFalsy();
// moving the timer to simulate the delay before the tooltip is shown
act(() => {
jest.advanceTimersByTime(0); // the tooltip should appear instantaneously
jest.advanceTimersByTime(TOOLTIP_TIMEOUT);
});
expect(queryByText(helpMessage)).toBeTruthy();
// moving the timer to allow the tooltip to appear
act(() => {
jest.advanceTimersByTime(0);
});
expect(queryByText('TooltipText')).toBeTruthy();
});
it('does not hide the tooltip on mouseleave', async () => {
const { container, queryByText } = render(
<QuestionButton helpText={helpText} />
);
const questionButton = container.querySelector(
'.questionButton'
it('hides the tooltip on mouseleave', async () => {
const { container, queryByText } = render(<TestComponent />);
const testElement = container.querySelector(
'.test-element'
) as HTMLElement;
await userEventWithoutDelay.click(questionButton);
await userEventWithoutDelay.hover(testElement);
act(() => {
jest.advanceTimersByTime(0);
// making sure that the tooltip got shown
await waitFor(() => {
expect(queryByText('TooltipText')).toBeTruthy();
});
await userEventWithoutDelay.unhover(questionButton); // <-- should have no effect on the tooltip
expect(queryByText(helpMessage)).toBeTruthy();
await userEvent.unhover(testElement);
expect(queryByText('TooltipText')).toBeFalsy();
});
});
});
......@@ -16,7 +16,7 @@
import { useReducer, useEffect } from 'react';
import useHover from 'src/shared/hooks/useHover';
import useHover from './useHover';
import { TOOLTIP_TIMEOUT } from 'src/shared/components/tooltip/tooltip-constants';
......@@ -57,7 +57,7 @@ const initialState: State = {
isTooltipShown: false
};
export const useQuestionButtonBehaviour = () => {
export const useShowTooltip = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
......@@ -102,7 +102,7 @@ export const useQuestionButtonBehaviour = () => {
};
return {
questionButtonRef: hoverRef,
elementRef: hoverRef,
onClick: handleClick,
onTooltipCloseSignal,
shouldShowTooltip: state.isTooltipShown
......
.text {
margin-right: 1rem;
}
.alertIcon {
display: inline-block;
position: relative;
top: 4px;
}
.largeAlertIcon {
--alert-icon-width: 30px;
--alert-icon-height: 30px;
display: inline-block;
position: relative;
top: 10px;
}
/**
* 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 React from 'react';
import AlertButton from 'src/shared/components/alert-button/AlertButton';
import styles from './AlertButton.stories.scss';
export default {
title: 'Components/Shared Components/Alert button'
};
export const ErrorAlertButtonStory = () => (
<div>
<span className={styles.text}>Some error has occured</span>
<AlertButton tooltipContent="This is a hint" className={styles.alertIcon} />
</div>
);
ErrorAlertButtonStory.storyName = 'Error icon with tooltip';
export const WarningAlertButtonStory = () => (
<div>
<span className={styles.text}>Some warning to show</span>
<AlertButton
tooltipContent="This is a hint"
level="amber"
className={styles.alertIcon}
/>
</div>
);
WarningAlertButtonStory.storyName = 'Warning icon with tooltip';
export const ErrorAlertButtonOnlyStory = () => (
<div>
<span className={styles.text}>Bigger error icon</span>
<AlertButton className={styles.largeAlertIcon} />
</div>
);
ErrorAlertButtonOnlyStory.storyName = 'Error icon with no tooltip';
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