精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

一個隱藏的 HTML 屬性幫我省下了 500 行 JavaScript 代碼

開發 前端
如果你寫過稍微像樣一點的 Web 應用,你八成也干過這些事。 各種 modal、dropdown、tooltip……邏輯基本如出一轍。直到那天,我刷著 MDN,突然看到一個東西—— 它讓我懷疑:我這些年,是不是都在重復造瀏覽器已經造好的輪子?

上周二,我在改一個“遠古”管理后臺的時候,被自己氣笑了。

為了維護幾個彈窗,我居然堆了 500 多行 JavaScript

  • 管理焦點的 focus trap
  • 監聽 Esc 關閉
  • 點擊遮罩關閉
  • 一堆 ARIA 無障礙屬性
  • 禁止 body 滾動
  • 各種事件綁定、解綁、邊界情況……

如果你寫過稍微像樣一點的 Web 應用,你八成也干過這些事。 各種 modal、dropdown、tooltip……邏輯基本如出一轍。

直到那天,我刷著 MDN,突然看到一個東西—— 它讓我懷疑:我這些年,是不是都在重復造瀏覽器已經造好的輪子?

一個 原生 HTML 屬性,不需要任何庫、不依賴任何框架, 只加上一個單詞,就能幫你搞定:

可訪問性、鍵盤導航、焦點管理、關閉行為……

而且全部是 瀏覽器級別實現。

今天就來完整拆解這個玩意: ——那個能幫你刪掉幾百行 JS 的屬性:popover

那些年,我們為一個彈窗寫出的屎山

傳統做法,大概都長這樣。

先寫一個 div,再加一坨事件監聽、焦點管理、鍵盤處理…… 最后再祈禱:別在某個奇怪場景下突然炸掉。

class Modal {
  constructor(element) {
    this.element = element;
    this.overlay = element.querySelector('.modal-overlay');
    this.closeBtn = element.querySelector('.modal-close');
    this.focusableElements = [];
    this.previousFocus = null;
    this.isOpen = false;
    
    this.bindEvents();
  }
  
  open() {
    // 保存當前焦點
    this.previousFocus = document.activeElement;
    
    // 顯示彈窗
    this.element.classList.add('is-open');
    this.isOpen = true;
    
    // 禁止 body 滾動
    document.body.style.overflow = 'hidden';
    
    // 設置焦點陷阱
    this.trapFocus();
    
    // 聚焦第一個可聚焦元素
    this.focusFirstElement();
    
    // 給讀屏工具一個信號
    this.element.setAttribute('aria-hidden', 'false');
  }
  
  close() {
    this.element.classList.remove('is-open');
    this.isOpen = false;
    
    // 恢復 body 滾動
    document.body.style.overflow = '';
    
    // 把焦點還給觸發按鈕
    if (this.previousFocus) {
      this.previousFocus.focus();
    }
    
    this.element.setAttribute('aria-hidden', 'true');
  }
  
  trapFocus() {
    // 找出所有可聚焦元素
    this.focusableElements = Array.from(
      this.element.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      )
    );
    
    const firstElement = this.focusableElements[0];
    const lastElement = this.focusableElements[this.focusableElements.length - 1];
    
    this.element.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        if (e.shiftKey) {
          // Shift + Tab
          if (document.activeElement === firstElement) {
            e.preventDefault();
            lastElement.focus();
          }
        } else {
          // Tab
          if (document.activeElement === lastElement) {
            e.preventDefault();
            firstElement.focus();
          }
        }
      }
    });
  }
  
  focusFirstElement() {
    const firstFocusable = this.element.querySelector(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    if (firstFocusable) {
      firstFocusable.focus();
    }
  }
  
  bindEvents() {
    // 關閉按鈕
    this.closeBtn.addEventListener('click', () => this.close());
    
    // 點擊遮罩關閉
    this.overlay.addEventListener('click', (e) => {
      if (e.target === this.overlay) {
        this.close();
      }
    });
    
    // Esc 關閉
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.isOpen) {
        this.close();
      }
    });
  }
}

// 初始化
const modal = new Modal(document.getElementById('my-modal'));
document.getElementById('open-modal').addEventListener('click', () => {
  modal.open();
});

JS 寫完,還要配一大坨 CSS:

.modal {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
}

.modal.is-open {
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
}

.modal-content {
  position: relative;
  background: white;
  padding: 2rem;
  border-radius: 8px;
  max-width: 500px;
  width: 90%;
  max-height: 90vh;
  overflow-y: auto;
  z-index: 10000;
}

每個項目都要來一遍,每個彈窗都要寫個變體。 復制粘貼幾十次,改來改去, 最后從“就是頂一個 div 上來嘛”, 不知不覺進化成了 300–500 行的“彈窗框架”

更別提這些:

  • 嵌套彈窗誰先關、誰后關
  • 移動端 Safari 滾動抽風
  • 動態內容高度變化
  • 兼容鍵盤用戶和讀屏用戶……

那一刻我特別想問一句:

瀏覽器:你知道 overlay 是什么嗎? 你知不知道彈窗該怎么表現?

事實證明: 它早就知道了,是我們自己硬要重寫一遍。

圖片圖片

結果真相是:瀏覽器一個屬性,就能幫你全干了

真正讓我把那 500 行 JS 一鍵刪掉的,是這么幾行 HTML:

<button popovertarget="settings-modal">Open Settings</button>

<div id="settings-modal" popover>
  <h2>Settings</h2>
  <p>Configure your preferences</p>
  <button popovertarget="settings-modal" popovertargetaction="hide">Close</button>
</div>

沒看錯:

  • 沒有 JavaScript 控制顯示隱藏
  • 沒有自己管理焦點
  • 沒有自己寫 Esc 關閉、點擊空白關閉邏輯

你只寫了三個屬性:

  • popover
  • popovertarget
  • popovertargetaction

卻順帶拿到了:

? Esc 自動關閉

? 點空白自動關閉(視模式而定)

? 打開時自動把焦點移進彈層

? 關閉時自動把焦點還給觸發按鈕

? 自動加上合理 ARIA 屬性

? 置頂渲染(不用再打 z-index 仗)

? body 滾動、可訪問性、讀屏兼容統統幫你安排好

第一次試的時候,我真的愣住了:

這些年我絞盡腦汁寫的一堆 modal 管理邏輯, 瀏覽器原來早就準備好,只等我寫對一個屬性。

popover 這玩意,到底在背后做了什么?

圖片圖片

先看最小可用例子:

<!-- 觸發按鈕 -->
<button popovertarget="my-popover">Click Me</button>

