Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make descendents of fully clipped nodes invisible #848

Merged
merged 8 commits into from
Jul 1, 2021
Merged
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
1 change: 1 addition & 0 deletions packages/alfa-rules/src/common/predicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from "./predicate/has-border";
export * from "./predicate/has-box-shadow";
export * from "./predicate/has-cascaded-value-declared-in-inline-style";
export * from "./predicate/has-child";
export * from "./predicate/has-computed-style";
export * from "./predicate/has-descendant";
export * from "./predicate/has-explicit-role";
export * from "./predicate/has-heading-level";
Expand Down
23 changes: 23 additions & 0 deletions packages/alfa-rules/src/common/predicate/has-computed-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Device } from "@siteimprove/alfa-device";
import { Element, Text } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Context } from "@siteimprove/alfa-selector";
import { Property, Style } from "@siteimprove/alfa-style";

const { isElement } = Element;

export function hasComputedStyle<N extends Property.Name>(
name: N,
predicate: Predicate<Style.Computed<N>>,
device: Device,
context?: Context
): Predicate<Element | Text> {
return function hasComputedStyle(node): boolean {
return isElement(node)
? Style.from(node, device, context).computed(name).some(predicate)
: node
.parent({ flattened: true })
.filter(isElement)
.some(hasComputedStyle);
};
}
219 changes: 115 additions & 104 deletions packages/alfa-rules/src/common/predicate/is-clipped.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { Cache } from "@siteimprove/alfa-cache";
import { Device } from "@siteimprove/alfa-device";
import { Element, Text, Node } from "@siteimprove/alfa-dom";
import { Element, Node } from "@siteimprove/alfa-dom";
import { Predicate } from "@siteimprove/alfa-predicate";
import { Refinement } from "@siteimprove/alfa-refinement";
import { Context } from "@siteimprove/alfa-selector";
import { Style } from "@siteimprove/alfa-style";

const { abs } = Math;
const { isElement } = Element;
const { or, test } = Predicate;
const { isText } = Text;
const { and } = Refinement;

const cache = Cache.empty<Device, Cache<Context, Cache<Node, boolean>>>();

/**
* Checks if a node (or one of its ancestor) is fully clipped
*/
export function isClipped(
device: Device,
context: Context = Context.empty()
Expand All @@ -23,132 +27,139 @@ export function isClipped(
.get(node, () =>
test(
or(
isClippedBySize(device, context),
isClippedByMasking(device, context)
// Either it a clipped element
and(
isElement,
or(
isClippedBySize(device, context),
isClippedByIndent(device, context),
isClippedByMasking(device, context)
)
),
// Or its parent is clipped
(node: Node) =>
node
.parent({
flattened: true,
nested: true,
})
.some(isClipped(device, context))
),
node
)
);
}

/**
* Checks if an element's size is reduced to 0 or 1 pixel, and overflow is
* somehow hidden.
*/
function isClippedBySize(
device: Device,
context: Context = Context.empty()
): Predicate<Node> {
return function isClipped(node): boolean {
if (isElement(node)) {
const style = Style.from(node, device, context);

const { value: x } = style.computed("overflow-x");
const { value: y } = style.computed("overflow-y");

const { value: height } = style.computed("height");
const { value: width } = style.computed("width");

if (x.value === "hidden" || y.value === "hidden") {
for (const dimension of [height, width]) {
switch (dimension.type) {
case "percentage":
if (dimension.value <= 0) {
return true;
} else {
break;
}

case "length":
if (dimension.value <= 1) {
return true;
} else {
break;
}
}
}
}
): Predicate<Element> {
return function isClipped(element: Element): boolean {
const style = Style.from(element, device, context);

const {
value: { value: x },
} = style.computed("overflow-x");
const {
value: { value: y },
} = style.computed("overflow-y");

const { value: height } = style.computed("height");
const { value: width } = style.computed("width");

const hasNoScrollBar = x === "hidden" || y === "hidden";

if (x !== "visible" || y !== "visible") {
for (const dimension of [height, width]) {
switch (dimension.type) {
case "percentage":
if (dimension.value <= 0) {
return true;
} else {
break;
}

if (
x.value === "auto" ||
x.value === "scroll" ||
y.value === "auto" ||
y.value === "scroll"
) {
for (const dimension of [height, width]) {
switch (dimension.type) {
case "percentage":
if (dimension.value <= 0) {
return true;
} else {
break;
}

case "length":
if (dimension.value <= 0) {
return true;
} else {
break;
}
}
// Technically, 1×1 elements are (possibly) visible since they
// show one pixel of background. We assume this is used to hide
// elements and that the background is the same as the surrounding
// one.
case "length":
if (dimension.value <= (hasNoScrollBar ? 1 : 0)) {
return true;
} else {
break;
}
}
}
}

for (const parent of node.parent({ flattened: true })) {
if (isText(node) && isElement(parent)) {
const style = Style.from(parent, device, context);

const { value: x } = style.computed("overflow-x");

if (x.value === "hidden") {
const { value: indent } = style.computed("text-indent");
const { value: whitespace } = style.computed("white-space");
return false;
};
}

if (indent.value < 0 || whitespace.value === "nowrap") {
switch (indent.type) {
case "percentage":
if (abs(indent.value) >= 1) {
return true;
}
/**
* Checks if an element is fully indented out of its box.
*/
function isClippedByIndent(
device: Device,
context: Context
): Predicate<Element> {
return function isClipped(element: Element): boolean {
const style = Style.from(element, device, context);

const { value: x } = style.computed("overflow-x");

if (x.value === "hidden") {
const { value: indent } = style.computed("text-indent");
const { value: whitespace } = style.computed("white-space");

if (indent.value < 0 || whitespace.value === "nowrap") {
switch (indent.type) {
case "percentage":
if (abs(indent.value) >= 1) {
return true;
} else {
break;
}

case "length":
if (abs(indent.value) >= 999) {
return true;
}
case "length":
if (abs(indent.value) >= 999) {
return true;
} else {
break;
}
}
}
}

return isClipped(parent);
}

return false;
};
}

function isClippedByMasking(device: Device, context: Context): Predicate<Node> {
return function isClipped(node: Node): boolean {
if (isElement(node)) {
const style = Style.from(node, device, context);

const { value: clip } = style.computed("clip");
const { value: position } = style.computed("position");

if (
(position.value === "absolute" || position.value === "fixed") &&
clip.type === "shape" &&
((clip.shape.top.type === "length" &&
clip.shape.top.equals(clip.shape.bottom)) ||
(clip.shape.left.type === "length" &&
clip.shape.top.equals(clip.shape.right)))
) {
return true;
}
}

return node
.parent({
flattened: true,
nested: true,
})
.some(isClipped);
/**
* Checks if an element is fully masked by a clipping shape.
*/
function isClippedByMasking(
device: Device,
context: Context
): Predicate<Element> {
return function isClipped(element: Element): boolean {
const style = Style.from(element, device, context);

const { value: clip } = style.computed("clip");
const { value: position } = style.computed("position");

return (
(position.value === "absolute" || position.value === "fixed") &&
clip.type === "shape" &&
((clip.shape.top.type === "length" &&
clip.shape.top.equals(clip.shape.bottom)) ||
(clip.shape.left.type === "length" &&
clip.shape.top.equals(clip.shape.right)))
);
};
}
Loading