Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85804870
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
4 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment