摘要:而當用戶使用鍵盤操作,焦點發生變化后,當前焦點在哪一項上,必須給出明確的視覺提示,否則極容易帶來困難,甚至誤操作。回車鍵按下時,則會到相應賬號,或者取消操作關閉彈出層。再來看看組件庫的組件中的確認消息。
問題提出本文作者:文藺
原文地址:http://www.wemlion.com/2016/a...
本文由 @文藺 創作,轉載請保留此聲明。
所有權利保留,請勿用于商業目的。
需要說明的是,題目中所說的 Modal,指的是所有由前端開發者自定義的對話框,如通常用到的 Alert、Prompt、Confirm 等等,經常伴隨著一個半透明的灰黑色全局 mask。
事情源自某天使用某網站的頁面,出現一個自定義的 Comfirm,習慣性按下回車確認,等了很久也不見彈出層關閉。于是很絕望。繼而發現,真不是這一個網站的問題。
看看一般瀏覽器原生的 alert、prompt、confirm 可以發現,它們基本上都提供默認操作項(eg.確認/取消),通過回車鍵可以直接完成。傳統軟件如 PhotoShop 也是如此,否則真不知那些無需鼠標的 PS 大神是怎樣練出來的。然而不少自定義的插件,卻并未提供這樣的功能,往往不得不將手挪到鼠標或觸摸板上操作,極其不便。
主要存在兩個方面的問題,一是根本無法直接通過鍵盤操作對話框;二是雖然可以操作,但視覺提示并不明顯;三是整個應用對焦點的控制不夠,容易導致成本可能極高誤操作。下面先簡單談談這幾個問題,然后結合實際例子說明。
鍵盤、鼠標轉換成本先談第一個問題。鍵盤、鼠標之間的切換成本應該是很高的,尤其是當應用響應與用戶心理預期不符時,造成的焦慮感或挫敗感很影響用戶體驗。
因此在使用彈出 Modal 時,需要根據業務場景,設置一個默認選項(尤其是確認、取消、提交等操作),用戶通過回車鍵或鼠標點擊就可以輕松完成任務。不過,另外一種更好的方式是提供 Tab 和 (Shift + Tab) 兩種操作,通過鍵盤可以自由地在彈出層不同選項之間按照順序切換。
明確的焦點提示接著轉到第二個,視覺提示的問題。沒有明確是視覺提示,會造成用戶的疑惑。
有默認選項的時候,無論是通過閃動的光標、背景色的變化,還是邊框、outline 等形式,都需要明確提示用戶,Modal 彈出時默認操作項是什么。而當用戶使用鍵盤操作,焦點發生變化后,當前焦點在哪一項上,必須給出明確的視覺提示,否則極容易帶來困難,甚至誤操作。
應用對焦點的控制第三個問題實際是第二個問題的延續。前面提到焦點的視覺提示問題,其實還有一種可能性,可能導致嚴重后果,這就是用戶通過 Tab 操作,將焦點移動到彈出層 mask 背后的應用中。
這時候,一方面沒有給出清晰的提示,一方面沒有對焦點進行很好的控制,萬一焦點在一個敏感位置(如一個外部跳轉鏈接),按下回車鍵,這時候頁面將刷新,用戶此前的數據面臨蕩然無存的境地。腦洞再大一點,萬一 mask 背后獲得焦點的是巨額交易乃至核按鈕(233333),那么恭喜你,攻城獅生涯就此結束。
我們知道,當網頁彈出原生 alert 時,整個頁面其他部分處于不可操作狀態。使用類似的自定義組件替代這些功能時,縱然我們不能阻塞頁面其他所有部分,但也得采取一定的控制,防止意外發生,最簡便的方法便是將 Tab 的控制權掌握在開發者手中。
當然,相比于第三點,前兩點應該是必須項,第三項可以算是加分項,在一些普通應用中不實現影響也不大。
舉栗子我們來看下一些實現。
首先是 Github 的 Fork。如圖所示:
首先 fork 操作不提供默認選項。但使用 Tab 和 (Shift + Tab) 時,焦點只會在整個彈出層中用紫色框線標出的四個部分中依次切換。回車鍵按下時,則會 fork 到相應賬號,或者取消操作、關閉彈出層。
接著看 SweetAlert,以第一個 Confirm 為例:
Confirm 彈出后,默認選項是右邊的 “Yes, delete it!”,點擊回車鍵,就會產生確認反饋。通過 Tab 操作幾次發現,整個頁面的焦點,只能夠在圖中兩個按鈕之間切換。同時仔細觀察還能,獲得焦點狀態(:focus)的按鈕,會有一個不太容易察覺的 box-shadow。
再來看看 Vue 組件庫 Element 的 MessageBox 組件 中的 “確認消息” Demo。
先說結果,嗯,有提供默認選項,切換到不同選項時,也會有視覺提示(“取消”按鈕的顏色和邊框色會變,“確認”按鈕背景亮度有明顯變化)。然而有兩點做得還不夠:第一,無法通過 Tab 輕易切換到“?”關閉按鈕;第二點,也是最致命的是,根本就沒有獲取到 Tab 的控制權,以至于多按幾次 Tab 然后回車,頁面跳到主頁,而彈框依然存在(如圖所示)。
和 Element 類似,Weui 中的 Dialog 也存在類似問題,但 Weui 主要面向移動端,倒也情有可原。
如何實現控制一開始我想到了 tabIndex 這個屬性。但這貨控制起來,十分不方便,在本文場景中使用它,簡直是惹火燒身。
那么我能想到最簡單的實現,莫過于手動調用 focus 事件了。 Simple Demo 。代碼示例如下:
var $ = (sel) => document.querySelector(sel); $("#js-show-alert").addEventListener("click", () => { $("#js-prompt").style.display = "block"; $("#js-confirm").focus(); }); $("#js-cancel").addEventListener("click", () => { if (confirm("Are your sure?")) { $("#js-prompt").style.display = "none"; } }); $("#js-confirm").addEventListener("click", () => { $("#js-prompt").style.display = "none"; alert("Confirmed! The Prompt will disappear"); });
樣式方面,按鈕使用的是 button,整體并未 reset,所以按鈕獲取焦點時,是有 outline 的。這也提醒我們,可以直接通過 :focus 偽類來實現 tab 選中時的樣式,這樣做好懂又靠譜。
還存在另外一點問題,上面的代碼中,#js-confirm 元素是默認獲取焦點的。那么,假如我們不需要任何默認焦點,而又需要用戶按下 Tab 時,讓 #js-prompt 中的按鈕依次獲得焦點呢?
這里提一點不成熟的想法(23333333)。只要讓容器元素 #js-prompt 獲得焦點即可:$("#js-prompt").focus();。在控制臺上打印下 HTMLElement.prototype.focus,就會明白我的意思了。但這么做,可能是野路子(我還沒細看 HTML 中元素獲得焦點相關的規范)。
但是,上面 Demo 的實現,沒有考慮到 Tab 控制權的接管。要接管 Tab,就得使用事件綁定來完成。下面看下 Github 是如何實現的。
如何取得控制權找到 Github 對應的源碼,又通過開發者工具發現彈出層容器有個值為 facebox 的 id。
接下來,開始 Ctrl + F 大法。利用長久積累的火眼金睛,找到下面這樣兩段代碼:
就是它了。
先看第二段,整理后如下,重點直接劃在注釋中:
// 彈出層出現 document.addEventListener("facebox:reveal", function() { var t = document.getElementById("facebox"); setTimeout(function() { e(t) }, 0); // 為 document 綁定 keydown 事件 // 事件回調函數 n 就是第一張截圖的函數 r(document).on("keydown", n); }); // 彈出層關閉后 document.addEventListener("facebox:afterClose", function() { // 移除出現時候為 document // 綁定的 keydown 事件 r(document).off("keydown", n); // 彈出中已經獲得焦點的元素 // 強行失去焦點 r("#facebox :focus").blur() }); // ....
再來看看函數 n 做了什么。
function n(e) { var t = void 0; // hotkey 是自定義的 // 這里開始控制 tab 和 shift+tab 操作 if ("tab" === (t = e.hotkey) || "shift+tab" === t) { // 阻止默認事件 e.preventDefault(); // 彈出層 var n = r("#facebox"), // 變量 i 存儲彈出層中可以獲得焦點的、可見的、可用的 // input, button, .btn, textarea 等元素集合 // filter 用到的函數 o 用于過濾不可見的相關元素 // o = require("github/visible")["default"] i = r(Array.from(n.find("input, button, .btn, textarea")).filter(o)).filter(function() { return !this.disabled }), // tab 方向 s = "shift+tab" === e.hotkey ? -1 : 1, // i 集合中當前獲得焦點的元素的 index a = i.index(i.filter(":focus")), // i 集合中下一個獲得焦點的元素的位置 u = a + s; // 如果下一個獲得焦點的位置超出 i 集合范圍 // 或者當前沒有任何元素獲得焦點、并且使用的是向前的 tab 鍵 // 則將焦點置于 i 集合中的第一個元素上 if (u === i.length || -1 === a && "tab" === e.hotkey) { i.first().focus() } // 當前沒有任何元素獲得焦點 // 并且使用 shift+tab 向前 // 則將焦點置于 i 集合中最后一個元素 else if(-1 === a){ i.last().focus(); } // 正常行為 // 將焦點置于下一個元素 else { i.get(u).focus(); } } }
好了,本文也就結束了。
嗯,今天(2016-12-04)我想起來那個完全無法使用的例子了:
本文作者:文藺
原文地址:http://www.wemlion.com/2016/a...
轉載請注明來源
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91228.html
摘要:尤其是遇到二次確認等場景因此,打算從頭整理移動彈窗的基礎知識,以彈窗體系為切入點,從定義出發,對移動彈窗進行分類,然后分別分析每一類彈窗的應用場景,以及在使用過程中需要注意的點。 摘要: 最為常見的【彈窗】反而是最捉摸不定的東西。各種類型的彈窗傻傻分不清楚,不知道在什么場景下應該用哪種彈窗。尤其是遇到二次確認等場景…… 因此,打算從頭整理移動彈窗的基礎知識,以iOS彈窗體系為切入點,從...
摘要:其他的項目使用了拼裝樣式驗證傳入的屬性是否是函數驗證父組件傳入的數據格式是否正確五參考文獻談談的使用使用場景 仿 taro-ui 實現 modal 組件 小程序組件. 簡介: 項目中使用到彈窗類的組件,重新制造了一個輪子. 源碼地址: https://github.com/xiangxiong... 編寫完modal組件總計花了28分鐘. 效果圖: showImg(htt...
摘要:其他的項目使用了拼裝樣式驗證傳入的屬性是否是函數驗證父組件傳入的數據格式是否正確五參考文獻談談的使用使用場景 仿 taro-ui 實現 modal 組件 小程序組件. 簡介: 項目中使用到彈窗類的組件,重新制造了一個輪子. 源碼地址: https://github.com/xiangxiong... 編寫完modal組件總計花了28分鐘. 效果圖: showImg(htt...
閱讀 3371·2023-04-25 14:07
閱讀 3436·2021-09-28 09:35
閱讀 2079·2019-08-30 15:55
閱讀 1396·2019-08-30 13:48
閱讀 2496·2019-08-30 13:16
閱讀 3196·2019-08-30 12:54
閱讀 3231·2019-08-30 11:19
閱讀 1868·2019-08-29 17:17