Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 11 additions & 257 deletions playground/src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,263 +1,17 @@
/**
* The playground could use some love 💖. To the brave soul reading this
* message, any help would be appreciated 🙏
*
* The code is full of bad assertion 😆
*/
import { ToastContainer, toast } from '../../../src';

import { Checkbox } from './Checkbox';
import { ContainerCode, ContainerCodeProps } from './ContainerCode';
import { Header } from './Header';
import { Radio } from './Radio';
import { ToastCode, ToastCodeProps } from './ToastCode';
import { flags, positions, themes, transitions, typs } from './constants';
function App() {
return (
<div style={{ padding: 40 }}>
<button onClick={() => toast('Test', { autoClose: 30000 })}>Show Toast</button>

import React from 'react';
import { Id, toast, ToastContainer } from '../../../src';
import { defaultProps } from '../../../src/components/ToastContainer';
<div style={{ height: '120vh' }} />

// Attach to window. Can be useful to debug
// @ts-ignore
window.toast = toast;

class App extends React.Component {
state = App.getDefaultState();
toastId: Id;
resolvePromise = true;

static getDefaultState() {
return {
...defaultProps,
transition: 'bounce',
type: 'default',
progress: '',
disableAutoClose: false,
limit: 0,
theme: 'light'
};
}

handleReset = () =>
this.setState({
...App.getDefaultState()
});

clearAll = () => toast.dismiss();

showToast = () => {
this.toastId =
this.state.type === 'default'
? toast('🦄 Wow so easy !', { progress: this.state.progress })
: toast[this.state.type]('🚀 Wow so easy !', {
progress: this.state.progress
});
};

firePromise = () => {
toast.promise(
new Promise((resolve, reject) => {
setTimeout(() => {
this.resolvePromise ? resolve(null) : reject(null);
this.resolvePromise = !this.resolvePromise;
}, 3000);
}),
{
pending: 'Promise is pending',
success: 'Promise resolved 👌',
error: 'Promise rejected 🤯'
}
);
};

updateToast = () => toast.update(this.toastId, { progress: this.state.progress });

handleAutoCloseDelay = e =>
this.setState({
autoClose: e.target.value > 0 ? parseInt(e.target.value, 10) : 1
});

isDefaultProps() {
return (
this.state.position === 'top-right' &&
this.state.autoClose === 5000 &&
!this.state.disableAutoClose &&
!this.state.hideProgressBar &&
!this.state.newestOnTop &&
!this.state.rtl &&
this.state.pauseOnFocusLoss &&
this.state.pauseOnHover &&
this.state.closeOnClick &&
this.state.draggable &&
this.state.theme === 'light'
);
}

handleRadioOrSelect = e =>
this.setState({
[e.target.name]: e.target.name === 'limit' ? parseInt(e.target.value, 10) : e.target.value
});

toggleCheckbox = e =>
this.setState({
[e.target.name]: !this.state[e.target.name]
});

renderFlags() {
return flags.map(({ id, label }) => (
<li key={id}>
<Checkbox id={id} label={label} onChange={this.toggleCheckbox} checked={this.state[id]} />
</li>
));
}

render() {
return (
<main>
<Header />
<div className="container">
<p>
By default, all toasts will inherit ToastContainer's props. Props defined on toast supersede
ToastContainer's props. Props marked with * can only be set on the ToastContainer. The demo is not
exhaustive, check the repo for more!
</p>
<section className="container__options">
<div>
<h3>Position</h3>
<ul>
<Radio
options={positions}
name="position"
checked={this.state.position as string}
onChange={this.handleRadioOrSelect}
/>
</ul>
</div>
<div>
<h3>Type</h3>
<ul>
<Radio options={typs} name="type" checked={this.state.type} onChange={this.handleRadioOrSelect} />
</ul>
</div>
<div>
<h3>Options</h3>
<div className="options_wrapper">
<label htmlFor="autoClose">
Delay
<input
type="number"
name="autoClose"
id="autoClose"
value={this.state.autoClose as unknown as string}
onChange={this.handleAutoCloseDelay}
disabled={this.state.disableAutoClose}
/>
ms
</label>
<label htmlFor="transition">
Transition
<select
name="transition"
id="transition"
onChange={this.handleRadioOrSelect}
value={this.state.transition}
>
{Object.keys(transitions).map(k => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</label>
<label htmlFor="theme">
Theme
<select name="theme" id="theme" onChange={this.handleRadioOrSelect} value={this.state.theme}>
{themes.map(k => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</label>
<label htmlFor="progress">
Progress
<input
type="number"
name="progress"
id="progress"
value={this.state.progress}
onChange={this.handleRadioOrSelect}
/>
</label>
<label htmlFor="limit">
Limit
<input
type="number"
name="limit"
id="limit"
value={this.state.limit}
onChange={this.handleRadioOrSelect}
/>
</label>
</div>
<ul>{this.renderFlags()}</ul>
</div>
</section>
<section>
<ContainerCode
{...(this.state as unknown as ContainerCodeProps)}
isDefaultProps={this.isDefaultProps() as boolean}
/>
<ToastCode {...(this.state as unknown as ToastCodeProps)} />
</section>
<div className="cta__wrapper">
<ul className="container__actions">
<li>
<button className="btn" onClick={this.showToast}>
<span role="img" aria-label="show alert">
🚀
</span>{' '}
Show Toast
</button>
</li>
<li>
<button className="btn" onClick={this.firePromise}>
Promise
</button>
</li>
<li>
<button className="btn" onClick={this.updateToast}>
Update
</button>
</li>
<li>
<button className="btn bg-red" onClick={this.clearAll}>
<span role="img" aria-label="clear all">
💩
</span>{' '}
Clear All
</button>
</li>
<li>
<button className="btn bg-blue" onClick={this.handleReset}>
<span role="img" aria-label="reset options">
🔄
</span>{' '}
Reset
</button>
</li>
</ul>
</div>
</div>
<ToastContainer
{...this.state}
transition={transitions[this.state.transition]}
autoClose={this.state.disableAutoClose ? false : this.state.autoClose}
/>
<ToastContainer containerId="xxx" position="top-left" autoClose={false} theme="dark" limit={3} />
<ToastContainer limit={3} containerId="yyy" autoClose={false} position="top-right" />
</main>
);
}
<input placeholder="Tap to open keyboard" style={{ padding: 10, fontSize: 16 }} />
<div style={{ height: '120vh' }} />
<ToastContainer position="top-right" />
</div>
);
}

export { App };
62 changes: 57 additions & 5 deletions src/components/ToastContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function ToastContainer(props: ToastContainerProps) {
};
const stacked = props.stacked;
const [collapsed, setIsCollapsed] = useState(true);
const [visualViewportState, setVisualViewportState] = useState<{
offsetTop: number;
offsetLeft: number;
} | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { getToastToRender, isToastActive, count } = useToastContainer(containerProps);
const { className, style, rtl, containerId, hotKeys } = containerProps;
Expand All @@ -58,6 +62,14 @@ export function ToastContainer(props: ToastContainerProps) {
}
}

function isIOSMobile() {
if (typeof navigator === 'undefined') return false;
const ua = navigator.userAgent;
const iOS = /iPad|iPhone|iPod/.test(ua);
const iPadOS13Plus = ua.includes('Mac') && 'ontouchend' in document;
return iOS || iPadOS13Plus;
}

useIsomorphicLayoutEffect(() => {
if (stacked) {
const nodes = containerRef.current!.querySelectorAll('[data-in="true"]');
Expand Down Expand Up @@ -103,11 +115,35 @@ export function ToastContainer(props: ToastContainerProps) {
}

document.addEventListener('keydown', focusFirst);
return () => document.removeEventListener('keydown', focusFirst);
}, [hotKeys]);

useEffect(() => {
if (!isIOSMobile()) return;
const vv = window.visualViewport;
if (!vv) return;

const update = () => {
// offsetTop: distance between the top of the visual viewport and the top of the layout viewport.
// offsetLeft: same for horizontal axis.
// These change when the keyboard opens or the user scrolls on iOS.
// By applying these as a CSS translate we re-anchor `position: fixed` elements
// (which are fixed to the layout viewport on iOS) back to the visible screen.
setVisualViewportState({
offsetTop: vv.offsetTop,
offsetLeft: vv.offsetLeft
});
};

vv.addEventListener('resize', update);
vv.addEventListener('scroll', update);
update();

return () => {
document.removeEventListener('keydown', focusFirst);
vv.removeEventListener('resize', update);
vv.removeEventListener('scroll', update);
};
}, [hotKeys]);
}, []);

return (
<section
Expand All @@ -127,9 +163,25 @@ export function ToastContainer(props: ToastContainerProps) {
aria-label={containerProps['aria-label']}
>
{getToastToRender((position, toastList) => {
const containerStyle: React.CSSProperties = !toastList.length
? { ...style, pointerEvents: 'none' }
: { ...style };
const isTop = position.includes('top');
const isCenter = position.includes('center');

let containerStyle: React.CSSProperties = {
...style,
...(toastList.length ? {} : { pointerEvents: 'none' })
};

if (isTop && visualViewportState) {
const { offsetTop, offsetLeft } = visualViewportState;
// Translate the container by the visual viewport's offset.
// This counteracts iOS's behaviour of fixing elements to the layout viewport
// so the toast stays pinned to the actual visible corner of the screen.
const existingTransform = isCenter ? 'translateX(-50%) ' : '';
containerStyle = {
...containerStyle,
transform: `${existingTransform}translate(${offsetLeft}px, ${offsetTop}px)`
};
}

return (
<div
Expand Down