Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ _As defined by CSS 4 and / or jQuery._
elements that are links and have not been visited.
- [`:visited`](https://developer.mozilla.org/en-US/docs/Web/CSS/:visited),
[`:hover`](https://developer.mozilla.org/en-US/docs/Web/CSS/:hover),
[`:active`](https://developer.mozilla.org/en-US/docs/Web/CSS/:active)
[`:active`](https://developer.mozilla.org/en-US/docs/Web/CSS/:active),
[`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible),
[`:focus-within`](https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within)
(these depend on optional `Adapter` methods, so these will only match
elements if implemented in `Adapter`)
- [`:checked`](https://developer.mozilla.org/en-US/docs/Web/CSS/:checked):
Expand Down
43 changes: 42 additions & 1 deletion src/pseudo-selectors/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,58 @@ export const filters: Record<string, Filter> = {
},

hover: dynamicStatePseudo("isHovered"),
"focus-visible": dynamicStatePseudo("isFocusVisible"),
"focus-within": focusWithinPseudo,
visited: dynamicStatePseudo("isVisited"),
active: dynamicStatePseudo("isActive"),
};

/**
* `:focus-within` matches an element that is focused or has a focused descendant.
* Depends on the optional `Adapter.isFocused` method.
*/
function focusWithinPseudo<Node, ElementNode extends Node>(
next: CompiledQuery<ElementNode>,
_rule: string,
options: InternalOptions<Node, ElementNode>,
): CompiledQuery<ElementNode> {
const { adapter } = options;
const isFocused = adapter.isFocused;

if (typeof isFocused !== "function") {
return boolbase.falseFunc;
}

return cacheParentResults(next, options, (element) => {
if (isFocused(element)) {
return true;
}

const queue = [...adapter.getChildren(element)];

for (const node of queue) {
if (!adapter.isTag(node)) {
continue;
}

if (isFocused(node)) {
return true;
}

queue.push(...adapter.getChildren(node));
}

return false;
});
}

/**
* Dynamic state pseudos. These depend on optional Adapter methods.
* @param name The name of the adapter method to call.
* @returns Pseudo for the `filters` object.
*/
function dynamicStatePseudo(
name: "isHovered" | "isVisited" | "isActive",
name: "isHovered" | "isFocusVisible" | "isVisited" | "isActive",
): Filter {
return function dynamicPseudo(next, _rule, { adapter }) {
const filterFunction = adapter[name];
Expand Down
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ export interface Adapter<Node, ElementNode extends Node> {
*/
isHovered?: (element: ElementNode) => boolean;

/**
* Is the element focused?
*/
isFocused?: (element: ElementNode) => boolean;

/**
* Is the element in focus-visible state?
*/
isFocusVisible?: (element: ElementNode) => boolean;

/**
* Is the element in visited state?
*/
Expand Down
51 changes: 51 additions & 0 deletions test/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,5 +460,56 @@ describe("API", () => {
const dom = parseDocument(`${"<p>foo".repeat(10)}`);
expect(CSSselect.selectAll("p:hover", dom)).toHaveLength(0);
});

it("should support isFocusVisible", () => {
const dom = parseDocument(`${"<button>foo</button>".repeat(3)}`)
.children as Element[];

const adapter = {
...DomUtils,
isTag,
isFocusVisible: (element: Element) =>
element === dom[dom.length - 1],
};

const selection = CSSselect.selectAll("button:focus-visible", dom, {
adapter,
});
expect(selection).toHaveLength(1);
expect(selection[0]).toBe(dom[dom.length - 1]);
});

it("should not match any elements if `isFocusVisible` is not defined", () => {
const dom = parseDocument(`${"<button>foo</button>".repeat(3)}`);
expect(CSSselect.selectAll("button:focus-visible", dom)).toHaveLength(
0,
);
});

it("should support isFocused for :focus-within", () => {
const [dom] = parseDocument(
"<div><p><span>foo</span></p><p>bar</p></div>",
).children as Element[];
const focused = ((dom.children[0] as Element).children[0] as Element);

const adapter = {
...DomUtils,
isTag,
isFocused: (element: Element) => element === focused,
};

const selection = CSSselect.selectAll(":focus-within", [dom], {
adapter,
});
expect(selection).toHaveLength(3);
expect(selection).toContain(dom);
expect(selection).toContain(dom.children[0]);
expect(selection).toContain(focused);
});

it("should not match any elements if `isFocused` is not defined", () => {
const dom = parseDocument("<div><span>foo</span></div>");
expect(CSSselect.selectAll(":focus-within", dom)).toHaveLength(0);
});
});
});