-
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.
Fixed Popover focusing incorrect element when closed
- Loading branch information
1 parent
befe767
commit 6d89db1
Showing
4 changed files
with
175 additions
and
11 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,39 @@ | ||
import { | ||
findFirstFocusableNode, | ||
FOCUSABLE_SELECTOR, | ||
} from '@shopify/javascript-utilities/focus'; | ||
|
||
export function handleMouseUpByBlurring({ | ||
currentTarget, | ||
}: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) { | ||
currentTarget.blur(); | ||
} | ||
|
||
export function nextFocusableNode( | ||
node: HTMLElement, | ||
): HTMLElement | Element | null { | ||
let nextElement = node.nextElementSibling; | ||
|
||
while (nextElement) { | ||
if (nextElement.matches(FOCUSABLE_SELECTOR)) return nextElement; | ||
|
||
if (nextElement instanceof HTMLElement) { | ||
const innerFocusableElement = findFirstFocusableNode(nextElement); | ||
if (innerFocusableElement) return innerFocusableElement; | ||
} | ||
|
||
nextElement = nextElement.nextElementSibling; | ||
} | ||
|
||
return nextElement; | ||
} | ||
|
||
export function focusNextFocusableNode(node: HTMLElement) { | ||
const nextFocusable = nextFocusableNode(node); | ||
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 |
---|---|---|
@@ -1,14 +1,100 @@ | ||
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("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); | ||
}); | ||
}); | ||
|
||
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; | ||
} = {}, | ||
) { | ||
const { | ||
wrapperTag = 'div', | ||
activatorTag = 'button', | ||
otherNodeTag = 'button', | ||
otherNodeNestedTag = 'button', | ||
nested, | ||
} = options; | ||
const wrapper = document.createElement(wrapperTag); | ||
const activator = document.createElement(activatorTag); | ||
const otherNode = document.createElement(otherNodeTag); | ||
let otherNodeNested = null; | ||
|
||
if (nested) { | ||
otherNodeNested = document.createElement(otherNodeNestedTag); | ||
otherNode.appendChild(otherNodeNested); | ||
} | ||
|
||
wrapper.append(activator, otherNode); | ||
|
||
return {wrapper, activator, otherNode, otherNodeNested}; | ||
} |