Modal

值得学习的是在 Modal 系列组件中 Alert/Promt 组件采用函数式进行弹框的调用。

蒙层

场景: 点击蒙层区, 内容弹框关闭蒙层消失; 点击内容弹框区, 内容弹框不关闭蒙层不消失;

有什么方法实现这个效果呢?

  • 方案一: target 与 currentTarget 的使用
<div>蒙层区</div> <!-- ① 只修饰样式 -->
<div>             <!-- 和 ① 相同的点击区域 -->
  <div>内容区</div>
</div>

伪代码逻辑: 在 JavaScript 中通过 targetcurrentTarget 区分

if (target === currentTarget) { // 点击蒙层
  // 关闭弹框
}
  • 方案二: z-index 的运用

滑动穿透解决方案

下面为解决移动端滑动穿透的解决方案:

  1. 首先先将 document 上的 touchmove 事件禁用掉。(防止遮罩层滚动)
  2. 然后再阻止浮层内容冒泡, 这样子浮层内容就能 touchmove 了。(使浮层中需滚动的元素可滚动)
  3. 最后对浮层的边界值做处理, 在浮层内容拉到最顶部(scrollTop 为 0) 还上滑动或者浮层内容拉到最底部(scrollTop + clientHeight >= scrollHeight) 还下滑动时 防止其滑动穿透, 所以需要对其进行 event.preventDefault()

具体代码如下:

import { isNotReachIOS } from './mobileDetect'

/* 该方法解决 ios 滑动穿透问题, 经测试安卓 8.0 版本也适用 */
let lockedNum = 0
let initialClientY = 0
let documentListenerAdded = false

const lockedElements: HTMLElement[] = []

const preventDefault = (event: Event) => {
  if (!event.cancelable) return
  event.preventDefault()
}

// 如果位于滚动元素的
const handleScroll = (event: TouchEvent, targetElement: HTMLElement) => {
  const clientY = event.targetTouches[0].clientY - initialClientY

  if (targetElement) {
    const { scrollTop, scrollHeight, clientHeight } = targetElement
    const isOnTop = clientY > 0 && scrollTop === 0
    const isOnBottom = clientY < 0 && scrollTop + clientHeight >= scrollHeight

    if (isOnTop || isOnBottom) {
      return preventDefault(event)
    }
  }

  event.stopPropagation()
  return false
}

/* 逻辑: 首先先将 document 上的 touchmove 事件禁用掉。(防止遮罩层滚动)
然后再将需要进行滚动的元素阻止其冒泡, 这样子滚动的元素就能 touchmove 了。(使弹框层中需滚动的元素可滚动)
最后对弹框层的边界值做处理, 在浮层内容拉到最顶部(scrollTop 为 0) 还上滑动或者浮层内容拉到最底部(scrollTop + clientHeight >= scrollHeight) 还下滑动时
要防止其滑动穿透, 所以需要对其进行 event.prevent */
const lock = (targetElement: HTMLElement) => {
  if (!documentListenerAdded) {
    // 针对 ios9 以下的机型需要单独处理
    isNotReachIOS(10)
      ? document.addEventListener('touchmove', preventDefault)
      : document.addEventListener('touchmove', preventDefault, { passive: false })
    documentListenerAdded = true
  }

  if (targetElement && lockedElements.indexOf(targetElement) === -1) {
    targetElement.ontouchstart = (event: TouchEvent) => {
      initialClientY = event.targetTouches[0].clientY
    }

    targetElement.ontouchmove = (event: TouchEvent) => {
      if (event.targetTouches.length !== 1) return
      handleScroll(event, targetElement)
    }

    lockedElements.push(targetElement)
    lockedNum += 1
  }
}

const unlock = (targetElement: HTMLElement) => {
  const index = lockedElements.indexOf(targetElement)
  if (index !== -1) {
    lockedNum -= 1
    targetElement.ontouchmove = null
    targetElement.ontouchstart = null
    lockedElements.splice(index, 1)
  }

  if (lockedNum === 0 && documentListenerAdded) {
    isNotReachIOS(10)
      ? (document as any).removeEventListener('touchmove', preventDefault)
      : (document as any).removeEventListener('touchmove', preventDefault, { passive: false })
    documentListenerAdded = false
  }
}

export { lock, unlock }

使用了上述方法后效果如下:

参考 滑动穿透(锁 body)终极探索

微信 ios 下 input 唤醒键盘视图错误兼容问题

该问题其实和 modal 没关系, 但是在这个场景中被遇到了, 在动图中可以看到 input 唤醒键盘后然后待键盘消失后, 键盘占用的那部分位置并没有被重新填充上, 使用了 scrollIntoView 也没有效果。

解决方案如下:

使用 focusinfocusout 能分别监听键盘的弹起与关闭, 在 focusout 事件中使用 window.scrollTo() 将 body 视图移动回原来位置。

实现代码如下:

// 微信 ios 下 input 唤醒键盘视图错误兼容问题
function HackWxIos() {
  ;(function() {
    let myFunction: any
    let isWXAndIos = isWeiXinAndIos()
    if (isWXAndIos) {
      // 保存当前视图的滚动位置, 供键盘关闭时使用;
      const originScrollY = window.scrollY
      // 既是微信浏览器 又是ios============(因为查到只有在微信环境下,ios手机上才会出现input失去焦点的时候页面被顶起)
      document.body.addEventListener('focusin', () => {
        // 软键盘弹起事件
        clearTimeout(myFunction)
      })
      document.body.addEventListener('focusout', () => {
        // 软键盘关闭事件
        clearTimeout(myFunction)
        myFunction = setTimeout(function() {
          window.scrollTo({ top: originScrollY, left: 0, behavior: 'smooth' }) // 重点  =======当键盘收起的时候让页面回到原始位置
        }, 200)
      })
    }
  })()

  function isWeiXinAndIos() {
    // window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,这个属性可以用来判断浏览器类型
    let ua = '' + window.navigator.userAgent.toLowerCase()
    // 通过正则表达式匹配ua中是否含有MicroMessenger字符串且是IOS系统
    let isWeixin = /MicroMessenger/i.test(ua) // 是在微信浏览器
    let isIos = /\(i[^;]+;( U;)? CPU.+Mac OS X/i.test(ua) // 是IOS系统
    return isWeixin && isIos
  }
}

export default HackWxIos

知乎 —— Modal 为什么设计成组件的形式,为啥不全设计成函数 api 直接调用?