-
-
Notifications
You must be signed in to change notification settings - Fork 245
feat: manage focus for accessible click/context popups #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,10 @@ import PopupContent from './PopupContent'; | |
| import useOffsetStyle from '../hooks/useOffsetStyle'; | ||
| import { useEvent } from '@rc-component/util'; | ||
| import type { PortalProps } from '@rc-component/portal'; | ||
| import { | ||
| focusPopupRootOrFirst, | ||
| handlePopupTabTrap, | ||
| } from '../focusUtils'; | ||
|
|
||
| export interface MobileConfig { | ||
| mask?: boolean; | ||
|
|
@@ -85,6 +89,12 @@ export interface PopupProps { | |
|
|
||
| // Mobile | ||
| mobile?: MobileConfig; | ||
|
|
||
| /** | ||
| * Move focus into the popup when it opens and return it to `target` when it closes. | ||
| * Tab cycles within the popup. Escape is handled by Portal `onEsc`. | ||
| */ | ||
| focusPopup?: boolean; | ||
| } | ||
|
|
||
| const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | ||
|
|
@@ -149,8 +159,13 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | |
| stretch, | ||
| targetWidth, | ||
| targetHeight, | ||
|
|
||
| focusPopup, | ||
| } = props; | ||
|
|
||
| const rootRef = React.useRef<HTMLDivElement>(null); | ||
| const prevOpenRef = React.useRef(false); | ||
|
|
||
| const popupContent = typeof popup === 'function' ? popup() : popup; | ||
|
|
||
| // We can not remove holder only when motion finished. | ||
|
|
@@ -208,12 +223,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | |
| offsetY, | ||
| ); | ||
|
|
||
| // ========================= Render ========================= | ||
| if (!show) { | ||
| return null; | ||
| } | ||
|
|
||
| // >>>>> Misc | ||
| // >>>>> Misc (computed before conditional return; hooks must run every render) | ||
| const miscStyle: React.CSSProperties = {}; | ||
| if (stretch) { | ||
| if (stretch.includes('height') && targetHeight) { | ||
|
|
@@ -232,6 +242,50 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | |
| miscStyle.pointerEvents = 'none'; | ||
| } | ||
|
|
||
| useLayoutEffect(() => { | ||
| if (!focusPopup) { | ||
| prevOpenRef.current = open; | ||
| return; | ||
| } | ||
|
|
||
| const root = rootRef.current; | ||
| const wasOpen = prevOpenRef.current; | ||
| prevOpenRef.current = open; | ||
|
|
||
| if (open && !wasOpen && root && isNodeVisible) { | ||
| focusPopupRootOrFirst(root); | ||
| } else if (!open && wasOpen && root) { | ||
| const active = document.activeElement as HTMLElement | null; | ||
| // Only restore trigger focus if focus is still inside the popup (e.g. Escape). | ||
| // If the user dismissed by clicking elsewhere, activeElement may already be | ||
| // outside — avoid stealing focus from that target with target.focus(). | ||
| if ( | ||
| target?.isConnected && | ||
| active && | ||
| (root === active || root.contains(active)) | ||
| ) { | ||
|
Comment on lines
+258
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we check whether we have an active element before moving the focus to the target (which I assume is the previously focused element prior opening the popover)? I think the code can be simplified into: |
||
| target.focus(); | ||
| } | ||
| } | ||
| }, [open, focusPopup, isNodeVisible, target]); | ||
|
|
||
| const onPopupKeyDownCapture = useEvent( | ||
| (e: React.KeyboardEvent<HTMLDivElement>) => { | ||
| if (!focusPopup || !open) { | ||
| return; | ||
| } | ||
| const root = rootRef.current; | ||
| if (root) { | ||
| handlePopupTabTrap(e, root); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| // ========================= Render ========================= | ||
| if (!show) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Portal | ||
| open={forceRender || isNodeVisible} | ||
|
|
@@ -276,7 +330,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | |
|
|
||
| return ( | ||
| <div | ||
| ref={composeRef(resizeObserverRef, ref, motionRef)} | ||
| ref={composeRef(resizeObserverRef, ref, motionRef, rootRef)} | ||
| className={cls} | ||
| style={ | ||
| { | ||
|
|
@@ -295,6 +349,7 @@ const Popup = React.forwardRef<HTMLDivElement, PopupProps>((props, ref) => { | |
| onPointerEnter={onPointerEnter} | ||
| onClick={onClick} | ||
| onPointerDownCapture={onPointerDownCapture} | ||
| onKeyDownCapture={onPopupKeyDownCapture} | ||
| > | ||
| {arrow && ( | ||
| <Arrow | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,171 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type * as React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import focusableSelectors from 'focusable-selectors'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TABBABLE_SELECTOR = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| focusableSelectors.join(','); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Subtree cannot contain tab stops the browser will use. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @see https://github.com/KittyGiraudel/a11y-dialog/blob/4674ff3e4d626430a028a64969328e339c533ce8/src/dom-utils.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function canHaveTabbableChildren(el: HTMLElement): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (el.shadowRoot && el.getAttribute('tabindex') === '-1') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return !el.matches(':disabled, [hidden], [inert]'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isNonVisibleForInteraction(el: HTMLElement): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.matches('details:not([open]) *') && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !el.matches('details > summary:first-of-type') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return !( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.offsetWidth || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.offsetHeight || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el.getClientRects().length | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function isTabbable(el: HTMLElement, win: Window): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (el.shadowRoot?.delegatesFocus) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!el.matches(TABBABLE_SELECTOR)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isNonVisibleForInteraction(el)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (el.closest('[aria-hidden="true"]') || el.closest('[inert]')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const style = win.getComputedStyle(el); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (style.display === 'none' || style.visibility === 'hidden') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The suggestion from Gemini makes sense. You want to check for the element’s offset dimensions and bounding rects instead of every possible way of manually hiding an element with CSS. function isVisible(element) {
return Boolean(
element.offsetWidth ||
element.offsetHeight ||
element.getClientRects().length
)
}
Comment on lines
+45
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is redundant with
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isNonVisibleForInteraction() catches elements that have no rendered box (offsetWidth/offsetHeight/getClientRects() all empty), which maps well to many display:none cases. But visibility:hidden elements can still have layout dimensions and client rects, so they can slip past that check. The explicit style guard prevents those from being treated as tabbable. So I'd keep this block, mainly for visibility:hidden correctness. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getNextChildEl(parent: ParentNode, forward: boolean): Element | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return forward ? parent.firstElementChild : parent.lastElementChild; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getNextSiblingEl(el: Element, forward: boolean): Element | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return forward ? el.nextElementSibling : el.previousElementSibling; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * First or last tabbable descendant in tree order (light DOM, shadow roots, slots). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @see https://github.com/KittyGiraudel/a11y-dialog/blob/4674ff3e4d626430a028a64969328e339c533ce8/src/dom-utils.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function findTabbableEl( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| el: HTMLElement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| forward: boolean, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| win: Window, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): HTMLElement | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (forward && isTabbable(el, win)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return el; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (canHaveTabbableChildren(el)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (el.shadowRoot) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let next = getNextChildEl(el.shadowRoot, forward); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hit = findTabbableEl(next as HTMLElement, forward, win); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hit) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next = getNextSiblingEl(next, forward); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (el.localName === 'slot') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const assigned = (el as HTMLSlotElement).assignedElements({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flatten: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) as HTMLElement[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ordered = forward ? assigned : [...assigned].reverse(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < ordered.length; i += 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hit = findTabbableEl(ordered[i], forward, win); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hit) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 插槽的 fallback 内容现在不会参与遍历。 当 🔧 建议修改 } else if (el.localName === 'slot') {
const assigned = (el as HTMLSlotElement).assignedElements({
flatten: true,
}) as HTMLElement[];
- const ordered = forward ? assigned : [...assigned].reverse();
+ const candidates =
+ assigned.length > 0
+ ? assigned
+ : (Array.from(el.children) as HTMLElement[]);
+ const ordered = forward ? candidates : [...candidates].reverse();
for (let i = 0; i < ordered.length; i += 1) {
const hit = findTabbableEl(ordered[i], forward, win);
if (hit) {
return hit;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let next = getNextChildEl(el, forward); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (next) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hit = findTabbableEl(next as HTMLElement, forward, win); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hit) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hit; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| next = getNextSiblingEl(next, forward); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!forward && isTabbable(el, win)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return el; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** First and last tabbable nodes inside `container` (inclusive). `last === first` if only one. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function getTabbableEdges( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container: HTMLElement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): readonly [HTMLElement | null, HTMLElement | null] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const win = container.ownerDocument.defaultView!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const first = findTabbableEl(container, true, win); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const last = first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? findTabbableEl(container, false, win) || first | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [first, last] as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function focusPopupRootOrFirst( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container: HTMLElement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): HTMLElement | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [first] = getTabbableEdges(container); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (first) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return first; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!container.hasAttribute('tabindex')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container.setAttribute('tabindex', '-1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return container; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function handlePopupTabTrap( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e: React.KeyboardEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container: HTMLElement, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key !== 'Tab' || e.defaultPrevented) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [first, last] = getTabbableEdges(container); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const active = document.activeElement as HTMLElement | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!active || !container.contains(active)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+151
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not understand this: if the container doesn’t contain the active element, then the trap has failed, hasn’t it? Isn’t the whole idea to ensure the container keeps containing the active element while the trap is up?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should keep this guard. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!first || !last) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (active === container) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!e.shiftKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (active === last || active === container) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (active === first || active === container) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last.focus(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding third-party packages requires confirmation.