Skip to content

Commit

Permalink
feat: [#1515] Adds support for the selectors :focus and :focus-visible (
Browse files Browse the repository at this point in the history
#1520)

* feat: [#1515] Adds support for the selectors :focus and :focus-visible

* chore: [#1515] Attempts to fix flaky unit tests
  • Loading branch information
capricorn86 authored Aug 29, 2024
1 parent 2b20cc3 commit 71d243a
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 24 deletions.
27 changes: 14 additions & 13 deletions packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ export default class BrowserFrameNavigator {
// Fixes issue where evaluating the response can throw an error.
// By using requestAnimationFrame() the error will not reject the promise.
// The error will be caught by process error level listener or a try and catch in the requestAnimationFrame().
frame.window.requestAnimationFrame(() => frame.window.eval(code));

// We need to wait for the next tick before resolving navigation listeners and ending the ready state task.
await new Promise((resolve) => frame.window.setTimeout(() => resolve(null)));
await new Promise((resolve) => {
frame.window.requestAnimationFrame(() => {
frame.window.requestAnimationFrame(resolve);
frame.window.eval(code);
});
});

readyStateManager.endTask();
resolveNavigationListeners();
Expand Down Expand Up @@ -235,15 +237,14 @@ export default class BrowserFrameNavigator {
// Fixes issue where evaluating the response can throw an error.
// By using requestAnimationFrame() the error will not reject the promise.
// The error will be caught by process error level listener or a try and catch in the requestAnimationFrame().
frame.window.requestAnimationFrame(() => (frame.content = responseText));

// Finalize the navigation
await new Promise((resolve) =>
frame.window.setTimeout(() => {
finalize();
resolve(null);
})
);
await new Promise((resolve) => {
frame.window.requestAnimationFrame(() => {
frame.window.requestAnimationFrame(resolve);
frame.content = responseText;
});
});

finalize();

return response;
}
Expand Down
29 changes: 18 additions & 11 deletions packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ export default class HTMLElementUtility {
* @param element Element.
*/
public static blur(element: HTMLElement | SVGElement): void {
const document = element[PropertySymbol.ownerDocument];

if (
element[PropertySymbol.ownerDocument][PropertySymbol.activeElement] !== element ||
document[PropertySymbol.activeElement] !== element ||
!element[PropertySymbol.isConnected]
) {
return;
}

const relatedTarget =
element[PropertySymbol.ownerDocument][PropertySymbol.nextActiveElement] ?? null;
const relatedTarget = document[PropertySymbol.nextActiveElement] ?? null;

document[PropertySymbol.activeElement] = null;

element[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = null;
document[PropertySymbol.clearCache]();

element.dispatchEvent(
new FocusEvent('blur', {
Expand All @@ -49,26 +52,30 @@ export default class HTMLElementUtility {
* @param element Element.
*/
public static focus(element: HTMLElement | SVGElement): void {
const document = element[PropertySymbol.ownerDocument];

if (
element[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === element ||
document[PropertySymbol.activeElement] === element ||
!element[PropertySymbol.isConnected]
) {
return;
}

// Set the next active element so `blur` can use it for `relatedTarget`.
element[PropertySymbol.ownerDocument][PropertySymbol.nextActiveElement] = element;
document[PropertySymbol.nextActiveElement] = element;

const relatedTarget = element[PropertySymbol.ownerDocument][PropertySymbol.activeElement];
const relatedTarget = document[PropertySymbol.activeElement];

if (element[PropertySymbol.ownerDocument][PropertySymbol.activeElement] !== null) {
element[PropertySymbol.ownerDocument][PropertySymbol.activeElement].blur();
if (document[PropertySymbol.activeElement] !== null) {
document[PropertySymbol.activeElement].blur();
}

// Clean up after blur, so it does not affect next blur call.
element[PropertySymbol.ownerDocument][PropertySymbol.nextActiveElement] = null;
document[PropertySymbol.nextActiveElement] = null;

document[PropertySymbol.activeElement] = element;

element[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = element;
document[PropertySymbol.clearCache]();

element.dispatchEvent(
new FocusEvent('focus', {
Expand Down
1 change: 1 addition & 0 deletions packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,7 @@ export default class Node extends EventTarget {
this[PropertySymbol.rootNode] = null;

if (this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] === <unknown>this) {
this[PropertySymbol.ownerDocument][PropertySymbol.clearCache]();
this[PropertySymbol.ownerDocument][PropertySymbol.activeElement] = null;
}

Expand Down
7 changes: 7 additions & 0 deletions packages/happy-dom/src/query-selector/QuerySelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ export default class QuerySelector {

element[PropertySymbol.cache].matches.set(selector, cachedItem);

if (element[PropertySymbol.isConnected]) {
// Document is affected for the ":target" selector
(element[PropertySymbol.ownerDocument] || element)[PropertySymbol.affectsCache].push(
cachedItem
);
}

for (const items of SelectorParser.getSelectorGroups(selector, options)) {
const result = this.matchSelector(element, items.reverse(), cachedItem);

Expand Down
5 changes: 5 additions & 0 deletions packages/happy-dom/src/query-selector/SelectorItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ export default class SelectorItem {
}
}
return null;
case 'focus':
case 'focus-visible':
return element[PropertySymbol.ownerDocument].activeElement === element
? { priorityWeight: 10 }
: null;
default:
return null;
}
Expand Down
59 changes: 59 additions & 0 deletions packages/happy-dom/test/query-selector/QuerySelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,25 @@ describe('QuerySelector', () => {
"Failed to execute 'querySelectorAll' on 'HTMLDivElement': 'a#' is not a valid selector."
);
});

it('Returns true for selector with CSS pseudo ":focus" and ":focus-visible"', () => {
document.body.innerHTML = QuerySelectorHTML;
const span = <HTMLElement>document.querySelector('span.class1');
const div = <HTMLElement>document.querySelector('div.class1');

expect(document.querySelectorAll(':focus')[0]).toBe(document.body);
expect(document.querySelectorAll(':focus-visible')[0]).toBe(document.body);

span.focus();

expect(document.querySelectorAll(':focus')[0]).toBe(span);
expect(document.querySelectorAll(':focus-visible')[0]).toBe(span);

div.focus();

expect(document.querySelectorAll(':focus')[0]).toBe(div);
expect(document.querySelectorAll(':focus-visible')[0]).toBe(div);
});
});

describe('querySelector', () => {
Expand Down Expand Up @@ -1413,6 +1432,25 @@ describe('QuerySelector', () => {
expect(div.querySelector('span#datalist_id') === null).toBe(true);
expect(div.querySelector('span#span_id') === span).toBe(true);
});

it('Returns true for selector with CSS pseudo ":focus" and ":focus-visible"', () => {
document.body.innerHTML = QuerySelectorHTML;
const span = <HTMLElement>document.querySelector('span.class1');
const div = <HTMLElement>document.querySelector('div.class1');

expect(document.querySelector(':focus')).toBe(document.body);
expect(document.querySelector(':focus-visible')).toBe(document.body);

span.focus();

expect(document.querySelector(':focus')).toBe(span);
expect(document.querySelector(':focus-visible')).toBe(span);

div.focus();

expect(document.querySelector(':focus')).toBe(div);
expect(document.querySelector(':focus-visible')).toBe(div);
});
});

describe('matches()', () => {
Expand Down Expand Up @@ -1502,6 +1540,27 @@ describe('QuerySelector', () => {
expect(element.matches(':where(div)')).toBe(false);
});

it('Returns true for selector with CSS pseudo ":focus" and ":focus-visible"', () => {
document.body.innerHTML = QuerySelectorHTML;
const span = <HTMLElement>document.querySelector('span.class1');
const div = <HTMLElement>document.querySelector('div.class1');

expect(span.matches(':focus')).toBe(false);
expect(span.matches(':focus-visible')).toBe(false);

span.focus();

expect(span.matches(':focus')).toBe(true);
expect(span.matches(':focus-visible')).toBe(true);

div.focus();

expect(span.matches(':focus')).toBe(false);
expect(span.matches(':focus-visible')).toBe(false);
expect(div.matches(':focus')).toBe(true);
expect(div.matches(':focus-visible')).toBe(true);
});

it('Throws an error when providing an invalid selector', () => {
const div = document.createElement('div');
div.innerHTML = '<div class="foo"></div>';
Expand Down

0 comments on commit 71d243a

Please sign in to comment.