Skip to content

Commit 048cb87

Browse files
kbabbittchromium-wpt-export-bot
authored andcommitted
Fire style attribute change event on change to attributeStyleMap
- Refactor StyleAttributeMutationScope to hold an Element as context rather than an AbstractPropertySetCSSStyleDeclaration. - Wrap mutations effected from InlineStylePropertyMap in a StyleAttributeMutationScope. - In InlineStylePropertyMap, call SetProperty() directly rather than delegating to Element::SetInlineStyleProperty() to avoid duplicate mutation records. Fixed: 40710238 Change-Id: I2287d68a21d5e126aa7c449260b2da8df9fa599d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7823966 Reviewed-by: Rune Lillesveen <futhark@chromium.org> Commit-Queue: Kevin Babbitt <kbabbitt@microsoft.com> Cr-Commit-Position: refs/heads/main@{#1628636}
1 parent 342ad4e commit 048cb87

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<!doctype html>
2+
<meta charset="utf-8">
3+
<title>StylePropertyMap mutations trigger attributeChangedCallback</title>
4+
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set">
5+
<link rel="help" href="https://html.spec.whatwg.org/multipage/custom-elements.html#concept-custom-element-definition-lifecycle-callbacks">
6+
<script src="/resources/testharness.js"></script>
7+
<script src="/resources/testharnessreport.js"></script>
8+
<body>
9+
<script>
10+
'use strict';
11+
12+
class TestElement extends HTMLElement {
13+
static observedAttributes = ['style'];
14+
constructor() {
15+
super();
16+
this.calls = [];
17+
}
18+
attributeChangedCallback(name, oldValue, newValue) {
19+
this.calls.push({ name, oldValue, newValue });
20+
}
21+
}
22+
customElements.define('test-el', TestElement);
23+
24+
function createTestElement(t) {
25+
const el = document.createElement('test-el');
26+
document.body.appendChild(el);
27+
t.add_cleanup(() => el.remove());
28+
return el;
29+
}
30+
31+
32+
function flushReactions() {
33+
return Promise.resolve();
34+
}
35+
36+
promise_test(async t => {
37+
const el = createTestElement(t);
38+
el.attributeStyleMap.set('display', new CSSKeywordValue('block'));
39+
await flushReactions();
40+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire once');
41+
assert_equals(el.calls[0].name, 'style');
42+
assert_equals(el.calls[0].oldValue, null);
43+
assert_equals(el.calls[0].newValue, 'display: block;');
44+
}, 'attributeStyleMap.set() triggers attributeChangedCallback');
45+
46+
promise_test(async t => {
47+
const el = createTestElement(t);
48+
el.attributeStyleMap.set('color', 'red');
49+
await flushReactions();
50+
assert_equals(el.calls.length, 1);
51+
el.calls.length = 0;
52+
el.attributeStyleMap.set('color', 'blue');
53+
await flushReactions();
54+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire for update');
55+
assert_equals(el.calls[0].name, 'style');
56+
assert_equals(el.calls[0].oldValue, 'color: red;');
57+
assert_equals(el.calls[0].newValue, 'color: blue;');
58+
}, 'attributeStyleMap.set() triggers attributeChangedCallback with correct old/new values');
59+
60+
promise_test(async t => {
61+
const el = createTestElement(t);
62+
el.attributeStyleMap.set('display', new CSSKeywordValue('block'));
63+
await flushReactions();
64+
el.calls.length = 0;
65+
el.attributeStyleMap.delete('display');
66+
await flushReactions();
67+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire on delete');
68+
assert_equals(el.calls[0].name, 'style');
69+
}, 'attributeStyleMap.delete() triggers attributeChangedCallback');
70+
71+
promise_test(async t => {
72+
const el = createTestElement(t);
73+
el.attributeStyleMap.set('display', new CSSKeywordValue('block'));
74+
el.attributeStyleMap.set('color', 'red');
75+
await flushReactions();
76+
el.calls.length = 0;
77+
el.attributeStyleMap.clear();
78+
await flushReactions();
79+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire on clear');
80+
assert_equals(el.calls[0].name, 'style');
81+
}, 'attributeStyleMap.clear() triggers attributeChangedCallback');
82+
83+
promise_test(async t => {
84+
const el = createTestElement(t);
85+
el.attributeStyleMap.set('transition-duration', '1s');
86+
await flushReactions();
87+
el.calls.length = 0;
88+
el.attributeStyleMap.append('transition-duration', '2s');
89+
await flushReactions();
90+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire on append');
91+
assert_equals(el.calls[0].name, 'style');
92+
}, 'attributeStyleMap.append() triggers attributeChangedCallback');
93+
94+
promise_test(async t => {
95+
const el = createTestElement(t);
96+
el.attributeStyleMap.set('--my-var', new CSSUnparsedValue(['hello']));
97+
await flushReactions();
98+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire for custom property');
99+
assert_equals(el.calls[0].name, 'style');
100+
assert_equals(el.calls[0].oldValue, null);
101+
}, 'attributeStyleMap.set() with custom property triggers attributeChangedCallback');
102+
103+
promise_test(async t => {
104+
const el = createTestElement(t);
105+
el.attributeStyleMap.set('--my-var', new CSSUnparsedValue(['hello']));
106+
await flushReactions();
107+
el.calls.length = 0;
108+
el.attributeStyleMap.delete('--my-var');
109+
await flushReactions();
110+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire for custom property delete');
111+
assert_equals(el.calls[0].name, 'style');
112+
}, 'attributeStyleMap.delete() with custom property triggers attributeChangedCallback');
113+
114+
promise_test(async t => {
115+
const el = createTestElement(t);
116+
el.attributeStyleMap.set('margin', '10px');
117+
await flushReactions();
118+
assert_equals(el.calls.length, 1, 'attributeChangedCallback should fire for shorthand');
119+
assert_equals(el.calls[0].name, 'style');
120+
assert_equals(el.calls[0].oldValue, null);
121+
}, 'attributeStyleMap.set() with shorthand property triggers attributeChangedCallback');
122+
123+
promise_test(async t => {
124+
const el = createTestElement(t);
125+
// el.style.setProperty has [CEReactions] so its callback fires synchronously.
126+
el.style.setProperty('display', 'none');
127+
assert_equals(el.calls.length, 1);
128+
assert_equals(el.calls[0].oldValue, null);
129+
assert_equals(el.calls[0].newValue, 'display: none;');
130+
el.calls.length = 0;
131+
// attributeStyleMap.set uses the backup reaction queue.
132+
el.attributeStyleMap.set('display', new CSSKeywordValue('block'));
133+
await flushReactions();
134+
assert_equals(el.calls.length, 1);
135+
assert_equals(el.calls[0].oldValue, 'display: none;');
136+
assert_equals(el.calls[0].newValue, 'display: block;');
137+
el.calls.length = 0;
138+
el.style.setProperty('display', 'none');
139+
assert_equals(el.calls.length, 1);
140+
assert_equals(el.calls[0].oldValue, 'display: block;');
141+
assert_equals(el.calls[0].newValue, 'display: none;');
142+
}, 'attributeStyleMap and style interleave correctly with attributeChangedCallback');
143+
144+
promise_test(async t => {
145+
const el = createTestElement(t);
146+
const observer = new MutationObserver(() => {});
147+
observer.observe(el, { attributes: true, attributeOldValue: true });
148+
t.add_cleanup(() => observer.disconnect());
149+
150+
el.attributeStyleMap.set('display', new CSSKeywordValue('block'));
151+
const records = observer.takeRecords();
152+
assert_equals(records.length, 1, 'MutationObserver should fire exactly once');
153+
assert_equals(records[0].attributeName, 'style');
154+
assert_equals(records[0].oldValue, null);
155+
}, 'attributeStyleMap.set() triggers MutationObserver without duplicates');
156+
</script>
157+
</body>
158+
</html>

0 commit comments

Comments
 (0)