Page MenuHomePhorge

No OneTemporary

Size
4 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
index 207c3e779d..ab8e8d45e4 100644
--- a/src/components/popover/popover.js
+++ b/src/components/popover/popover.js
@@ -87,6 +87,8 @@ const Popover = {
teleport: false,
lockReEntry: false,
hidden: true,
+ nextFocus: null,
+ redirectFocus: false,
styles: {},
oldSize: { width: 0, height: 0 },
scrollable: null,
@@ -334,6 +336,8 @@ const Popover = {
hidePopover() {
if (this.disabled) return
if (!this.hidden) this.$emit('close')
+ this.redirectFocus = false
+ this.nextFocus = null
this.hidden = true
this.parentPopover?.onChildPopoverState(this, false)
if (this.trigger === 'click') {
@@ -373,6 +377,65 @@ const Popover = {
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
}
},
+
+ // # Focus management.
+ //
+ // Since our content is far away in separate #modals or #popovers container
+ // the native tabindex flow is broken, but we still want user to be able to
+ // focus popover contents. To do so we do a bit of focus management.
+ //
+ // We do a little focus trapping and redirecting. First, we must establish
+ // our order. Opening a popover (including auto-open by hover/focus) and
+ // hitting tab should select content's first focusable element. Shift-tab
+ // away from popover trigger should hide popover and continue on as normal.
+ // While inside content, shift-tabbing from first focusable element should
+ // focus trigger again, and tabbing beyond last focusable element should
+ // focus element next in line relative to trigger.
+ //
+ // We use two tricks here:
+ // 1. we add two bogus ".focus-trap" elements before and after popover
+ // contents. Those are aria-hidden="1" and empty spans so they don't
+ // interfere with the UI or accessibility. We use them to detect when
+ // user tab-navigated beyond first and last focusable elements and act
+ // accordingly
+ // 2. when we open up a popover we listen for "Tab" keypress and engage
+ // "redirectFocus", so that when trigger loses focus (immideately after)
+ // we a) focus content's first focusable element and b) save reference
+ // to the next intended focus target.
+ //
+ // There is a blind spot in this approach. If user opened a popover and
+ // navigated in it with a mouse we never get to know the "next intended
+ // focus target" so focus order ends up being broken.
+ //
+ // There's probably a better way to figure out this "next target" but for
+ // now this will do.
+ onFocus(e) {
+ if (this.trigger === 'hover') {
+ this.showPopover()
+ }
+ },
+ onBlur(e) {
+ if (this.redirectFocus) {
+ const firstFocusable = this.$refs.content?.querySelector('*:not(.focus-trap)').querySelector('a[href], button, [tabindex="0"], input')
+ if (firstFocusable) {
+ firstFocusable?.focus()
+ this.nextFocus = e.relatedTarget
+ } else {
+ this.hidePopover()
+ }
+ }
+ this.redirectFocus = false
+ },
+ onTab(e) {
+ this.redirectFocus = true
+ },
+ onForwardTabContentFocus() {
+ this.nextFocus?.focus()
+ this.hidePopover()
+ },
+ onReverseTabContentFocus() {
+ this.$refs.trigger.focus()
+ },
onClick(e) {
if (this.trigger === 'click') {
e.preventDefault()
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
index 4b750faa43..fec243f6e0 100644
--- a/src/components/popover/popover.scss
+++ b/src/components/popover/popover.scss
@@ -9,6 +9,12 @@
max-width: calc(100vw - 20px);
--_shadow: var(--shadow);
+
+ .focus-trap {
+ position: absolute;
+ width: 0;
+ height: 0;
+ }
}
.popover-default {
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
index 557da4942f..ea4e4a2dfd 100644
--- a/src/components/popover/popover.vue
+++ b/src/components/popover/popover.vue
@@ -9,8 +9,10 @@
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
@click="onClick"
- @focus="onMouseenter"
- @blur="onMouseleave"
+ @focus="onFocus"
+ @blur="onBlur"
+ @keydown.tab.exact="onTab"
+ @keydown.tab.shift.exact="hidePopover"
v-bind="allTriggerAttrs"
ref="trigger"
>
@@ -34,12 +36,24 @@
@click="onClickContent"
v-bind="$attrs"
>
+ <span
+ class="focus-trap -before"
+ aria-hidden="1"
+ tabindex="0"
+ @focus="onReverseTabContentFocus"
+ />
<slot
name="content"
class="popover-inner"
:close="hidePopover"
:resize="resizePopover"
/>
+ <span
+ class="focus-trap -after"
+ aria-hidden="1"
+ tabindex="0"
+ @focus="onForwardTabContentFocus"
+ />
</div>
</transition>
</teleport>

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 10, 4:08 PM (1 d, 5 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785345
Default Alt Text
(4 KB)

Event Timeline