Skip to content

Commit

Permalink
Merge branch 'main' into contain-property
Browse files Browse the repository at this point in the history
  • Loading branch information
rcj-siteimprove committed Sep 24, 2024
2 parents ea892cd + 8f8d779 commit cb48337
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/strange-masks-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@siteimprove/alfa-selector": minor
---

**Added:** The `:checked` pseudo-class is now supported.
1 change: 1 addition & 0 deletions .github/codeql/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ paths:
paths-ignore:
- "**/node_modules"
- "**/*.spec.js"
- "**/dist/**"
6 changes: 4 additions & 2 deletions docs/review/api/alfa-selector.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Nth } from '@siteimprove/alfa-css';
import { Option } from '@siteimprove/alfa-option';
import { Parser } from '@siteimprove/alfa-parser';
import { Parser as Parser_2 } from '@siteimprove/alfa-css';
import { Predicate } from '@siteimprove/alfa-predicate';
import { Refinement } from '@siteimprove/alfa-refinement';
import { Serializable } from '@siteimprove/alfa-json';
import { Slice } from '@siteimprove/alfa-slice';
Expand Down Expand Up @@ -362,6 +363,7 @@ export namespace List {

// Warning: (ae-forgotten-export) The symbol "Active" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "AnyLink" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Checked" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Disabled" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Empty" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "Enabled" needs to be exported by the entry point index.d.ts
Expand Down Expand Up @@ -390,14 +392,14 @@ export namespace List {
// Warning: (ae-forgotten-export) The symbol "Where" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export type PseudoClass = Active | AnyLink | Disabled | Empty | Enabled | FirstChild | FirstOfType | Focus | FocusVisible | FocusWithin | Has | Host | HostContext | Hover | Is | LastChild | LastOfType | Link | Not | NthChild | NthLastChild | NthLastOfType | NthOfType | OnlyChild | OnlyOfType | Root | Visited | Where;
export type PseudoClass = Active | AnyLink | Checked | Disabled | Empty | Enabled | FirstChild | FirstOfType | Focus | FocusVisible | FocusWithin | Has | Host | HostContext | Hover | Is | LastChild | LastOfType | Link | Not | NthChild | NthLastChild | NthLastOfType | NthOfType | OnlyChild | OnlyOfType | Root | Visited | Where;

// @public (undocumented)
export namespace PseudoClass {
// (undocumented)
export function isPseudoClass(value: unknown): value is PseudoClass;
// (undocumented)
export type JSON = Active.JSON | AnyLink.JSON | Disabled.JSON | Empty.JSON | Enabled.JSON | FirstChild.JSON | FirstOfType.JSON | Focus.JSON | FocusVisible.JSON | FocusWithin.JSON | Has.JSON | Host.JSON | HostContext.JSON | Hover.JSON | Is.JSON | LastChild.JSON | LastOfType.JSON | Link.JSON | Not.JSON | NthChild.JSON | NthLastChild.JSON | NthLastOfType.JSON | NthOfType.JSON | OnlyChild.JSON | OnlyOfType.JSON | Root.JSON | Visited.JSON | Where.JSON;
export type JSON = Active.JSON | AnyLink.JSON | Checked.JSON | Disabled.JSON | Empty.JSON | Enabled.JSON | FirstChild.JSON | FirstOfType.JSON | Focus.JSON | FocusVisible.JSON | FocusWithin.JSON | Has.JSON | Host.JSON | HostContext.JSON | Hover.JSON | Is.JSON | LastChild.JSON | LastOfType.JSON | Link.JSON | Not.JSON | NthChild.JSON | NthLastChild.JSON | NthLastOfType.JSON | NthOfType.JSON | OnlyChild.JSON | OnlyOfType.JSON | Root.JSON | Visited.JSON | Where.JSON;
const // (undocumented)
isHost: typeof Host.isHost;
// Warning: (ae-incompatible-release-tags) The symbol "parse" is marked as @public, but its signature references "Absolute" which is marked as @internal
Expand Down
48 changes: 48 additions & 0 deletions packages/alfa-selector/src/selector/simple/pseudo-class/checked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Element } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";

import { PseudoClassSelector } from "./pseudo-class.js";

const { hasAttribute, hasInputType, hasName } = Element;
const { and, or } = Predicate;

/**
* {@link https://drafts.csswg.org/selectors/#checked}
*/
export class Checked extends PseudoClassSelector<"checked"> {
public static of(): Checked {
return new Checked();
}

private constructor() {
super("checked");
}

public *[Symbol.iterator](): Iterator<Checked> {
yield this;
}

/**
* @privateRemarks
* Checkedness and selectedness can change during the lifecycle of an element,
* but we do not have access to that. We rely on the content attributes being
* correctly set in the snapshot we test.
*/
public matches = or(
and(hasInputType("checkbox", "radio"), hasAttribute("checked")),
and(hasName("option"), hasAttribute("selected")),
);

public toJSON(): Checked.JSON {
return super.toJSON();
}
}

export namespace Checked {
export interface JSON extends PseudoClassSelector.JSON<"checked"> {}

export const parse = PseudoClassSelector.parseNonFunctional(
"checked",
Checked.of,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Compound } from "../../compound.js";

import { Active } from "./active.js";
import { AnyLink } from "./any-link.js";
import { Checked } from "./checked.js";
import { Disabled } from "./disabled.js";
import { Empty } from "./empty.js";
import { Enabled } from "./enabled.js";
Expand Down Expand Up @@ -47,6 +48,7 @@ const { or } = Refinement;
export type PseudoClass =
| Active
| AnyLink
| Checked
| Disabled
| Empty
| Enabled
Expand Down Expand Up @@ -81,6 +83,7 @@ export namespace PseudoClass {
export type JSON =
| Active.JSON
| AnyLink.JSON
| Checked.JSON
| Disabled.JSON
| Empty.JSON
| Enabled.JSON
Expand Down Expand Up @@ -123,6 +126,7 @@ export namespace PseudoClass {
return either<Slice<Token>, PseudoClass, string>(
Active.parse,
AnyLink.parse,
Checked.parse,
Disabled.parse,
Empty.parse,
Enabled.parse,
Expand Down
1 change: 1 addition & 0 deletions packages/alfa-selector/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"./selector/simple/universal.ts",
"./selector/simple/pseudo-class/active.ts",
"./selector/simple/pseudo-class/any-link.ts",
"./selector/simple/pseudo-class/checked.ts",
"./selector/simple/pseudo-class/disabled.ts",
"./selector/simple/pseudo-class/empty.ts",
"./selector/simple/pseudo-class/enabled.ts",
Expand Down
81 changes: 51 additions & 30 deletions packages/alfa-selector/test/pseudo-class.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ test("#matches() checks if an element matches a :first-child selector", (t) => {
{b}
</div>;

t.equal(selector.matches(a), true);
t.equal(selector.matches(b), false);
t(selector.matches(a));
t(!selector.matches(b));
});

test("#matches() checks if an element matches a :last-child selector", (t) => {
Expand All @@ -77,8 +77,8 @@ test("#matches() checks if an element matches a :last-child selector", (t) => {
Hello
</div>;

t.equal(selector.matches(a), false);
t.equal(selector.matches(b), true);
t(!selector.matches(a));
t(selector.matches(b));
});

test("#matches() checks if an element matches an :only-child selector", (t) => {
Expand All @@ -98,8 +98,8 @@ test("#matches() checks if an element matches an :only-child selector", (t) => {
Hello
</div>;

t.equal(selector.matches(a), true);
t.equal(selector.matches(b), false);
t(selector.matches(a));
t(!selector.matches(b));
});

test("#matches() checks if an element matches an :nth-of-type selector", (t) => {
Expand All @@ -120,10 +120,10 @@ test("#matches() checks if an element matches an :nth-of-type selector", (t) =>
{d}
</div>;

t.equal(selector.matches(a), true);
t.equal(selector.matches(b), false);
t.equal(selector.matches(c), true);
t.equal(selector.matches(d), false);
t(selector.matches(a));
t(!selector.matches(b));
t(selector.matches(c));
t(!selector.matches(d));
});

test("#matches() checks if an element matches an :nth-last-of-type selector", (t) => {
Expand All @@ -144,10 +144,10 @@ test("#matches() checks if an element matches an :nth-last-of-type selector", (t
<span />
</div>;

t.equal(selector.matches(a), false);
t.equal(selector.matches(b), true);
t.equal(selector.matches(c), false);
t.equal(selector.matches(d), true);
t(!selector.matches(a));
t(selector.matches(b));
t(!selector.matches(c));
t(selector.matches(d));
});

test("#matches() checks if an element matches a :first-of-type selector", (t) => {
Expand All @@ -163,8 +163,8 @@ test("#matches() checks if an element matches a :first-of-type selector", (t) =>
{b}
</div>;

t.equal(selector.matches(a), true);
t.equal(selector.matches(b), false);
t(selector.matches(a));
t(!selector.matches(b));
});

test("#matches() checks if an element matches a :last-of-type selector", (t) => {
Expand All @@ -180,8 +180,8 @@ test("#matches() checks if an element matches a :last-of-type selector", (t) =>
<div />
</div>;

t.equal(selector.matches(a), false);
t.equal(selector.matches(b), true);
t(!selector.matches(a));
t(selector.matches(b));
});

test("#matches() checks if an element matches a :only-of-type selector", (t) => {
Expand All @@ -203,17 +203,17 @@ test("#matches() checks if an element matches a :only-of-type selector", (t) =>
<div />
</div>;

t.equal(selector.matches(a), true);
t.equal(selector.matches(b), false);
t(selector.matches(a));
t(!selector.matches(b));
});

test("#matches() checks if an element matches a :hover selector", (t) => {
const selector = parse(":hover");

const p = <p />;

t.equal(selector.matches(p), false);
t.equal(selector.matches(p, Context.hover(p)), true);
t(!selector.matches(p));
t(selector.matches(p, Context.hover(p)));
});

test("#matches() checks if an element matches a :hover selector when its descendant is hovered", (t) => {
Expand All @@ -222,25 +222,25 @@ test("#matches() checks if an element matches a :hover selector when its descend
const target = <span> Hello </span>;
const p = <div> {target} </div>;

t.equal(selector.matches(p, Context.hover(target)), true);
t(selector.matches(p, Context.hover(target)));
});

test("#matches() checks if an element matches an :active selector", (t) => {
const selector = parse(":active");

const p = <p />;

t.equal(selector.matches(p), false);
t.equal(selector.matches(p, Context.active(p)), true);
t(!selector.matches(p));
t(selector.matches(p, Context.active(p)));
});

test("#matches() checks if an element matches a :focus selector", (t) => {
const selector = parse(":focus");

const p = <p />;

t.equal(selector.matches(p), false);
t.equal(selector.matches(p, Context.focus(p)), true);
t(!selector.matches(p));
t(selector.matches(p, Context.focus(p)));
});

test("#matches() checks if an element matches a :focus-within selector", (t) => {
Expand All @@ -249,9 +249,9 @@ test("#matches() checks if an element matches a :focus-within selector", (t) =>
const button = <button />;
const p = <p>{button}</p>;

t.equal(selector.matches(p), false);
t.equal(selector.matches(p, Context.focus(p)), true);
t.equal(selector.matches(p, Context.focus(button)), true);
t(!selector.matches(p));
t(selector.matches(p, Context.focus(p)));
t(selector.matches(p, Context.focus(button)));
});

test("#matches() checks if an element matches a :link selector", (t) => {
Expand Down Expand Up @@ -328,3 +328,24 @@ test("#matches() checks if an element matches a :any-link selector", (t) => {
t.equal(selector.matches(element), false, element.toString());
}
});

test("#matches() checks if an element matches a :checked selector", (t) => {
const selector = parse(":checked");

for (const element of [
<input type="checkbox" checked />,
<input type="radio" checked />,
<option selected />,
]) {
t.equal(selector.matches(element), true, element.toString());
}

for (const element of [
<input type="checkbox" />,
<input type="radio" />,
<input />,
<option />,
]) {
t.equal(selector.matches(element), false, element.toString());
}
});

0 comments on commit cb48337

Please sign in to comment.