香草javascript陷阱焦点在模式(可访问性选项卡)

Vanilla javascript Trap Focus in modal (accessibility tabbing )

本文关键字:访问 选项 陷阱 javascript 焦点 模式 香草      更新时间:2023-09-26

这应该是相当简单的,但由于某些原因它不工作,我得到适当的控制台。日志在正确的时间,但焦点没有去正确的地方,请参考我的jsfiddle

https://jsfiddle.net/bqt0np9d/

function checkTabPress(e) {
    "use strict";
    // pick passed event of global event object
    e = e || event;
    if (e.keyCode === 9) {
        if (e.shiftKey) {
            console.log('back tab pressed');
            firstItem.onblur=function(){
                console.log('last a focus left');
                lastItem.focus();
            };
            e.preventDefault();
        }
        console.log('tab pressed');
        lastItem.onblur=function(){
            console.log('last a focus left');
            firstItem.focus();
        };
        e.preventDefault();
    }
}
modal.addEventListener('keyup', checkTabPress);

我必须将焦点锁定在我们在React组件中使用的模态中。我添加了eventlistener键下和收集Tab和Shift+Tab

class Modal extends Component {
    componentDidMount() {
        window.addEventListener("keyup", this.handleKeyUp, false);
        window.addEventListener("keydown", this.handleKeyDown, false);
    }
    componentWillUnmount() {
        window.removeEventListener("keyup", this.handleKeyUp, false);
        window.removeEventListener("keydown", this.handleKeyDown, false);
    }
    handleKeyDown = (e) => {
        //Fetch node list from which required elements could be grabbed as needed.
        const modal = document.getElementById("modal_parent");
        const tags = [...modal.querySelectorAll('select, input, textarea, button, a, li')].filter(e1 => window.getComputedStyle(e1).getPropertyValue('display') === 'block');
        const focusable = modal.querySelectorAll('button, [href], input, select, textarea, li, a,[tabindex]:not([tabindex="-1"])');
        const firstFocusable = focusable[0];
        const lastFocusable = focusable[focusable.length - 1];
        if (e.ctrlKey || e.altKey) {
            return;
        }
        const keys = {
            9: () => { //9 = TAB
                if (e.shiftKey && e.target === firstFocusable) {
                    lastFocusable.focus();
                }
                if (e.target === lastFocusable) {
                    firstFocusable.focus();
                }
            }
        };
        if (keys[e.keyCode]) {
            keys[e.keyCode]();
        }
    }
}

我认为我已经解决了在模态上捕获焦点的问题,方法是使用tab, shift+tab和箭头键检测键上和键下,focus, focusin, focusout在模态内的第一个和最后一个可聚焦元素上,以及为窗口设置焦点事件,以便在焦点"逃逸"的情况下将焦点设置回表单上的第一个可聚焦元素上。模态也适用于使用选项卡从地址栏跳转到文档的情况,但奇怪的事情发生了。我不小心在我的一个浏览器中激活了"插入符浏览",这时我意识到所有捕获焦点的方法都失败了。我个人用一只兔子来解决这个模态。我尝试了focusin, focusout在模态上,在伪类中匹配焦点,{capture: true}在模态和窗口的焦点事件上,没有工作。

这就是我解决它的方法。

我重新创建了模态以具有不同的结构。为了简单起见,我省略了很多东西,比如aria属性,类,如何获得所有可聚焦的元素等。

<component-name>
#shadow-root (closed)
<div class="wrapper">
    <div class="backdrop"></div>
    <div class="window>
        <div tabindex="0" class="trap-focus-top">&nbsp;</div>
        <div class="content">
            <div class="controls"><!-- Close button, whatever --></div>
            <header><slot name="header"></slot></header>
            <div class="body"><slot></slot></div>
            <footer><slot name="footer"></slot></footer>
        </div>
        <div tabindex="0" class="trap-focus-bottom">&nbsp;</div>
    </div>
</div>
</component-name>
  1. 在contentsdiv中搜索可聚焦的元素,保存第一个和最后一个。如果你只找到一个,那么这个将是第一个也是最后一个。如果找到零,则将body (.body)表索引的div设置为"0";这样你至少有一个元素可以设置焦点。

  2. 在contentdiv之前和之后,我们有两个可聚焦div, trap-focus-top和trap-focus-bottom,第一个在获得焦点时将焦点跳转到第一步检测到的最后一个可聚焦元素,第二个将焦点跳转到第一步检测到的第一个可聚焦元素。不需要捕获任何关键事件,只需将事件集中在这些元素上。如果你注意到非换行空格 ;对于trap-focus元素,这是为了模拟内容,因为我注意到箭头键穿过这些元素时没有触发任何事件。当我意识到这一点时,我添加了一些内容,一切都工作了,所以我添加了一个不中断的空间,并对元素进行了样式化,使它们不占用任何空间。

  3. 窗口捕获所有焦点事件,并将use捕获标志设置为true,这样每个目标与组件不同的焦点事件(阴影根中的焦点事件不会被实际目标捕获,而是组件本身)将导致焦点再次被设置在模态元素上。

