Commit 35aa05f7 authored by Kent Tamura's avatar Kent Tamura Committed by Commit Bot

std-switch: Fix no transition in a page with text selection

'transitionrun' event is delayed if a page has text selection, and the
transition detection logic in style.mjs didn't work in this case.

After this CL, we remove '$part-transitioning' part in
attributeChangedCallback. This works well because Blink doesn't compute
style for <std-switch> before calling attributeChangedCallback for
'on' attribute.

Change-Id: I8f25eca61fd52441d06bf86173c19d1c764bb859
Bug: 992454
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1758002Reviewed-by: default avatarFergal Daly <fergal@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#688427}
parent 6cf8e72a
...@@ -28,6 +28,8 @@ export class StdSwitchElement extends HTMLElement { ...@@ -28,6 +28,8 @@ export class StdSwitchElement extends HTMLElement {
return [STATE_ATTR]; return [STATE_ATTR];
} }
#inUserAction = false;
constructor() { constructor() {
super(); super();
if (new.target !== StdSwitchElement) { if (new.target !== StdSwitchElement) {
...@@ -47,6 +49,11 @@ export class StdSwitchElement extends HTMLElement { ...@@ -47,6 +49,11 @@ export class StdSwitchElement extends HTMLElement {
// TODO(tkent): We should not add aria-checked attribute. // TODO(tkent): We should not add aria-checked attribute.
// https://github.com/WICG/aom/issues/127 // https://github.com/WICG/aom/issues/127
this.setAttribute('aria-checked', newValue !== null ? 'true' : 'false'); this.setAttribute('aria-checked', newValue !== null ? 'true' : 'false');
if (!this.#inUserAction) {
for (const element of this[_containerElement].querySelectorAll('*')) {
style.unmarkTransition(element);
}
}
} }
} }
...@@ -91,7 +98,12 @@ export class StdSwitchElement extends HTMLElement { ...@@ -91,7 +98,12 @@ export class StdSwitchElement extends HTMLElement {
for (const element of this[_containerElement].querySelectorAll('*')) { for (const element of this[_containerElement].querySelectorAll('*')) {
style.markTransition(element); style.markTransition(element);
} }
this.on = !this.on; this.#inUserAction = true;
try {
this.on = !this.on;
} finally {
this.#inUserAction = false;
}
this.dispatchEvent(new Event('input', {bubbles: true})); this.dispatchEvent(new Event('input', {bubbles: true}));
this.dispatchEvent(new Event('change', {bubbles: true})); this.dispatchEvent(new Event('change', {bubbles: true}));
} }
......
...@@ -152,74 +152,33 @@ export function styleSheetFactory() { ...@@ -152,74 +152,33 @@ export function styleSheetFactory() {
} }
/** /**
* Add '$part-transitioning' part to the element if the element already has
* a part name.
*
* TODO(tkent): We should apply custom state.
*
* @param {!Element} element * @param {!Element} element
*/ */
function setupTransitionCounter(element) { export function markTransition(element) {
if (element.runningTransitions !== undefined) { // Should check hasAttribute() to avoid creating a DOMTokenList instance.
if (!element.hasAttribute('part') || element.part.length < 1) {
return; return;
} }
element.runningTransitions = 0; element.part.add(element.part[0] + '-transitioning');
element.addEventListener('transitionrun', e => {
if (e.target === element) {
++element.runningTransitions;
}
});
function handleEndOrCancel(e) {
// Need to check runningTransitions>0 due to superfluous transitioncancel
// events; crbug.com/979556.
if (e.target === element && element.runningTransitions > 0) {
--element.runningTransitions;
}
}
element.addEventListener('transitionend', handleEndOrCancel);
element.addEventListener('transitioncancel', handleEndOrCancel);
} }
/** /**
* Add '$part-transitioning' part to the element, and remove it on * Remove '$part-transitioning' part from the element if the element already has
* 'transitionend' event or remove it immediately if the element has no * a part name.
* transitions.
* *
* TODO(tkent): We should apply custom state. * TODO(tkent): We should apply custom state.
* *
* @param {!Element} element * @param {!Element} element
*/ */
export function markTransition(element) { export function unmarkTransition(element) {
// Should check hasAttribute() to avoid creating a DOMTokenList instance. // Should check hasAttribute() to avoid creating a DOMTokenList instance.
if (!element.hasAttribute('part') || element.part.length < 1) { if (!element.hasAttribute('part') || element.part.length < 1) {
return; return;
} }
element.part.remove(element.part[0] + '-transitioning');
setupTransitionCounter(element);
const partName = element.part[0] + '-transitioning';
if (element.part.contains(partName)) {
return;
}
// The style with partName might have transitions, and might have no
// transitions. We need to add partName anyway because we have no other
// ways to check existence of transitions. Then, we need to remove
// partName because state change without markTransition() should have
// style without partName.
//
// We add partName in an animation frame, and continue to request
// animation frames until runningTransitions becomes 0. If the style with
// partName has no transitions, runningTransitions keeps 0, and the second
// animation frame removes partName.
window.requestAnimationFrame(() => {
element.part.add(partName);
// If the element has a transition, it must start on the rendering just
// after this rAF callback. So we check runningTransitions in the next
// frame.
function removeIfNoTransitions() {
// No transitions started, or all transitions were completed.
if (element.runningTransitions === 0) {
element.part.remove(partName);
} else {
window.requestAnimationFrame(removeIfNoTransitions);
}
}
window.requestAnimationFrame(removeIfNoTransitions);
});
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
<script src="../resources/testdriver.js"></script> <script src="../resources/testdriver.js"></script>
<script src="../resources/testdriver-vendor.js"></script> <script src="../resources/testdriver-vendor.js"></script>
<body> <body>
<p>Some text</p>
<script type="module"> <script type="module">
import 'std:elements/switch'; import 'std:elements/switch';
...@@ -13,39 +14,71 @@ function singleFrame() { ...@@ -13,39 +14,71 @@ function singleFrame() {
}); });
} }
promise_test(async () => { function setupSwitchElement() {
let switchElement = document.createElement('std-switch'); let element = document.createElement('std-switch');
document.body.appendChild(switchElement); document.body.appendChild(element);
assert_false(switchElement.on);
let thumb = internals.shadowRoot(switchElement).getElementById('thumb'); let thumb = internals.shadowRoot(element).getElementById('thumb');
let transitionStartCounter = 0; element.transitionStartCounter = 0;
let transitionEndCounter = 0; element.transitionEndCounter = 0;
thumb.addEventListener('transitionstart', e => { ++transitionStartCounter; }); thumb.addEventListener('transitionstart', e => {
thumb.addEventListener('transitionend', e => { ++transitionEndCounter; }); ++element.transitionStartCounter;
});
await test_driver.click(switchElement); thumb.addEventListener('transitionend', e => {
assert_true(switchElement.on, 'click'); ++element.transitionEndCounter;
});
return element;
}
async function assertClickTransition(element) {
assert_false(element.on);
await test_driver.click(element);
assert_true(element.on, 'click');
await singleFrame(); await singleFrame();
assert_true(transitionStartCounter > 0, 'A transition starts'); assert_true(element.transitionStartCounter > 0, 'A transition starts');
if (transitionStartCounter > transitionEndCounter) { if (element.transitionStartCounter > element.transitionEndCounter) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
let thumb = internals.shadowRoot(element).getElementById('thumb');
thumb.addEventListener('transitionend', () => { thumb.addEventListener('transitionend', () => {
if (transitionStartCounter == transitionEndCounter) { if (element.transitionStartCounter == element.transitionEndCounter) {
resolve(); resolve();
} }
}); });
}); });
} }
assert_equals(thumb.runningTransitions, 0); }
// The track element doesn't animate by default.
let trackElement = internals.shadowRoot(switchElement).getElementById('track');
assert_equals(trackElement.runningTransitions, 0);
// Changing the state with an IDL attribute should not animate. // This should be called just after an operation which should not start
transitionStartCounter = 0; // transition. element.transitionStartCounter must be 0 before the operation.
switchElement.on = false; async function assertNoTransition(element) {
await singleFrame(); await singleFrame();
assert_equals(transitionStartCounter, 0, 'No additional events'); assert_equals(element.transitionStartCounter, 0, 'No transitionstart events');
}
promise_test(async () => {
let switchElement = setupSwitchElement();
await assertClickTransition(switchElement);
// Changing the state with an IDL attribute should not animate even after
// click transition.
switchElement.transitionStartCounter = 0;
switchElement.on = false;
await assertNoTransition(switchElement);
}, 'Click event handler should start transition'); }, 'Click event handler should start transition');
promise_test(async () => {
let switchElement = setupSwitchElement();
switchElement.on = !switchElement.on;
await assertNoTransition(switchElement);
}, 'Changing the state with an IDL attribute should not start transition');
promise_test(async () => {
// crbug.com/992454 Transition didn't start if there is a text selection.
const range = document.createRange();
range.selectNodeContents(document.querySelector('p'));
window.getSelection().addRange(range);
await assertClickTransition(setupSwitchElement());
}, 'Click event handler should start transition even with text selection');
</script> </script>
</body> </body>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment