Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 38 additions & 1 deletion src/pseudo-selectors/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,17 +203,54 @@ export const filters: Record<string, Filter> = {
},

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

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);
});
});
});