现在还有另一个问题,假设在任何控件之外的模态上没有可聚焦的元素,比如关闭模态的按钮,然后我们在模态主体上设置tabindex为0,你的焦点应该从关闭按钮转到模态主体,反之亦然,现在,插入符号浏览在内容上不起作用因为div。body会有焦点,而不是实际的内容。这意味着我必须创建另一个函数,该函数在正文接收焦点时将光标放置在内容的开头。

startCursor = () => {
    /* componentbody is a placeholder for the element with the actual content */
    let text = componentbody.childNodes[0];
    if (text) {
        let range = new Range();
        let selection = document.getSelection();
        range.setStart(text, 0);
        range.setEnd(text, 0);
        selection.removeAllRanges();
        selection.addRange(range);
        componentbody.scrollTop = 0;/* In case the body has a scrollbar */
    }
}

对任何人来说,这就是我的工作。

其中一个问题是您正在使用keyup而不是keydown。keyup只会在选项卡已经启动后才会启动。但是,对代码进行更改会导致键盘被困在其中一个链接上。代码有缺陷。

下面是一些你想要的代码(使用jQuery)

http://dylanb.github.io/javascripts/periodic-1.1.js

// Add keyboard handling for TAB circling
  $modal.on('keydown', function (e) {
    var cancel = false;
    if (e.ctrlKey || e.metaKey || e.altKey) {
      return;
    }
    switch(e.which) {
      case 27: // ESC
        $modal.hide();
        lastfocus.focus();
        cancel = true;
        break;
      case 9: // TAB
        if (e.shiftKey) {
          if (e.target === links[0]) {
            links[links.length - 1].focus();
            cancel = true;
          }
        } else {
          if (e.target === links[links.length - 1]) {
            links[0].focus();
            cancel = true;
          }
        }
        break;
    }
    if (cancel) {
      e.preventDefault();
    }
  });

你可以在这里看到这个对话框的工作版本

http://dylanb.github.io/periodic-aria11-attributes.html

单击其中一个彩色框中的文本以查看弹出的对话框。

e.preventDefault()keyup事件没有影响(默认浏览器动作已经被触发)

尽管如此,您的示例仍然有效。但前提是模态

前后都有链接

如果你用下面的代码改变你的HTML代码,在模态的前面和后面分别添加一个链接;你会发现你的焦点被困在了模态:

 <a href="#">other link</a>
 <div id="modal">
     <a href="#">Link One</a>
     <a href="#">Link Two</a>
 </div>
 <a href="#">other link</a>

这是因为在这种情况下没有默认的浏览器操作,因此也没有阻止的操作。

在模态中捕获焦点是非常困难的。如果你能够在你的项目中安装第三方依赖,你可以使用focus-trap包。

你可以很容易地用Javascript捕获焦点到任何组件;

import { createFocusTrap } from 'focus-trap'
const modal = document.getElementById('modal')
const focusTrap = createFocusTrap('#modal', {
    onActivate: function () {
        modal.className = 'trap is-visible'
    },
    onDeactivate: function () {
        modal.className = 'trap'
    },
})
document.getElementById('show').addEventListener('click', function () {
    focusTrap.activate()
})
document.getElementById('hide').addEventListener('click', function () {
    focusTrap.deactivate()
})

甚至React;

import React from 'react'
import ReactDOM from 'react-dom'
// Use the wrapper package of `focus-trap` to use with React.
import FocusTrap from 'focus-trap-react'
const Demo = () => {
    const [showModal, setShowModal] = React.useState(false)
    return (
        <div>
            <button onClick={() => setShowModal(true)}>show modal</button>
            <FocusTrap active={showModal}>
                <div id="modal">
                    Modal with <a href="#">with</a> <a href="#">some</a>{' '}
                    <a href="#">focusable</a> elements.
                    <button onClick={() => setShowModal(false)}>
                        hide modal
                    </button>
                </div>
            </FocusTrap>
        </div>
    )
}
ReactDOM.render(<Demo />, document.getElementById('demo'))

我在这里写了一篇关于这个包的小文章,解释了如何在Javascript或React中使用它。