Skip to content

Commit df6fd0d

Browse files
authored
Merge pull request #534 from jmcgavin/fix-toggle-attribute-polyfill
fix: toggleAttribute polyfill should retain force argument
2 parents 09b97de + 2d7391a commit df6fd0d

File tree

5 files changed

+90
-20
lines changed

5 files changed

+90
-20
lines changed

packages/scoped-custom-element-registry/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
<!-- ## Unreleased -->
8+
## Unreleased
9+
10+
### Fixed
11+
12+
- toggleAttribute polyfill now retains the force argument if it is present
913

1014
# [0.0.7] - 2023-01-06
1115

packages/scoped-custom-element-registry/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ upgrade it.
2222
Notes/limitations:
2323

2424
- In order to leverage native CE when available, `observedAttributes` handling
25-
must be simulated by patching `setAttribute`/`getAttribute` to call
25+
must be simulated by patching `setAttribute`/`getAttribute`/`toggleAttribute` to call
2626
`attributeChangedCallback` manually, since while we can delegate constructors,
2727
the `observedAttributes` respected by the browser are fixed at define time.
2828
This means that native reflecting properties are not observable when set via

packages/scoped-custom-element-registry/src/scoped-custom-element-registry.js

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ if (!ShadowRoot.prototype.createElement) {
5151
);
5252
}
5353
// Since observedAttributes can't change, we approximate it by patching
54-
// set/removeAttribute on the user's class
54+
// set/remove/toggleAttribute on the user's class
5555
const attributeChangedCallback =
5656
elementClass.prototype.attributeChangedCallback;
5757
const observedAttributes = new Set(elementClass.observedAttributes || []);
@@ -307,8 +307,8 @@ if (!ShadowRoot.prototype.createElement) {
307307
};
308308
};
309309

310-
// Helper to patch CE class setAttribute/getAttribute to implement
311-
// attributeChangedCallback
310+
// Helper to patch CE class setAttribute/getAttribute/toggleAttribute to
311+
// implement attributeChangedCallback
312312
const patchAttributes = (
313313
elementClass,
314314
observedAttributes,
@@ -348,15 +348,15 @@ if (!ShadowRoot.prototype.createElement) {
348348
}
349349
const toggleAttribute = elementClass.prototype.toggleAttribute;
350350
if (toggleAttribute) {
351-
elementClass.prototype.toggleAttribute = function (n) {
351+
elementClass.prototype.toggleAttribute = function (n, force) {
352352
const name = n.toLowerCase();
353353
if (observedAttributes.has(name)) {
354354
const old = this.getAttribute(name);
355-
toggleAttribute.call(this, name);
355+
toggleAttribute.call(this, name, force);
356356
const newValue = this.getAttribute(name);
357357
attributeChangedCallback.call(this, name, old, newValue);
358358
} else {
359-
toggleAttribute.call(this, name);
359+
toggleAttribute.call(this, name, force);
360360
}
361361
};
362362
}
@@ -387,17 +387,19 @@ if (!ShadowRoot.prototype.createElement) {
387387
patchHTMLElement(definition.elementClass);
388388
new definition.elementClass();
389389
}
390-
// Approximate observedAttributes from the user class, since the stand-in element had none
391-
definition.observedAttributes.forEach((attr) => {
392-
if (instance.hasAttribute(attr)) {
393-
definition.attributeChangedCallback.call(
394-
instance,
395-
attr,
396-
null,
397-
instance.getAttribute(attr)
398-
);
399-
}
400-
});
390+
if (definition.attributeChangedCallback) {
391+
// Approximate observedAttributes from the user class, since the stand-in element had none
392+
definition.observedAttributes.forEach((attr) => {
393+
if (instance.hasAttribute(attr)) {
394+
definition.attributeChangedCallback.call(
395+
instance,
396+
attr,
397+
null,
398+
instance.getAttribute(attr)
399+
);
400+
}
401+
});
402+
}
401403
if (isUpgrade && definition.connectedCallback && instance.isConnected) {
402404
definition.connectedCallback.call(instance);
403405
}

packages/scoped-custom-element-registry/test/Element.test.html.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import {expect} from '@open-wc/testing';
22

3-
import {getTestElement, getShadowRoot, getHTML} from './utils.js';
3+
import {
4+
getTestElement,
5+
getObservedAttributesTestElement,
6+
getShadowRoot,
7+
getHTML,
8+
} from './utils.js';
49

510
describe('Element', () => {
611
describe('global registry', () => {
@@ -104,4 +109,47 @@ describe('Element', () => {
104109
expect($el.firstElementChild).to.not.be.instanceof(CustomElementClass);
105110
});
106111
});
112+
113+
describe('attributes', () => {
114+
it('should call setAttribute', () => {
115+
const {tagName, CustomElementClass} = getObservedAttributesTestElement([
116+
'foo',
117+
]);
118+
customElements.define(tagName, CustomElementClass);
119+
const $el = document.createElement(tagName);
120+
121+
$el.setAttribute('foo', 'bar');
122+
123+
expect($el.getAttribute('foo')).to.equal('bar');
124+
});
125+
126+
it('should call removeAttribute', () => {
127+
const {tagName, CustomElementClass} = getObservedAttributesTestElement([
128+
'foo',
129+
]);
130+
customElements.define(tagName, CustomElementClass);
131+
const $el = getHTML(`<${tagName} foo></${tagName}>`);
132+
133+
$el.removeAttribute('foo');
134+
135+
expect($el.hasAttribute('foo')).to.be.false;
136+
});
137+
138+
it('should call toggleAttribute', () => {
139+
const {tagName, CustomElementClass} = getObservedAttributesTestElement([
140+
'foo',
141+
]);
142+
customElements.define(tagName, CustomElementClass);
143+
const $el = document.createElement(tagName);
144+
145+
$el.toggleAttribute('foo', false);
146+
147+
expect($el.hasAttribute('foo')).to.be.false;
148+
149+
$el.setAttribute('foo', '');
150+
$el.toggleAttribute('foo', true);
151+
152+
expect($el.hasAttribute('foo')).to.be.true;
153+
});
154+
});
107155
});

packages/scoped-custom-element-registry/test/utils.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ export const getTestElement = () => ({
3030
CustomElementClass: class extends HTMLElement {},
3131
});
3232

33+
/**
34+
*
35+
* @param {Array<string>} observedAttributeNames the names of the attributes you want to observe
36+
* @returns {{CustomElementClass: typeof HTMLElement, tagName: string}}
37+
*/
38+
export const getObservedAttributesTestElement = (
39+
observedAttributeNames = []
40+
) => ({
41+
tagName: getTestTagName(),
42+
CustomElementClass: class extends HTMLElement {
43+
static get observedAttributes() {
44+
return observedAttributeNames;
45+
}
46+
},
47+
});
48+
3349
export const getFormAssociatedTestElement = () => ({
3450
tagName: getTestTagName(),
3551
CustomElementClass: class extends HTMLElement {

0 commit comments

Comments
 (0)