0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

refactor(ui): refactor toast (#1928)

refactor taost using react modal
This commit is contained in:
simeng-li 2022-09-15 15:52:36 +08:00 committed by GitHub
parent 0ae13f091b
commit fa9bf16092
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 41 additions and 40 deletions

View file

@ -54,6 +54,7 @@
input { input {
padding-right: calc(24px + _.unit(4)); padding-right: calc(24px + _.unit(4));
outline: none;
} }
} }

View file

@ -21,14 +21,26 @@
background: var(--color-toast); background: var(--color-toast);
box-shadow: var(--shadow-2); box-shadow: var(--shadow-2);
text-align: center; text-align: center;
opacity: 0%;
transition: opacity 0.3s ease-in-out;
word-break: break-word; word-break: break-word;
pointer-events: auto;
}
&[data-visible='true'] { /* stylelint-disable selector-class-pattern */
:global {
.ReactModal__Content[role='toast'] {
opacity: 0%;
transition: opacity 0.3s ease-in-out;
}
.ReactModal__Content--after-open[role='toast'] {
opacity: 100%; opacity: 100%;
} }
.ReactModal__Content--before-close[role='toast'] {
opacity: 0%;
}
} }
/* stylelint-enable selector-class-pattern */
:global(body.desktop) { :global(body.desktop) {
.toast { .toast {

View file

@ -7,20 +7,20 @@ jest.useFakeTimers();
describe('Toast Component', () => { describe('Toast Component', () => {
it('showToast', () => { it('showToast', () => {
const message = 'mock toast message'; const message = 'mock toast message';
const { queryByText } = render(<Toast isVisible message={message} />); const { queryByText } = render(<Toast message={message} />);
expect(queryByText(message)).not.toBeNull(); expect(queryByText(message)).not.toBeNull();
}); });
it('should not render empty toast', () => { it('should not render empty toast', () => {
const message = 'mock toast message'; const message = 'mock toast message';
const { queryByText } = render(<Toast message={message} />); const { queryByText } = render(<Toast message="" />);
expect(queryByText(message)).toBeNull(); expect(queryByText(message)).toBeNull();
}); });
it('should run callback method when transition end', () => { it('should run callback method when transition end', () => {
const callback = jest.fn(); const callback = jest.fn();
const message = 'mock toast message'; const message = 'mock toast message';
const { container } = render(<Toast isVisible message={message} callback={callback} />); const { container } = render(<Toast message={message} callback={callback} />);
const toast = container.querySelector('[data-visible=true]'); const toast = container.querySelector('[data-visible=true]');
act(() => { act(() => {

View file

@ -1,59 +1,43 @@
import { useState, useRef, useEffect, useCallback } from 'react'; import { useEffect, useState } from 'react';
import ReactModal from 'react-modal';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
type Props = { type Props = {
message: string; message: string;
isVisible?: boolean;
duration?: number; duration?: number;
callback?: () => void; callback?: () => void;
}; };
const Toast = ({ message, isVisible = false, duration = 3000, callback }: Props) => { const Toast = ({ message, duration = 3000, callback }: Props) => {
const toastElement = useRef<HTMLDivElement>(null); const [text, setText] = useState('');
const [show, setShow] = useState(false);
const callbackHandler = useCallback(() => {
// Only execute on hide transitionend event
if (toastElement.current?.dataset.visible === 'true') {
return;
}
callback?.();
}, [callback]);
useEffect(() => { useEffect(() => {
if (!isVisible) { if (!message) {
return; return;
} }
setShow(true); setText(message);
const timer = setTimeout(() => { const timer = setTimeout(() => {
setShow(false); callback?.();
}, duration); }, duration);
return () => { return () => {
clearTimeout(timer); clearTimeout(timer);
setShow(false);
}; };
}, [callback, duration, isVisible]); }, [callback, duration, message, text]);
if (!isVisible) {
return null;
}
return ( return (
<div className={styles.toastContainer}> <ReactModal
<div role="toast"
ref={toastElement} isOpen={Boolean(message)}
className={styles.toast} overlayClassName={styles.toastContainer}
data-visible={show} className={styles.toast}
onTransitionEnd={callbackHandler} closeTimeoutMS={300}
> >
{message} {text}
</div> </ReactModal>
</div>
); );
}; };

View file

@ -41,7 +41,7 @@ const AppContent = ({ children }: Props) => {
{platform === 'web' && <div className={styles.placeHolder} />} {platform === 'web' && <div className={styles.placeHolder} />}
<main className={styles.content}>{children}</main> <main className={styles.content}>{children}</main>
{platform === 'web' && <div className={styles.placeHolder} />} {platform === 'web' && <div className={styles.placeHolder} />}
<Toast message={toast} isVisible={Boolean(toast)} callback={hideToast} /> <Toast message={toast} callback={hideToast} />
</div> </div>
); );
}; };

View file

@ -27,3 +27,7 @@ input::-webkit-inner-spin-button {
input[type='number'] { input[type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
:focus-visible {
outline: solid 1px var(--color-primary);
}