Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 61 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /html/semantics/sections/headingoffset-and-headingreset.html - WPT Dashboard Interop Dashboard
<!doctype html>
<meta charset="utf-8" />
<meta name="author" title="Keith Cirkel" href="mailto:wpt@keithcirkel.co.uk" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="resources/invoker-utils.js"></script>
<div headingoffset="1" title="container headingoffset=1">
<!-- h1s are now h2s and so on -->
<h1 data-expected-offset="2"><!-- Level 2, h1 + 1 = 2 --></h1>
<h2 data-expected-offset="3"><!-- Level 3, h2 + 1 = 3 --></h2>
<h3 data-expected-offset="4"><!-- Level 4, h3 + 1 = 4 --></h3>
<div headingoffset="2" title="container headingoffset=2">
<!-- h1s are now h4s -->
<h1 data-expected-offset="4"><!-- Level 4, h1 + 2 + 1 = 4 --></h1>
<h2 data-expected-offset="5"><!-- Level 5, h2 + 2 + 1 = 5 --></h2>
<div headingreset title="container headingreset">
<!-- h1s are now h1s -->
<h1 data-expected-offset="1"><!-- Level 1, h1 (headingreset)--></h1>
</div>
<dialog open title="container dialog">
<!-- non-modal dialogs do not headingreset, h1s are still h4s -->
<h1 data-expected-offset="4"><!-- Level 4, h1 + 2 + 1 = 4 --></h1>
<h1 data-expected-offset="1" headingreset>
<!-- Level 1, h1 (headingreset) -->
</h1>
</dialog>
</div>
</div>
<!-- Clamping -->
<div headingoffset="8" title="container headingoffset=8">
<!-- h1s are now h9s -->
<h1 data-expected-offset="9"><!-- Level 9, h1 + 8 --></h1>
<h2 data-expected-offset="9"><!-- Level 9, h2 + 8 (clamped) --></h2>
<h3 data-expected-offset="9"><!-- Level 9, h3 + 8 (clamped) --></h3>
<h4 data-expected-offset="9"><!-- Level 9, h4 + 8 (clamped) --></h4>
<h5 data-expected-offset="9"><!-- Level 9, h5 + 8 (clamped) --></h5>
<h6 data-expected-offset="9"><!-- Level 9, h6 + 8 (clamped) --></h6>
<div headingreset title="container headingreset">
<!-- h1s are now h1s -->
<h1 data-expected-offset="1"><!-- Level 1, h1 (headingreset)--></h1>
</div>
<dialog open title="container dialog">
<!-- non-modal dialogs do not headingreset, h1s are still h4s -->
<h1 data-expected-offset="9"><!-- Level 9, h1 + 8 --></h1>
</dialog>
</div>
<!-- Negative headingoffsets are clamped to `0` -->
<div headingoffset="-3" title="container headingoffset=-3">
<h1 data-expected-offset="1"><!-- Level 1, h1 + (-3 clamped to 0) --></h1>
<h2 data-expected-offset="2"><!-- Level 2, h2 + (-3 clamped to 0) --></h2>
<h3 data-expected-offset="3"><!-- Level 3, h3 + (-3 clamped to 0) --></h3>
<h4 data-expected-offset="4"><!-- Level 4, h4 + (-3 clamped to 0) --></h4>
<h5 data-expected-offset="5"><!-- Level 5, h5 + (-3 clamped to 0) --></h5>
<h6 data-expected-offset="6"><!-- Level 6, h6 + (-3 clamped to 0) --></h6>
<div headingreset title="container headingreset">
<!-- h1s are now h1s -->
<h1 data-expected-offset="1"><!-- Level 1, h1 (headingreset)--></h1>
</div>
<dialog open title="container dialog">
<!-- non-modal dialogs do not headingreset, h1s are still h1s -->
<h1 data-expected-offset="1"><!-- Level 1, h1 + (-3 clamped to 0) --></h1>
</dialog>
</div>
<!-- Resetting applies after headingOffset -->
<div headingreset title="container headingreset + headingoffset">
<div headingoffset="2" headingreset>
<div headingoffset="2">
<h1 data-expected-offset="5"><!-- Level 5, h1 + 2 + 2 = 5 --></h1>
<h2 data-expected-offset="6"><!-- Level 6, h2 + 2 + 2 = 6 --></h2>
<h3 data-expected-offset="7"><!-- Level 7, h3 + 2 + 2 = 7 --></h3>
<h4 data-expected-offset="8"><!-- Level 8, h4 + 2 + 2 = 8 --></h4>
<h5 data-expected-offset="9"><!-- Level 9, h5 + 2 + 2 = 9 --></h5>
<h6 data-expected-offset="9">
<!-- Level 9, h6 + 2 + 2 = (10 clamped to 9) -->
</h6>
<h1 data-expected-offset="3" headingoffset="2" headingreset>
<!-- Level 3, h1 + 2 + (headingreset) -->
</h1>
<h2 data-expected-offset="4" headingoffset="2" headingreset>
<!-- Level 4, h2 + 2 + (headingreset) -->
</h2>
</div>
</div>
</div>
<!-- Ensure shadow roots work -->
<div headingoffset="1" title="container shadowroot headingoffset=1">
<template shadowrootmode="open">
<h1 data-expected-offset="2"><!-- Level 2, h1 + 1 --></h1>
<h2 data-expected-offset="2" headingreset>
<!-- Level 2, h2 (headingreset) -->
</h2>
</template>
</div>
<!-- Ensure slotted elements are correctly set -->
<div headingoffset="1" title="container shadowroot slotted headingoffset=1">
<template shadowrootmode="open">
<h1 data-expected-offset="2"><!-- Level 2, h1 + 1 --></h1>
<h2 data-expected-offset="3"><!-- Level 3, h2 + 1 --></h2>
<h3 data-expected-offset="3" headingreset>
<!-- Level 3, h3 (headingreset) -->
</h3>
<slot></slot>
</template>
<h1><!-- Level 2, h1 + 1 --></h1>
</div>
<!-- Ensure slotted elements respect their parents -->
<div
headingoffset="1"
title="container shadowroot slotted with container headingoffset=1"
>
<template shadowrootmode="open">
<h1 data-expected-offset="2"><!-- Level 2, h1 + 1 --></h1>
<div headingoffset="1" title="container inside shadowroot headingoffset=1">
<slot></slot>
</div>
</template>
<h2 data-expected-offset="4"><!-- Level 4, h2 + 1 + 1 --></h2>
<h4 data-expected-offset="6"><!-- Level 6, h4 + 1 + 1 --></h4>
<h4 data-expected-offset="4" headingreset>
<!-- Level 4, h4 (headingreset) -->
</h4>
</div>
<!-- Ensure the slot can be decorated with headingoffset -->
<div
headingoffset="1"
title="container shadowroot slot with attr headingoffset=1"
>
<template shadowrootmode="open">
<h1 data-expected-offset="2"><!-- Level 2, h1 + 1 --></h1>
<slot headingoffset="1"></slot>
</template>
<h2 data-expected-offset="4"><!-- Level 4, h2 + 1 + 1 --></h2>
</div>
<h1 data-expected-offset="2" headingoffset="1"><!-- Level 2, h1 + 1 --></h1>
<h2 data-expected-offset="3" headingoffset="1"><!-- Level 3, h2 + 1 --></h2>
<h1 data-expected-offset="3" headingoffset="2"><!-- Level 3, h1 + 2--></h1>
<h2 data-expected-offset="4" headingoffset="2"><!-- Level 4, h2 + 2 --></h2>
<h1 data-expected-offset="2" headingoffset="1" headingreset>
<!-- Level 2, h1 + 1 (headingreset) -->
</h1>
<h2 data-expected-offset="3" headingoffset="1" headingreset>
<!-- Level 3, h2 + 1 (headingreset) -->
</h2>
<h1 data-expected-offset="3" headingoffset="2" headingreset>
<!-- Level 3, h1 + 2 (headingreset) -->
</h1>
<h2 data-expected-offset="4" headingoffset="2" headingreset>
<!-- Level 4, h2 + 2 (headingreset) -->
</h2>
<h1 data-expected-offset="9" headingoffset="20" headingreset>
<!-- Level 9, h1 + 20 (clamped) -->
</h1>
<h2 data-expected-offset="9" headingoffset="20" headingreset>
<!-- Level 9, h2 + 20 (clamped) -->
</h2>
<h1 data-expected-offset="1" headingoffset="0" headingreset>
<!-- Level 1, h1 + 0 -->
</h1>
<h2 data-expected-offset="2" headingoffset="0" headingreset>
<!-- Level 2, h2 + 0 -->
</h2>
<div title="container with no attr">
<h1 data-expected-offset="2" headingoffset="1"><!-- Level 2, h1 + 1 --></h1>
<h2 data-expected-offset="3" headingoffset="1"><!-- Level 3, h2 + 1 --></h2>
<h1 data-expected-offset="2" headingoffset="1" headingreset>
<!-- Level 2, h1 + 1 -->
</h1>
<h2 data-expected-offset="3" headingoffset="1" headingreset>
<!-- Level 3, h2 + 1 -->
</h2>
<h1 data-expected-offset="1" headingoffset="-1" headingreset>
<!-- Level 1, h1 + (-1 clamped to 0) -->
</h1>
<h2 data-expected-offset="2" headingoffset="-1" headingreset>
<!-- Level 2, h2 + (-1 clamped to 0) -->
</h2>
</div>
<div headingreset title="many nested + and - values">
<div headingoffset="-1">
<div headingoffset="3">
<div headingoffset="-6">
<div headingoffset="1">
<h1 data-expected-offset="5">
<!-- Level 5, h1 + 1 + (-6 clamped to 0) + 3 + (-1 clamped to 0) -->
</h1>
</div>
</div>
</div>
</div>
</div>
<h1 data-expected-offset="9" headingoffset="9" aria-level="3">
<!-- Level 3 -->
</h1>
<div headingoffset="9" id="modalParent">
<dialog id="modal">
<h1></h1>
</dialog>
</div>
<script>
const attribute = (el, val) => el.setAttribute("headingoffset", val);
const property = (el, val) => (el.headingOffset = val);
const levels = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const matchAllLevels = (el) =>
levels.map((l) => el.matches(`:heading(${l})`));
for (const fn of [attribute, property]) {
test(function () {
const el = document.createElement("h1");
assert_true(el.matches(":heading"), `h1 should match :heading`);
assert_true(el.matches(":heading(1)"), `h1 should match :heading(1)`);
assert_equals(
el.headingOffset,
0,
`h1 has an initial headingOffset of 0`,
);
fn(el, 3);
assert_equals(el.headingOffset, 3, `h1 now has a headingOffset of 3`);
assert_false(
el.matches(":heading(1)"),
`h1[headingoffset=3] should no longer match :heading(1)`,
);
assert_true(
el.matches(":heading(4)"),
`h1[headingoffset=3] should match :heading(4)`,
);
}, `headingoffset (set via ${fn.name}) should change the level a heading matches against`);
test(function () {
const parent = document.createElement("div");
const el = document.createElement("h1");
parent.append(el);
assert_true(el.matches(":heading"), `h1 should match :heading`);
assert_true(el.matches(":heading(1)"), `h1 should match :heading(1)`);
assert_equals(
parent.headingOffset,
0,
`parent has an initial headingOffset of 0`,
);
fn(parent, 3);
assert_equals(parent.headingOffset, 3, `h1 now has a headingOffset of 3`);
assert_false(
el.matches(":heading(1)"),
`h1[headingoffset=3] should no longer match :heading(1)`,
);
assert_true(
el.matches(":heading(4)"),
`div[headingoffset=3] h1 should match :heading(4)`,
);
const bools = matchAllLevels(el);
const expected_bools = levels.map((l) => l == 4);
assert_array_equals(
bools,
expected_bools,
`${el.outerHTML} should match only the expected heading level`,
);
assert_false(el.headingReset, "h1 has an initial headingReset=false");
el.headingReset = true;
assert_true(el.headingReset, "h1 now has a headingReset of true");
assert_false(
el.matches(":heading(4)"),
`div[headingoffset=3] h1[headingreset] should not match :heading(4)`,
);
assert_true(
el.matches(":heading(1)"),
`div[headingoffset=3] h1[headingreset] should match :heading(1)`,
);
const reset_bools = matchAllLevels(el);
const reset_expected_bools = levels.map((l) => l == 1);
assert_array_equals(
reset_bools,
reset_expected_bools,
`${el.outerHTML} should match only the expected heading level`,
);
}, `headingoffset (set via ${fn.name}) should change the level when a parent changes offset`);
}
test(function (t) {
const el = modal.querySelector("h1");
assert_false(modal.headingReset, `modal dialog has not set heading reset`);
modal.showModal();
t.add_cleanup(() => modal.close());
assert_true(modal.headingReset, `modal dialogs return headingReset true`);
assert_true(el.matches(":heading"), `h1 inside modal should match :heading`);
assert_true(el.matches(":heading(1)"), `h1 inside modal should match :heading(1)`);
const bools = matchAllLevels(el);
const expected_bools = levels.map((l) => l == 1);
assert_array_equals(
bools,
expected_bools,
`${el.outerHTML} should match only the expected heading level`,
);
}, `headingoffset should not impact modals or explicit headingreset containers`);
let i = 0;
for (const el of document.querySelectorAll("[data-expected-offset]")) {
i += 1;
test(function () {
const expected = parseInt(el.getAttribute("data-expected-offset"));
assert_true(
expected > 0 && expected < 10,
"expected offset should be a level from 1 to 9",
);
assert_true(
el.matches(":heading"),
`${el.outerHTML} should match :heading`,
);
const bools = matchAllLevels(el);
const expected_bools = levels.map((l) => l == expected);
assert_true(
el.matches(`:heading(${expected})`),
`${el.outerHTML} should match :heading(${expected})`,
);
assert_array_equals(
bools,
expected_bools,
`${el.outerHTML} should match only the expected heading level`,
);
}, `case ${i}: heading level for ${el.outerHTML} should match based on expected document structure`);
}
</script>