<!-- 彈出層本體 -->
<div id="my-popover" popover>
  <h3>I'm a popover!</h3>
  <p>Click outside or press Escape to close me.</p>
</div>

popover 這個屬性的意思大概是:

“瀏覽器,這個元素是一個覆蓋層,你負責給它安排好該有的行為。”

加上之后,瀏覽器會自動做這些事:

  • 把這個元素從普通文檔流里挪出去
  • 放進一個專門的 Top Layer(最頂層渲染層)
  • 默認隱藏(不需要你寫 display: none
  • 自動補充可訪問性信息
  • 自動處理鍵盤事件(Esc 等)
  • 自動管理焦點進出

popovertarget="my-popover" 則告訴按鈕: “點我,就去打開那個 ID 叫 my-popover 的 popover。”

狀態管理?事件?焦點? 統統由瀏覽器接管。

三種模式:一個屬性覆蓋 dropdown、modal、tooltip

圖片圖片

popover 不是只有開和關那么簡單,它有三種模式:

<!-- 1. 默認(auto)模式:可輕松關閉 -->
<div id="menu" popover>
  <!-- 等同于 popover="auto" -->
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
</div>

<!-- 2. manual 模式:必須顯式關閉 -->
<div id="alert" popover="manual">
  <h3>Important!</h3>
  <p>You must choose an option.</p>
  <button popovertarget="alert" popovertargetaction="hide">OK</button>
</div>

<!-- 3. hint 模式(實驗):超容易消失的小提示 -->
<div id="tooltip" popover="hint">
  <p>This is a tooltip</p>
</div>

auto 模式(默認): 很適合下拉菜單、導航菜單、小浮層、輕量彈出內容。

  • 點擊空白:會自動關閉
  • 按 Esc:會關閉
  • 打開另一個 popover:當前這個會自動關掉

manual 模式: 用在“用戶不能隨便丟失內容”的場景:

  • 危險操作確認彈窗
  • 多步驟向導
  • 強制操作鎖屏
  • 核心流程中的阻斷性 dialog

這類彈窗,只有你明確告訴瀏覽器“關掉”時才會關閉, 用戶點空白、亂按鍵盤都不會誤關。

hint 模式(還在推進中): 適合那種“順手看一眼的提示”,比如:

  • 懸浮提示(tooltip)
  • 短暫的成功提醒
  • 非關鍵的說明類提示

一句經驗總結:

如果這個彈出內容關掉了,用戶會煩, ——用 manual

其它都讓瀏覽器幫你自動關,auto 即可。

popovertargetaction:精準控制打開/關閉/切換

默認情況下,帶 popovertarget 的按鈕,行為是“切換”(toggle)。

如果你需要更精細的控制,比如分開“打開”和“關閉”按鈕,就用:

<div id="settings" popover>
  <!-- 默認:toggle 行為 -->
  <button popovertarget="settings">Toggle Settings</button>
  
  <!-- 顯式:只負責打開 -->
  <button popovertarget="settings" popovertargetaction="show">
    Open Settings
  </button>
  
  <!-- 顯式:只負責關閉 -->
  <button popovertarget="settings" popovertargetaction="hide">
    Close Settings
  </button>
</div>

這對 UX 很重要: 你不會希望“關閉”按鈕偶爾因為狀態不同而變成“打開”。

真·生產可用模式合集:可以直接 Copy 的那種

下面這些就是我在項目里真正在用的模式。 每一塊你都可以直接搬進代碼里開始用。

模式一:用戶頭像下拉菜單(Dropdown Nav)

完全可以取代你原來那一堆 dropdown 庫。

<nav class="main-nav">
  <button popovertarget="user-menu" class="nav-trigger">
    <img src="avatar.jpg" alt="User menu" class="avatar">
    <span>John Doe</span>
    <svg class="chevron" width="16" height="16" viewBox="0 0 16 16">
      <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none"/>
    </svg>
  </button>
  
  <div id="user-menu" popover class="dropdown-menu">
    <a href="/profile" class="menu-item">
      <svg width="20" height="20"><use href="#icon-user"/></svg>
      Profile
    </a>
    <a href="/settings" class="menu-item">
      <svg width="20" height="20"><use href="#icon-settings"/></svg>
      Settings
    </a>
    <a href="/billing" class="menu-item">
      <svg width="20" height="20"><use href="#icon-credit-card"/></svg>
      Billing
    </a>
    <hr class="menu-divider">
    <a href="/logout" class="menu-item menu-item--danger">
      <svg width="20" height="20"><use href="#icon-logout"/></svg>
      Logout
    </a>
  </div>
</nav>
/* 彈出層樣式 */
.dropdown-menu {
  margin: 0;
  padding: 0;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
  min-width: 200px;
}

/* 菜單項 */
.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  color: #1f2937;
  text-decoration: none;
  transition: background 0.15s;
}

.menu-item:hover {
  background: #f3f4f6;
}

.menu-item:first-child {
  border-radius: 8px 8px 0 0;
}

.menu-item:last-child {
  border-radius: 0 0 8px 8px;
}

.menu-item--danger {
  color: #dc2626;
}

.menu-divider {
  margin: 4px 0;
  border: none;
  border-top: 1px solid #e5e7eb;
}

/* 觸發按鈕 */
.nav-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: transparent;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.nav-trigger:hover {
  background: #f9fafb;
}

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.chevron {
  transition: transform 0.2s;
}

/* 利用 :has() 讓箭頭旋轉 */
.nav-trigger:has(+ [popover]:popover-open) .chevron {
  transform: rotate(180deg);
}

零 JS。

  • 點擊:彈出菜單
  • 點擊外面:收起
  • Esc:收起
  • Tab:鍵盤焦點在菜單項之間順滑流動

瀏覽器第一次表現得像個“成熟組件庫”。

模式二:有動畫、有遮罩的確認彈窗(Modal)

真正意義上的“正經彈窗”:帶背景遮罩、動畫、按鈕區。

<button popovertarget="confirm-delete" class="btn btn-danger">
  Delete Account
</button>

<div id="confirm-delete" popover="manual" class="modal">
  <div class="modal-header">
    <h2>Delete Account?</h2>
    <button popovertarget="confirm-delete" popovertargetaction="hide" class="close-btn" aria-label="Close">
      <svg width="24" height="24" viewBox="0 0 24 24">
        <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2"/>
      </svg>
    </button>
  </div>
  
  <div class="modal-body">
    <p>This action cannot be undone. All your data will be permanently deleted.</p>
    <p>Are you absolutely sure?</p>
  </div>
  
  <div class="modal-footer">
    <button popovertarget="confirm-delete" popovertargetaction="hide" class="btn btn-secondary">
      Cancel
    </button>
    <button onclick="deleteAccount()" class="btn btn-danger">
      Yes, Delete Everything
    </button>
  </div>
