diff --git a/playground/src/components/App.tsx b/playground/src/components/App.tsx index acde3d51..485404a0 100644 --- a/playground/src/components/App.tsx +++ b/playground/src/components/App.tsx @@ -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 ( +
+ -import React from 'react'; -import { Id, toast, ToastContainer } from '../../../src'; -import { defaultProps } from '../../../src/components/ToastContainer'; +
-// 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 }) => ( -
  • - -
  • - )); - } - - render() { - return ( -
    -
    -
    -

    - 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! -

    -
    -
    -

    Position

    -
      - -
    -
    -
    -

    Type

    -
      - -
    -
    -
    -

    Options

    -
    - - - - - -
    -
      {this.renderFlags()}
    -
    -
    -
    - - -
    -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    - - - -
    - ); - } + +
    + +
    + ); } export { App }; diff --git a/src/components/ToastContainer.tsx b/src/components/ToastContainer.tsx index a3d22b2c..94d2adac 100644 --- a/src/components/ToastContainer.tsx +++ b/src/components/ToastContainer.tsx @@ -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(null); const { getToastToRender, isToastActive, count } = useToastContainer(containerProps); const { className, style, rtl, containerId, hotKeys } = containerProps; @@ -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"]'); @@ -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 (
    {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 (