-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2255 from Shopify/popover-focus-next
[Popover ] Fixed incorrect element being focused when closed
- Loading branch information
Showing
10 changed files
with
324 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,42 @@ | ||
import {FOCUSABLE_SELECTOR} from '@shopify/javascript-utilities/focus'; | ||
import {isElementInViewport} from './is-element-in-viewport'; | ||
|
||
type Filter = (element: Element) => void; | ||
|
||
export function handleMouseUpByBlurring({ | ||
currentTarget, | ||
}: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) { | ||
currentTarget.blur(); | ||
} | ||
|
||
export function nextFocusableNode( | ||
node: HTMLElement, | ||
filter?: Filter, | ||
): HTMLElement | Element | null { | ||
const allFocusableElements = [ | ||
...document.querySelectorAll(FOCUSABLE_SELECTOR), | ||
]; | ||
const sliceLocation = allFocusableElements.indexOf(node) + 1; | ||
const focusableElementsAfterNode = allFocusableElements.slice(sliceLocation); | ||
|
||
for (const focusableElement of focusableElementsAfterNode) { | ||
if ( | ||
isElementInViewport(focusableElement) && | ||
(!filter || (filter && filter(focusableElement))) | ||
) { | ||
return focusableElement; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
export function focusNextFocusableNode(node: HTMLElement, filter?: Filter) { | ||
const nextFocusable = nextFocusableNode(node, filter); | ||
if (nextFocusable && nextFocusable instanceof HTMLElement) { | ||
nextFocusable.focus(); | ||
return true; | ||
} | ||
|
||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export function isElementInViewport(element: Element) { | ||
const {top, left, bottom, right} = element.getBoundingClientRect(); | ||
|
||
return ( | ||
top >= 0 && | ||
right <= window.innerWidth && | ||
bottom <= window.innerHeight && | ||
left >= 0 | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,128 @@ | ||
import {MouseEvent} from 'react'; | ||
import {handleMouseUpByBlurring} from '../focus'; | ||
|
||
describe('focus', () => { | ||
describe('handleMouseUpByBlurring()', () => { | ||
it('calls blur on the currentTarget', () => { | ||
const currentTarget = document.createElement('button'); | ||
jest.spyOn(currentTarget, 'blur'); | ||
const mouseEvent = {currentTarget}; | ||
handleMouseUpByBlurring(mouseEvent as MouseEvent<HTMLButtonElement>); | ||
expect(currentTarget.blur).toHaveBeenCalled(); | ||
import { | ||
handleMouseUpByBlurring, | ||
focusNextFocusableNode, | ||
nextFocusableNode, | ||
} from '../focus'; | ||
|
||
describe('handleMouseUpByBlurring()', () => { | ||
it('calls blur on the currentTarget', () => { | ||
const currentTarget = document.createElement('button'); | ||
jest.spyOn(currentTarget, 'blur'); | ||
const mouseEvent = {currentTarget}; | ||
handleMouseUpByBlurring(mouseEvent as MouseEvent<HTMLButtonElement>); | ||
expect(currentTarget.blur).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('nextFocusableNode', () => { | ||
it('does not return the initial element as the focusable node', () => { | ||
const {activator, otherNode} = domSetup(); | ||
|
||
expect(nextFocusableNode(activator)).toBe(otherNode); | ||
}); | ||
|
||
it('returns null when a focusable element is not found', () => { | ||
const {activator} = domSetup({ | ||
otherNodeTag: 'div', | ||
}); | ||
|
||
expect(nextFocusableNode(activator)).toBeNull(); | ||
}); | ||
|
||
it('filters out elements', () => { | ||
const {activator} = domSetup(); | ||
|
||
expect(nextFocusableNode(activator, () => false)).toBeNull(); | ||
}); | ||
|
||
it("returns the parent of an adjacent element when it's focusable", () => { | ||
const {activator, otherNode} = domSetup(); | ||
|
||
expect(nextFocusableNode(activator)).toBe(otherNode); | ||
}); | ||
|
||
it('searches adjacent elements for focusable children', () => { | ||
const {activator, otherNodeNested} = domSetup({ | ||
otherNodeTag: 'div', | ||
nested: true, | ||
}); | ||
|
||
expect(nextFocusableNode(activator)).toBe(otherNodeNested); | ||
}); | ||
|
||
it('searches parent elements for focusable children', () => { | ||
const {activator, parentsFocusableNode} = domSetup({ | ||
otherNodeTag: 'div', | ||
parents: true, | ||
}); | ||
|
||
expect(nextFocusableNode(activator)).toBe(parentsFocusableNode); | ||
}); | ||
}); | ||
|
||
describe('focusNextFocusableNode', () => { | ||
it('returns true when the node was focused', () => { | ||
const {activator} = domSetup(); | ||
|
||
expect(focusNextFocusableNode(activator)).toBe(true); | ||
}); | ||
|
||
it('returns false when the node was not focused', () => { | ||
const {activator} = domSetup({otherNodeTag: 'div'}); | ||
|
||
expect(focusNextFocusableNode(activator)).toBe(false); | ||
}); | ||
|
||
it('focused the node', () => { | ||
const {activator, otherNode} = domSetup(); | ||
|
||
focusNextFocusableNode(activator); | ||
|
||
expect(document.activeElement).toBe(otherNode); | ||
}); | ||
}); | ||
|
||
function domSetup( | ||
options: { | ||
wrapperTag?: string; | ||
activatorTag?: string; | ||
otherNodeTag?: string; | ||
otherNodeNestedTag?: string; | ||
nested?: boolean; | ||
parents?: true; | ||
} = {}, | ||
) { | ||
const div = 'div'; | ||
const button = 'button'; | ||
const { | ||
wrapperTag = div, | ||
activatorTag = button, | ||
otherNodeTag = button, | ||
otherNodeNestedTag = button, | ||
nested, | ||
parents, | ||
} = options; | ||
const wrapper = document.createElement(wrapperTag); | ||
const activator = document.createElement(activatorTag); | ||
const otherNode = document.createElement(otherNodeTag); | ||
let otherNodeNested = null; | ||
let parentNode = null; | ||
let parentsFocusableNode = null; | ||
|
||
if (nested) { | ||
otherNodeNested = document.createElement(otherNodeNestedTag); | ||
otherNode.appendChild(otherNodeNested); | ||
} | ||
|
||
wrapper.append(activator, otherNode); | ||
|
||
if (parents) { | ||
parentNode = document.createElement(div); | ||
parentsFocusableNode = document.createElement(button); | ||
parentNode.append(wrapper, parentsFocusableNode); | ||
} | ||
|
||
document.body.appendChild(parentNode || wrapper); | ||
return {wrapper, activator, otherNode, otherNodeNested, parentsFocusableNode}; | ||
} |
Oops, something went wrong.