</div>
/* 彈窗容器 */
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 90%;
  max-width: 450px;
  margin: 0;
  padding: 0;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  background: white;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.15);
  
  /* 動畫初始狀態 */
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.2s, transform 0.2s, 
              overlay 0.2s allow-discrete, 
              display 0.2s allow-discrete;
}

/* 打開狀態 */
.modal:popover-open {
  opacity: 1;
  transform: scale(1);
}

/* 起始樣式,配合 allow-discrete */
@starting-style {
  .modal:popover-open {
    opacity: 0;
    transform: scale(0.95);
  }
}

/* 遮罩樣式 */
.modal::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
  
  opacity: 0;
  transition: opacity 0.2s, 
              overlay 0.2s allow-discrete, 
              display 0.2s allow-discrete;
}

.modal:popover-open::backdrop {
  opacity: 1;
}

@starting-style {
  .modal:popover-open::backdrop {
    opacity: 0;
  }
}

/* 彈窗結構 */
.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 24px;
  border-bottom: 1px solid #e5e7eb;
}

.modal-header h2 {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  color: #1f2937;
}

.modal-body {
  padding: 24px;
  color: #4b5563;
  line-height: 1.6;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  padding: 16px 24px;
  border-top: 1px solid #e5e7eb;
  background: #f9fafb;
  border-radius: 0 0 12px 12px;
}

/* 按鈕 */
.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.15s;
}

.btn-secondary {
  background: #f3f4f6;
  color: #374151;
}

.btn-secondary:hover {
  background: #e5e7eb;
}

.btn-danger {
  background: #dc2626;
  color: white;
}

.btn-danger:hover {
  background: #b91c1c;
}

.close-btn {
  padding: 4px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: #6b7280;
  transition: color 0.15s;
}

.close-btn:hover {
  color: #1f2937;
}
// 需要在刪除后做邏輯處理時
function deleteAccount() {
  // 刪除邏輯……
  console.log('Account deleted');
  
  // 手動關閉彈窗
  document.getElementById('confirm-delete').hidePopover();
  
  // 跳轉或展示成功頁
  window.location.href = '/goodbye';
}

這里的重點是:

  • popover="manual" 確保用戶不會點空白就誤關
  • 焦點管理、Esc 關閉、讀屏兼容——統統不用你操心

你只負責:文案 + 樣式 + 業務邏輯。

模式三:輕量 Tooltip 提示

不想再為 tooltip 裝一個庫?可以。

<button popovertarget="save-tooltip" class="icon-btn" aria-label="Save">
  <svg width="20" height="20"><use href="#icon-save"/></svg>
</button>

<div id="save-tooltip" popover role="tooltip" class="tooltip">
  Save changes (Ctrl+S)
</div>

<button popovertarget="delete-tooltip" class="icon-btn" aria-label="Delete">
  <svg width="20" height="20"><use href="#icon-trash"/></svg>
</button>

<div id="delete-tooltip" popover role="tooltip" class="tooltip">
  Delete item (Del)
</div>
.tooltip {
  margin: 0;
  padding: 8px 12px;
  background: #1f2937;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  white-space: nowrap;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.15s, transform 0.15s,
              overlay 0.15s allow-discrete,
              display 0.15s allow-discrete;
}

.tooltip:popover-open {
  opacity: 1;
  transform: translateY(0);
}

@starting-style {
  .tooltip:popover-open {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* 使用 anchor 定位(兼容的瀏覽器) */
.icon-btn {
  anchor-name: --trigger;
}

.tooltip {
  position-anchor: --trigger;
  position: absolute;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% -8px;
  
  /* 兼容不支持 anchor 的場景 */
  inset: auto;
}

/* 小三角 */
.tooltip::before {
  content: '';
  position: absolute;
  bottom: -4px;
  left: 50%;
  translate: -50% 0;
  width: 8px;
  height: 8px;
  background: #1f2937;
  transform: rotate(45deg);
}

如果你想要 hover 式提示,再加一點點 JS 即可:

// 給所有 tooltip 觸發器加 hover 行為
document.querySelectorAll('[popovertarget]').forEach(trigger => {
  const tooltipId = trigger.getAttribute('popovertarget');
  const tooltip = document.getElementById(tooltipId);
  
  if (tooltip?.getAttribute('role') === 'tooltip') {
    trigger.addEventListener('mouseenter', () => {
      tooltip.showPopover();
    });
    
    trigger.addEventListener('mouseleave', () => {
      tooltip.hidePopover();
    });
  }
});

模式四:多級嵌套菜單(子菜單秒開)

<button popovertarget="file-menu" class="menu-trigger">File</button>

<div id="file-menu" popover class="menu">
  <button class="menu-item">New File</button>
  <button popovertarget="open-submenu" class="menu-item">
    Open Recent
    <svg class="chevron-right" width="16" height="16">
      <path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="2" fill="none"/>
    </svg>
  </button>
  <button class="menu-item">Save</button>
  <hr class="menu-divider">
  <button class="menu-item">Exit</button>
</div>

<div id="open-submenu" popover class="menu submenu">
  <button class="menu-item">project-1.js</button>
  <button class="menu-item">index.html</button>
  <button class="menu-item">styles.css</button>
  <button class="menu-item">readme.md</button>
</div>
.menu {
  margin: 0;
  padding: 4px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  min-width: 200px;
}

.menu-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: none;
  border-radius: 4px;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s;
}

.menu-item:hover {
  background: #f3f4f6;
}

.chevron-right {
  opacity: 0.5;
}

.submenu {
  /* 子菜單相對父菜單定位 */
  margin-left: 4px;
}

打開“File” → 再打開 “Open Recent”。 點擊空白:全部按順序關閉。

按一次 Esc:關掉最近開的子菜單。 再按一次 Esc:關掉上層菜單。

整個層級關系和關閉順序,全是瀏覽器幫你管理。

模式五:右鍵菜單(Context Menu)

右鍵菜單,其實就是一個手動定位的 popover

<div id="content-area" class="content">
  Right-click anywhere in this area
</div>

<div id="context-menu" popover="manual" class="context-menu">
  <button onclick="handleCut()" class="menu-item">
    <svg width="16" height="16"><use href="#icon-cut"/></svg>
    Cut
    <span class="shortcut">Ctrl+X</span>
  </button>
  <button onclick="handleCopy()" class="menu-item">
    <svg width="16" height="16"><use href="#icon-copy"/></svg>
    Copy
    <span class="shortcut">Ctrl+C</span>
  </button>
  <button onclick="handlePaste()" class="menu-item">
    <svg width="16" height="16"><use href="#icon-paste"/></svg>
    Paste
    <span class="shortcut">Ctrl+V</span>
  </button>
  <hr class="menu-divider">
  <button onclick="handleDelete()" class="menu-item menu-item--danger">
    <svg width="16" height="16"><use href="#icon-trash"/></svg>
    Delete
    <span class="shortcut">Del</span>
  </button>
</div>
.context-menu {
  position: fixed;
  margin: 0;
  padding: 4px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  min-width: 220px;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 8px 12px;
  background: transparent;
  border: none;
  border-radius: 4px;
  text-align: left;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.15s;
}

.menu-item:hover {
  background: #f3f4f6;
}

.menu-item--danger {
  color: #dc2626;
}

.shortcut {
  margin-left: auto;
  font-size: 12px;
  color: #9ca3af;
}

.content {
  padding: 40px;
  background: #f9fafb;
  border: 2px dashed #e5e7eb;
  border-radius: 8px;
  text-align: center;
  color: #6b7280;
  user-select: none;
}
const contentArea = document.getElementById('content-area');
const contextMenu = document.getElementById('context-menu');

// 右鍵顯示菜單
contentArea.addEventListener('contextmenu', (e) => {
  e.preventDefault();
  
  // 位置跟隨鼠標
  contextMenu.style.left = e.clientX + 'px';
  contextMenu.style.top = e.clientY + 'px';
  
  contextMenu.showPopover();
});

// 點擊其他地方關閉菜單
document.addEventListener('click', (e) => {
  if (!contextMenu.contains(e.target) && e.target !== contentArea) {
    contextMenu.hidePopover();
  }
});

// 菜單行為
function handleCut() {
  console.log('Cut');
  contextMenu.hidePopover();
}

function handleCopy() {
  console.log('Copy');
  contextMenu.hidePopover();
}

function handlePaste() {
  console.log('Paste');
  contextMenu.hidePopover();
}

function handleDelete() {
  console.log('Delete');
  contextMenu.hidePopover();
}

當你確實需要 JS 控制時:API 簡直優雅到犯規

有些場景你確實需要 JS 控制,比如異步加載、校驗、組合交互,這時候可以用原生 API:

const popover = document.getElementById('my-popover');

// 打開
popover.showPopover();

// 關閉
popover.hidePopover();

// 切換
popover.togglePopover();

// 判斷當前是否打開
const isOpen = popover.matches(':popover-open');

就這三個方法 + 一個偽類, 替代過去需要你寫半個小框架的邏輯。

還有兩個事件,非常關鍵:

const popover = document.getElementById('my-popover');

// 狀態切換前觸發(可取消)
popover.addEventListener('beforetoggle', (event) => {
  console.log('Old state:', event.oldState); // "open" or "closed"
  console.log('New state:', event.newState); // "open" or "closed"
  
  // 比如:不通過校驗就不允許打開
  if (event.newState === 'open' && !isFormValid()) {
    event.preventDefault(); // 阻止打開
    showError('Please fix form errors');
  }
});

// 狀態切換后觸發
popover.addEventListener('toggle', (event) => {
  if (event.newState === 'open') {
    // 埋點
    trackEvent('modal_opened', { modalId: popover.id });
    
    // 動態加載內容
    loadModalContent();
    
    // 把焦點送到指定元素
    popover.querySelector('input').focus();
  } else {
    // 清理現場
    console.log('Modal closed');
  }
});
  • beforetoggle:特別適合作權限校驗、表單校驗、防誤操作
  • toggle:用來做副作用:加載數據、埋點、重置表單等等

實戰例子:帶校驗的“錯誤彈窗”

<button id="submit-form" popovertarget="validation-dialog">Submit Form</button>

<div id="validation-dialog" popover="manual" class="modal">
  <h2>Form Errors</h2>
  <ul id="error-list"></ul>
  <button popovertarget="validation-dialog" popovertargetaction="hide">
    Fix Errors
  </button>
</div>
const submitBtn = document.getElementById('submit-form');
const validationDialog = document.getElementById('validation-dialog');
const errorList = document.getElementById('error-list');

submitBtn.addEventListener('click', (e) => {
  const errors = validateForm();
  
  if (errors.length > 0) {
    e.preventDefault(); // 攔截提交
    
    // 把錯誤渲染進彈窗
    errorList.innerHTML = errors
      .map(err => `<li>${err}</li>`)
      .join('');
    
    validationDialog.showPopover();
  } else {
    // 校驗通過,正常提交
    submitForm();
  }
});

function validateForm() {
  const errors = [];
  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;
  
  if (!email.includes('@')) {
    errors.push('Invalid email address');
  }
  
  if (password.length < 8) {
    errors.push('Password must be at least 8 characters');
  }
  
  return errors;
}

function submitForm() {
  console.log('Form submitted successfully');
  // 真正的提交邏輯……
}

“保存中……” 這類加載彈窗,也可以用 popover 接管

<button onclick="saveData()">Save Changes</button>

<div id="loading-spinner" popover="manual" class="loading-modal">
  <div class="spinner"></div>
  <p>Saving your changes...</p>
</div>
.loading-modal {
  padding: 32px;
  border: none;
  border-radius: 12px;
  background: white;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
  text-align: center;
}

.spinner {
  width: 48px;
  height: 48px;
  margin: 0 auto 16px;
  border: 4px solid #e5e7eb;
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.loading-modal p {
  margin: 0;
  color: #6b7280;
  font-size: 14px;
}
async function saveData() {
  const loadingModal = document.getElementById('loading-spinner');
  
  try {
    // 展示加載態
    loadingModal.showPopover();
    
    // 模擬 API 調用
    await fetch('/api/save', {
      method: 'POST',
      body: JSON.stringify({ data: 'your data' })
    });
    
    alert('Saved successfully!');
  } catch (error) {
    alert('Failed to save: ' + error.message);
  } finally {
    // 無論成功失敗都要關掉
    loadingModal.hidePopover();
  }
}

一些“高級玩法”:讓 popover 真正融入你的業務流

1. 動態加載內容:只打開時才拉數據

<button popovertarget="user-profile">View Profile</button>

<div id="user-profile" popover class="profile-card">
  <div id="profile-content">
    <div class="skeleton-loader"></div>
  </div>
</div>
const profilePopover = document.getElementById('user-profile');
const profileContent = document.getElementById('profile-content');

profilePopover.addEventListener('toggle', async (event) => {
  if (event.newState === 'open') {
    try {
      const response = await fetch('/api/user/profile');
      const userData = await response.json();
      
      profileContent.innerHTML = `
        <img src="${userData.avatar}" alt="${userData.name}">
        <h3>${userData.name}</h3>
        <p>${userData.bio}</p>
        <a href="/profile/${userData.id}">View Full Profile</a>
      `;
    } catch (error) {
      profileContent.innerHTML = `
        <p class="error">Failed to load profile</p>
      `;
    }
  }
});

2. 權限控制:不讓他打開,就換一個彈窗

const restrictedPopover = document.getElementById('premium-feature');

restrictedPopover.addEventListener('beforetoggle', (event) => {
  if (event.newState === 'open') {
    // 檢查權限
    if (!userHasPremium()) {
      event.preventDefault();
      
      // 換成“升級會員”彈窗
      document.getElementById('upgrade-prompt').showPopover();
    }
  }
});

function userHasPremium() {
  return localStorage.getItem('premium') === 'true';
}

3. 鍵盤快捷鍵 + 命令面板

// 全局快捷鍵
document.addEventListener('keydown', (e) => {
  // Ctrl+K:打開命令面板
  if (e.ctrlKey && e.key === 'k') {
    e.preventDefault();
    document.getElementById('command-palette').showPopover();
  }
  
  // Ctrl+Shift+P:打開偏好設置
  if (e.ctrlKey && e.shiftKey && e.key === 'P') {
    e.preventDefault();
    document.getElementById('preferences').showPopover();
  }
});

4. 移動端 Bottom Sheet:原生彈層直接變底部抽屜

<button popovertarget="mobile-menu">Menu</button>

<div id="mobile-menu" popover class="bottom-sheet">
  <div class="bottom-sheet-handle"></div>
  <nav class="bottom-sheet-content">
    <a href="/home">Home</a>
    <a href="/explore">Explore</a>
    <a href="/notifications">Notifications</a>
    <a href="/profile">Profile</a>
  </nav>
</div>
@media (max-width: 768px) {
  .bottom-sheet {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 20px 20px 0 0;
    background: white;
    box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
    max-height: 80vh;
    
    transform: translateY(100%);
    transition: transform 0.3s ease-out,
                overlay 0.3s allow-discrete,
                display 0.3s allow-discrete;
  }
  
  .bottom-sheet:popover-open {
    transform: translateY(0);
  }
  
  @starting-style {
    .bottom-sheet:popover-open {
      transform: translateY(100%);
    }
  }
  
  .bottom-sheet-handle {
    width: 40px;
    height: 4px;
    margin: 12px auto;
    background: #d1d5db;
    border-radius: 2px;
  }
  
  .bottom-sheet-content {
    padding: 16px;
  }
  
  .bottom-sheet-content a {
    display: block;
    padding: 16px;
    color: #1f2937;
    text-decoration: none;
    font-size: 16px;
    border-radius: 8px;
    transition: background 0.15s;
  }
  
  .bottom-sheet-content a:hover {
    background: #f3f4f6;
  }
}

兼容性與漸進增強:它夠“上生產”嗎?

圖片圖片

截至 2025 年底,Popover API 支持情況:

? Chrome 114+

? Edge 114+

? Safari 17+

? Firefox 125+

全球覆蓋率大約在 接近 9 成。 對大多數現代 Web 應用來說,已經完全夠資格上生產。

如何優雅檢測支持情況?

// 檢測是否支持 Popover API
const supportsPopover = HTMLElement.prototype.hasOwnProperty('popover');

if (supportsPopover) {
  console.log('Popover API is supported');
  // 使用原生 popover
} else {
  console.log('Popover API not supported');
  // 加載 polyfill 或走降級方案
}

漸進增強方案:先保證能用,再增強體驗

<!-- 兜底:沒有 JS 也能用的版本 -->
<details class="fallback-menu">
  <summary>Menu</summary>
  <div class="menu-content">
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
  </div>
</details>

<!-- 增強版:有 popover 時啟用 -->
<button popovertarget="enhanced-menu" style="display: none;">Menu</button>
<div id="enhanced-menu" popover class="menu-content">
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
</div>
if (supportsPopover) {
  // 隱藏 fallback,展示增強版
  document.querySelector('.fallback-menu').style.display = 'none';
  document.querySelector('[popovertarget]').style.display = 'block';
}

給老瀏覽器一個“體面”的退路:polyfill

<script type="module">
  if (!HTMLElement.prototype.hasOwnProperty('popover')) {
    import('https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js');
  }
</script>

這個 polyfill 只有幾 KB(gzip 后), 核心行為都能模擬, 雖然 Top Layer 等高級特性可能略有差異, 但對大多數場景已經足夠友好。

真正的收益:不只是“省幾百行代碼”那么簡單

1. Bundle 體積:砍掉一整個“彈窗宇宙”

一個真實項目切換前后的對比:

切換前:

  • React 彈窗庫:23KB
  • 自己的彈窗管理器:8KB
  • 焦點陷阱工具:5KB
  • body 滾動鎖定:3KB

合計:39KB 只服務于彈窗。

切換到 popover 后:

  • 僅保留一個 polyfill:6KB

直接省掉 ~33KB,節約約 85%。

對于移動端用戶,這往往就是0.5–1 秒的首屏加載差距。

2. 運行時性能:JS 再努力,也拼不過瀏覽器 C++ 實現

JS 彈窗:

  • 每次打開要遍歷 DOM 找焦點元素
  • 綁一堆鍵盤/點擊事件
  • 自己維護狀態機

一次打開帶來的額外開銷:5–10ms 起跳(低端機更夸張)。

原生 popover:

  • 狀態、焦點切換都在瀏覽器引擎內部
  • 調度、渲染全是底層優化過的代碼

一次打開基本可以忽略不計。

當你要同時管理多個 overlay(菜單 + Tooltip + Modal)時, 差距會非常明顯。

3. 內存與復雜度:你少了一個永遠“半維護”的自制框架

我們過去寫的 modal 管理器,會一直持有:

  • DOM 引用
  • 事件回調
  • 狀態對象

當你頁面上有 10+ 個彈窗組件時,堆積的東西不會少。

而 popover 把這些“該瀏覽器管的事”都收回去了, 你只剩下業務邏輯需要維護。

常見坑:你可能會無意識做的幾件“反瀏覽器”行為

? 坑 1:自己給 [popover] 寫 display: none

/* 千萬別這么干 */
[popover] {
  display: none;
}

[popover]:popover-open {
  display: block;
}

后果:你把瀏覽器的可見性控制徹底打斷了:

  • 彈不出來
  • 事件不觸發
  • 焦點管理徹底失效

? 正確做法:完全不要管 display只在 .popover 類上做樣式(padding、陰影、圓角等)。

? 坑 2:繼續玩 z-index 盡頭對決

/* 也別這樣 */
[popover] {
  z-index: 999999;
}

Top Layer 是一個獨立于 z-index 的維度, 你寫再大的 z-index 都不會更“靠前”。

反而可能制造一些奇怪的兼容問題。

? 正確做法:不要給 popover 寫 z-index。Top Layer 天然幫你蓋住頁面上所有東西。

? 坑 3:關鍵彈窗卻用默認 auto 模式

<!-- ? 點擊空白就關掉:不適合危險操作 -->
<div id="confirm-delete" popover>
  <p>Delete everything?</p>
  <button>Yes</button>
  <button>No</button>
</div>

刪除賬號、危險操作的確認對話框, 一不小心點外面就關了,用戶會直接罵人。

? 正確寫法:

<!-- ? manual:必須顯式點擊按鈕才能關閉 -->
<div id="confirm-delete" popover="manual">
  <p>Delete everything?</p>
  <button popovertarget="confirm-delete" popovertargetaction="hide">Yes</button>
  <button popovertarget="confirm-delete" popovertargetaction="hide">No</button>
</div>

? 坑 4:堅持自己再維護一套“彈窗狀態機”

// ? 不要再寫這種管理棧了
let modalStack = [];
let isModalOpen = false;

function openModal(id) {
  isModalOpen = true;
  modalStack.push(id);
  // ……更多復雜邏輯
}

瀏覽器已經替你維護好了:誰打開、誰關閉、誰在頂層。 你再搭一個平行世界,只會導致兩邊狀態不同步。

? 正確做法:

  • 需要知道狀態時,用 :popover-open 檢查
  • 需要做副作用,用 beforetoggle 和 toggle

? 坑 5:用 JS 手搓一堆奇怪的動畫

// ? 無需再用 setInterval 做透明度動畫
function openModalWithAnimation(modal) {
  modal.style.opacity = '0';
  modal.showPopover();
  
  let opacity = 0;
  const interval = setInterval(() => {
    opacity += 0.1;
    modal.style.opacity = opacity;
    if (opacity >= 1) clearInterval(interval);
  }, 16);
}

動畫交給 CSS,JS 做業務。 世界會變得非常清爽。

? 正確寫法:

[popover] {
  opacity: 0;
  transition: opacity 0.2s;
}

[popover]:popover-open {
  opacity: 1;
}

想遷移到 popover?給你一份拆彈清單

階段一:盤點現狀

[ ] 列出項目里所有:modal / dropdown / tooltip / context menu [ ] 看看哪些是純展示、哪些有復雜業務邏輯 [ ] 標記出適合先遷移的簡單場景(比如用戶菜單、簡單彈窗) [ ] 評估你的用戶瀏覽器版本(看兼容性是否 OK)

階段二:動手改造

[ ] 選 1–2 個組件,用 popover 重寫 [ ] 用鍵盤 Tab / Shift+Tab / Esc 全面跑一遍 [ ] 用讀屏工具(NVDA / VoiceOver 等)聽一遍體驗 [ ] 檢查嵌套彈窗、多個 popover 同時存在時的行為 [ ] 確認 manual / auto 模式是否選對場合

階段三:測試 & 上線

[ ] 在 Chrome / Firefox / Safari / Edge 全部跑一遍 [ ] 做一次簡單的無障礙掃描(axe 等工具) [ ] 對比遷移前后的 bundle 體積與首屏時間 [ ] 用小規模灰度或 feature flag 掛上線 [ ] 逐步刪掉舊的 modal 管理代碼

真正的底層趨勢:Web 平臺終于在“長大”,我們也該收手了

Popover API 只是這波“原生 UI 能力升級”的一小塊。

你會發現最近幾年,瀏覽器在持續給我們補這些“久違的常識”:

  • <dialog> 原生對話框
  • popover 原生 overlay 管理
  • CSS anchor 定位 tooltip / 彈出層
  • inert 屬性一鍵禁用一整塊區域交互
  • 即將到來的原生自定義選擇框、原生 tooltip 元素……

以前,我們是被迫在框架里重建一整套瀏覽器已經部分支持的東西:

“我想要一個彈窗”

→ 安裝庫 → 寫樣式 → 管狀態 → 處理焦點 → 打補丁 → 被無障礙專家懟

現在,Web 平臺終于開始承擔它應該承擔的那部分責任:


“這些通用交互,我來幫你搞定,你只負責業務和體驗即可。”

框架不會因此“失業”, 它們會變得更輕、更專注:

  • React/Vue/Svelte 管控你的狀態和業務邏輯
  • 彈層、遮罩、菜單行為交給瀏覽器原生實現

最后一句:下次想寫一個彈窗,先問問自己——真的需要 JS 嗎?

我刪掉 500 行 modal 管理代碼,用幾個屬性替代, 得到的不是“勉強湊合”的實現, 而是:

  • 更好的無障礙支持
  • 更少的 Bug 面
  • 更小的包、更快的首屏、更順滑的交互

真正高級的前端不是“什么都自己寫一遍”, 而是知道:什么該交給平臺,什么才值得自己造輪子。

你可以從特別小的一步開始:

  • 找到項目里一個 dropdown 或彈窗
  • 用 popover 改寫一版
  • 親手體驗一下: 不寫 JS 的彈窗,到底爽不爽

等哪天,你再也不用在半夜兩點調焦點陷阱、 也不用為一個 z-index 失眠, 你會非常感謝,現在這個愿意嘗試原生方案的自己。

責任編輯:武曉燕 來源: 大遷世界
相關推薦

2014-05-15 09:45:58

Python解析器

2017-04-05 11:10:23

Javascript代碼前端

2025-09-05 04:15:00

2025-08-29 10:00:00

JavaScript瀏覽器API

2025-02-13 07:49:18

2020-09-09 16:00:22

Linux進程

2025-06-27 08:34:19

2017-07-19 13:27:44

前端Javascript模板引擎

2014-01-22 09:19:57

JavaScript引擎

2022-10-28 10:18:53

代碼績效Java

2024-02-22 14:24:34

2023-02-21 17:06:49

硬件軟件系統

2020-06-11 08:48:49

JavaScript開發技術

2025-01-13 00:00:10

SwaggerAI項目

2017-03-28 21:03:35

代碼React.js

2022-01-26 16:30:47

代碼虛擬機Linux

2011-11-30 09:46:32

超算TOP500超級計算機

2022-06-29 09:02:31

go腳本解釋器

2013-03-04 10:22:30

Python

2019-06-05 15:00:28

Java代碼區塊鏈
點贊
收藏

51CTO技術棧公眾號

三级精品视频| 又爽又大又黄a级毛片在线视频| 欧美成人久久| 精品国产乱码久久久久久夜甘婷婷 | 日韩风俗一区 二区| 黄在线观看网站| 婷婷视频在线| 99久久精品国产一区| 国产成人在线视频| 欧美色图亚洲天堂| 亚洲人成精品久久久| 欧美二区乱c少妇| 欧美成人免费在线观看视频| 天堂а√在线官网| av午夜精品一区二区三区| 国产美女久久精品香蕉69| 精品无码av在线| 日韩欧美三级| 亚洲免费电影在线观看| 涩多多在线观看| 自拍偷拍亚洲视频| 一区二区三区视频在线看| 欧美亚洲另类在线一区二区三区| 国产强伦人妻毛片| 日韩精品电影在线观看| 欧美第一黄网免费网站| 五月激情四射婷婷| 亚洲电影一级片| 精品国产精品网麻豆系列| 国内国产精品天干天干| 亚洲高清黄色| 欧美视频免费在线观看| 欧洲金发美女大战黑人| www.91在线| 2024国产精品| 国产激情美女久久久久久吹潮| 最新黄色网址在线观看| 久久精品官网| 国产91精品久| 日韩特黄一级片| 欧美三级乱码| 久久99热精品这里久久精品| 国产高潮国产高潮久久久91| 婷婷亚洲最大| 久久婷婷国产麻豆91天堂| 久久婷婷五月综合| 九一国产精品| 亚洲人成电影网站色| 中文字幕 亚洲一区| 第四色在线一区二区| 精品国产乱码久久久久久牛牛| 色黄视频免费看| 久久久久久久久成人| 7777精品久久久大香线蕉| 一女二男3p波多野结衣| 祥仔av免费一区二区三区四区| 欧美色手机在线观看| 亚欧在线免费观看| 成人一区视频| 7777精品伊人久久久大香线蕉经典版下载 | 亚洲精品福利免费在线观看| 超碰caoprom| 另类春色校园亚洲| 亚洲欧美一区二区激情| 蜜桃传媒一区二区亚洲av| 亚洲区小说区图片区qvod| 精品亚洲男同gayvideo网站| 在线观看日本中文字幕| 深夜福利久久| 丝袜美腿精品国产二区| 中文字幕五月天| 一本到12不卡视频在线dvd| 久久成人av网站| 国产福利久久久| 西西裸体人体做爰大胆久久久| 欧洲美女免费图片一区| 乱子伦一区二区三区| 久久精品久久99精品久久| 91久久精品国产91性色| 亚洲国产精品无码久久| 91亚洲精品久久久蜜桃| 亚洲高清不卡一区| 2024短剧网剧在线观看| 午夜精品国产更新| 美女网站免费观看视频| 91精品国产一区二区在线观看 | 九色在线播放| 国产精品乱码一区二三区小蝌蚪| youjizz.com亚洲| 成年网站在线视频网站| 91精品福利在线| 中文字幕55页| 九热爱视频精品视频| 日韩视频在线免费| 日韩精品视频免费播放| 日韩高清在线一区| 91免费在线观看网站| 神马精品久久| 亚洲视频在线一区二区| 日韩视频在线视频| 丁香婷婷久久| 日韩精品亚洲视频| 粉嫩av性色av蜜臀av网站| 国产一区二区三区久久久久久久久| 国产精品日日做人人爱| 国产成人手机在线| 国产精品视频一二三区| 久久国产精品网| 国产精品99| 日韩麻豆第一页| 免费中文字幕在线| 青草av.久久免费一区| 国产日韩一区欧美| 成人ww免费完整版在线观看| 一本到不卡精品视频在线观看| 黄色一级片免费播放| 九色成人国产蝌蚪91| 欧美精品激情在线| 国产精品国产三级国产aⅴ| 久久综合色8888| 欧美黄网在线观看| 成人在线免费电影网站| 亚洲精品日韩丝袜精品| 精品在线视频免费| 国产成人精品免费在线| 亚洲欧美久久久久一区二区三区| 精精国产xxx在线视频app| 欧美一区二区三区在线视频| 日本在线观看网址| 视频一区二区三区入口| 看高清中日韩色视频| 国产羞羞视频在线播放| 欧美一区二区大片| 91视频青青草| 精品一区二区三区免费毛片爱| 欧美激情专区| 超碰超碰人人人人精品| 亚洲第一在线视频| 久久久久久国产精品视频| 国模一区二区三区白浆| 亚洲三区在线观看| 日韩毛片免费视频一级特黄| 中文字幕av一区中文字幕天堂| 亚洲综合图片网| 26uuu久久综合| 97国产在线播放| 神马日本精品| 日本免费久久高清视频| 亚洲色欧美另类| 黑人巨大精品欧美一区二区三区 | 亚洲少妇屁股交4| 在线观看免费视频高清游戏推荐| 欧美天天综合| 国产欧美亚洲精品| 免费黄色在线看| 欧美一区二区三区四区五区| 小泽玛利亚一区二区免费| 国产资源精品在线观看| 丰满人妻一区二区三区53号| 6080亚洲理论片在线观看| 久久久久日韩精品久久久男男| 黄色小视频免费在线观看| 亚洲高清免费在线| 国产精品无码毛片| 久久综合九色| 一区二区精品在线| 国产麻豆一区二区三区| 久久久久久久香蕉网| 五月婷婷激情在线| 色综合久久久久综合体| www.涩涩爱| 国产麻豆视频精品| 欧美在线一区视频| 精品av一区二区| 亚洲一区二区三区久久| wwww亚洲| 尤物九九久久国产精品的分类 | 伊人天天久久大香线蕉av色| 精品国产不卡一区二区| 欧美精品久久久久| 男同在线观看| 91精品国产一区二区| 精品一区在线视频| 国产亚洲成aⅴ人片在线观看| 午夜免费福利视频在线观看| 欧美激情一级片一区二区| 久久av免费一区| 日韩av黄色| 欧美一级高清免费| 日本天堂在线观看| 日韩av在线免费观看| 在线观看免费高清视频| 亚洲不卡一区二区三区| jizz18女人高潮| 99久久精品费精品国产一区二区| 毛片毛片毛片毛片毛片毛片毛片毛片毛片 | 亚洲性受xxx喷奶水| 中文字幕不卡av| 婷婷亚洲一区二区三区| 精品污污网站免费看| 国产 日韩 欧美 成人| 国产精品毛片无遮挡高清| 深田咏美中文字幕| 久久成人综合网| 亚洲国产精品久久久久爰色欲| 图片小说视频色综合| 久久国产精品久久精品国产| 亚洲国产高清在线观看| 国产精品精品视频一区二区三区| 欧美韩日亚洲| 日韩在线视频导航| 你懂的视频在线观看| 精品三级在线观看| 国产又粗又长视频| 色吊一区二区三区| 日本一区二区网站| 亚洲三级小视频| 国产真人做爰视频免费| 99re在线精品| 亚洲午夜久久久久久久久| 狠狠色狠狠色综合| 欧美特级aaa| 日韩精品五月天| 波多野结衣家庭教师视频| 精品91视频| 国产在线视频综合| 亚洲天天影视网| 中文字幕中文字幕99| 第一会所亚洲原创| 欧美污视频久久久| 国产精品手机在线播放| 久久99精品久久久久子伦| 国语一区二区三区| 国产精品区一区| 一区中文字幕电影| 91手机在线播放| 中文久久电影小说| 国产欧美日韩精品在线观看| 成人涩涩视频| 国产精品九九九| 韩国主播福利视频一区二区三区| 97精品免费视频| 国产欧洲在线| 韩国三级电影久久久久久| 成年人国产在线观看| 久久久久久久激情视频| 毛片大全在线观看| 午夜精品久久久久久久久久久久久| 色呦呦呦在线观看| 久久久久久这里只有精品| 成人免费一区二区三区牛牛| 性欧美xxxx交| 在线手机中文字幕| 日韩av不卡电影| 视频一区在线免费看| 国产日韩亚洲欧美| 国产精品99久久免费| 91超碰rencao97精品| 一区二区三区自拍视频| 国产综合 伊人色| 欧美人与拘性视交免费看| 日韩精品另类天天更新| 日韩av有码| 强开小嫩苞一区二区三区网站| 午夜精品视频| 青青草视频在线免费播放| 鲁大师成人一区二区三区| 宅男噜噜噜66国产免费观看| 久久99久久精品| 黄色片子免费看| av在线一区二区三区| 中文字幕网站在线观看| 国产精品伦理在线| 国产亚洲欧美久久久久| 欧美视频中文字幕在线| 11024精品一区二区三区日韩| 日韩一区二区三区精品视频| 天天干天天插天天操| 国产一区二区三区18| 大地资源网3页在线观看| 韩国福利视频一区| 97成人超碰| yellow视频在线观看一区二区| 日本国产精品| 视频一区二区综合| 欧美777四色影| av无码精品一区二区三区| 蜜桃91丨九色丨蝌蚪91桃色| 精人妻一区二区三区| 欧美激情在线观看视频免费| 久久这里只有精品国产| 91久久久免费一区二区| jlzzjlzzjlzz亚洲人| 亚洲女人天堂色在线7777| 国内精品久久久久久野外| 午夜精品在线观看| 成人黄色免费观看| 97人人模人人爽人人少妇| 成久久久网站| 欧美网站免费观看| 国产一区二区剧情av在线| 99久久久无码国产精品性| 一区二区三区鲁丝不卡| 中文字幕一区二区三区免费看| 亚洲第一在线视频| 国产成人l区| 国产精品福利小视频| 老司机精品视频在线播放| 在线视频欧美一区| 男人天堂欧美日韩| 无码任你躁久久久久久老妇| 中文字幕亚洲一区二区va在线| 五月婷婷亚洲综合| 日韩欧美电影在线| 91网页在线观看| 日韩av毛片网| 美女一区2区| 久久精品在线免费视频| 日本亚洲欧美天堂免费| 久久无码人妻精品一区二区三区 | 国产资源在线视频| 国产又粗又猛又爽又黄91精品| av男人的天堂av| 欧美日韩国产一区中文午夜| 精品黑人一区二区三区在线观看| 在线日韩中文字幕| 人成在线免费网站| 国产精品国产精品国产专区蜜臀ah | 激情五月六月婷婷| 国内久久婷婷综合| 男人天堂资源网| 欧美日韩不卡一区二区| а√天堂中文在线资源bt在线| 日韩av免费在线播放| 午夜精品福利影院| 99精品人妻少妇一区二区| 暴力调教一区二区三区| 国产在线拍揄自揄拍无码视频| 日韩欧美中文一区| 91蜜桃在线视频| www.久久艹| 亚洲高清激情| 黄色污在线观看| 欧美午夜片在线免费观看| 五月婷婷综合久久| 欧美一区在线直播| 米奇777超碰欧美日韩亚洲| 18禁免费观看网站| 99国产精品视频免费观看| 国产成人愉拍精品久久| 亚洲精品视频在线播放| 亚洲精品88| 婷婷久久伊人| 久久99深爱久久99精品| 中文字幕在线2021| 欧美成人激情免费网| 77thz桃花论族在线观看| 精品国产乱码久久久久久88av| 中文亚洲字幕| av网站免费在线看| 56国语精品自产拍在线观看| av电影免费在线观看| 高清日韩一区| 午夜在线一区| 欧美视频一区二区在线| 91精品国产欧美一区二区成人| 一二三四区在线观看| 国产在线一区二区三区四区 | 夜夜嗨av一区二区三区四季av| 丰满少妇被猛烈进入| 91精品国产91久久久久| 你懂的一区二区三区| 中文字幕第17页| 亚洲午夜羞羞片| 欧美日本韩国一区二区| 国产伦精品一区二区三区精品视频| 影视亚洲一区二区三区| 182在线视频| 欧美三区在线观看| 欧美极品少妇videossex| 欧美一区二区影视| 国产一区二区福利视频| 欧美 日韩 精品| xxxx欧美18另类的高清| 丁香一区二区| 中文字幕 日韩 欧美| 亚洲午夜激情网站| 国产剧情在线观看| 99在线影院| 日韩 欧美一区二区三区| 激情五月少妇a| 亚洲视频777| aaa国产精品| 天堂网在线免费观看| 亚洲国产日韩a在线播放性色| 国产毛片av在线| 国产综合18久久久久久| 久久99日本精品| 无码人妻丰满熟妇精品| 欧美精品国产精品日韩精品|