Commit 8f0bd4da authored by Jack Steinberg's avatar Jack Steinberg Committed by Commit Bot

Create an attribute and property for toast closebutton and attach close functionality

This change sets the groundwork for the toast closebutton, by
adding an attribute to set the closebutton content,
creating a closeButton property to reflect that attribute,
and adding an event listener to close the toast when the closebutton is pressed.

This change implements the close button API described in
(https://github.com/jackbsteinberg/std-toast/blob/eec7728f7082a897d777181ac07b0448062ffca5/README.md),
and is the subject of debate on issue #48 on the explainer here
https://github.com/jackbsteinberg/std-toast/issues/48.

Future CLs will add the closeButton option for showToast
(https://github.com/jackbsteinberg/std-toast/tree/master#showtoastmessage-options)
Link: https://chromium-review.googlesource.com/c/chromium/src/+/1706453

Bug: 972945
Change-Id: I5d6c3055791a9f93ef414e575e128c61e2a944ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1699269Reviewed-by: default avatarFergal Daly <fergal@chromium.org>
Reviewed-by: default avatarKent Tamura <tkent@chromium.org>
Commit-Queue: Jack Steinberg <jacksteinberg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#678530}
parent 64a1040a
...@@ -45,21 +45,33 @@ function stylesheetFactory() { ...@@ -45,21 +45,33 @@ function stylesheetFactory() {
const generateStylesheet = stylesheetFactory(); const generateStylesheet = stylesheetFactory();
export class StdToastElement extends HTMLElement { export class StdToastElement extends HTMLElement {
static observedAttributes = ['open']; static observedAttributes = ['open', 'closebutton'];
#shadow = this.attachShadow({mode: 'closed'}); #shadow = this.attachShadow({mode: 'closed'});
#timeoutID; #timeoutID;
#actionSlot; #actionSlot;
#closeButtonElement;
constructor(message) { constructor(message) {
super(); super();
this.#shadow.adoptedStyleSheets = [generateStylesheet()]; this.#shadow.adoptedStyleSheets = [generateStylesheet()];
this.#shadow.innerHTML = `<slot></slot>`; this.#shadow.appendChild(document.createElement('slot'));
this.#actionSlot = document.createElement('slot'); this.#actionSlot = document.createElement('slot');
this.#actionSlot.setAttribute('name', 'action'); this.#actionSlot.setAttribute('name', 'action');
this.#shadow.appendChild(this.#actionSlot); this.#shadow.appendChild(this.#actionSlot);
this.#closeButtonElement = document.createElement('button');
this.#closeButtonElement.setAttribute('part', 'closebutton');
this.#closeButtonElement.setAttribute('aria-label', 'close');
this.#closeButtonElement.textContent = '×';
this.#shadow.appendChild(this.#closeButtonElement);
this.#closeButtonElement.addEventListener('click', () => {
this.hide();
});
if (message !== undefined) { if (message !== undefined) {
this.textContent = message; this.textContent = message;
} }
...@@ -94,6 +106,24 @@ export class StdToastElement extends HTMLElement { ...@@ -94,6 +106,24 @@ export class StdToastElement extends HTMLElement {
} }
} }
get closeButton() {
if (this.hasAttribute('closebutton')) {
const closeAttr = this.getAttribute('closebutton');
return closeAttr === '' ? true : closeAttr;
}
return false;
}
set closeButton(val) {
if (val === true) {
this.setAttribute('closebutton', '');
} else if (val === false) {
this.removeAttribute('closebutton');
} else {
this.setAttribute('closebutton', val);
}
}
show({duration = DEFAULT_DURATION} = {}) { show({duration = DEFAULT_DURATION} = {}) {
this.setAttribute('open', ''); this.setAttribute('open', '');
clearTimeout(this.#timeoutID); clearTimeout(this.#timeoutID);
...@@ -121,6 +151,18 @@ export class StdToastElement extends HTMLElement { ...@@ -121,6 +151,18 @@ export class StdToastElement extends HTMLElement {
this.#timeoutID = null; this.#timeoutID = null;
} }
break; break;
case 'closebutton':
if (newValue !== null) {
if (newValue === '') {
this.#closeButtonElement.textContent = '×';
this.#closeButtonElement.setAttribute('aria-label', 'close');
} else {
this.#closeButtonElement.textContent = newValue;
this.#closeButtonElement.removeAttribute('aria-label');
}
}
// if newValue === null we do nothing, since CSS will hide the button
break;
} }
} }
} }
......
...@@ -115,7 +115,7 @@ testToastElementAsync((t, toast) => { ...@@ -115,7 +115,7 @@ testToastElementAsync((t, toast) => {
}, 'toggling open attribute does not start timeout'); }, 'toggling open attribute does not start timeout');
testToastElement((toast) => { testToastElement((toast) => {
const permitted_properties = ['constructor', 'show', 'hide', 'toggle', 'open', 'action']; const permitted_properties = ['constructor', 'show', 'hide', 'toggle', 'open', 'action', 'closeButton'];
assert_array_equals(permitted_properties.sort(), Object.getOwnPropertyNames(toast.__proto__).sort()); assert_array_equals(permitted_properties.sort(), Object.getOwnPropertyNames(toast.__proto__).sort());
}, 'toast only exposes certain properties'); }, 'toast only exposes certain properties');
</script> </script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Toast: closebutton tests</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<main></main>
<script type="module">
import { testToastElement } from './resources/helpers.js';
testToastElement((toast) => {
toast.setAttribute('closebutton', '');
assert_true(toast.closeButton);
}, 'the closeButton property returns true with an empty attribute');
testToastElement((toast) => {
toast.setAttribute('closebutton', 'dismiss');
assert_equals(toast.closeButton, 'dismiss');
}, 'the closeButton property returns the set attribute value');
testToastElement((toast) => {
assert_false(toast.closeButton);
}, 'the closeButton property returns false with no attribute');
testToastElement((toast) => {
toast.setAttribute('closebutton', '');
assert_true(toast.closeButton);
toast.setAttribute('closebutton', 'dismiss');
assert_equals(toast.closeButton, 'dismiss');
toast.removeAttribute('closebutton');
assert_false(toast.closeButton);
}, 'the closeButton property changes when the attribute changes');
testToastElement((toast) => {
toast.closeButton = 'dismiss';
assert_equals(toast.getAttribute('closebutton'), 'dismiss');
}, 'setting the closeButton property to any string changes the attribute to that string');
testToastElement((toast) => {
toast.closeButton = '';
assert_equals(toast.getAttribute('closebutton'), '');
}, 'setting the closeButton property to empty string changes the attribute to empty string');
testToastElement((toast) => {
toast.closeButton = true;
assert_equals(toast.getAttribute('closebutton'), '');
}, 'setting the closeButton property to true changes the attribute to empty string');
testToastElement((toast) => {
toast.closeButton = false;
assert_false(toast.hasAttribute('closebutton'));
}, 'setting the closeButton property to false removes the attribute');
testToastElement((toast) => {
toast.closeButton = undefined;
assert_equals(toast.getAttribute('closebutton'), 'undefined');
}, 'setting the closeButton property to undefined stringifies and sets to that');
testToastElement((toast) => {
toast.closeButton = null;
assert_equals(toast.getAttribute('closebutton'), 'null');
}, 'setting the closeButton property to null stringifies and sets to that');
testToastElement((toast) => {
toast.closeButton = {};
assert_equals(toast.getAttribute('closebutton'), '[object Object]');
}, 'setting the closeButton property to {} stringifies and sets to [object Object]');
</script>
<!DOCTYPE html>
<meta charset="utf-8">
<title>Toast: close tests (with internals)</title>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../resources/testdriver.js"></script>
<script src="../resources/testdriver-vendor.js"></script>
<main></main>
<script type="module">
import 'std:elements/toast';
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', '');
document.querySelector('main').appendChild(toast);
toast.open = true;
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
toastCloseButton.click();
assert_false(toast.open);
}, 'clicking the toast close button closes the toast');
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', 'dismiss');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_equals(toastCloseButton.textContent, 'dismiss');
}, 'the closebutton attribute text shows up as the text content of the button');
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', '<b>bold</b> text');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_equals(toastCloseButton.textContent, '<b>bold</b> text');
assert_equals(toastCloseButton.querySelector('b'), null);
}, 'passing markup as the closebutton attribute text shows up as pure text content of the button');
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', '');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_equals(toastCloseButton.textContent, '×');
}, 'the × symbol shows up in the button when the closebutton attribute is present as empty string');
test(() => {
const toast = document.createElement('std-toast');
toast.closeButton = true;
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_equals(toastCloseButton.textContent, '×');
}, 'the × symbol shows up in the button when toast.closeButton is set to true');
test(() => {
const toast = document.createElement('std-toast');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_equals(toastCloseButton.getAttribute('aria-label'), 'close');
}, 'the closebutton has aria-label="close" by default');
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', '');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_true(toastCloseButton.hasAttribute('aria-label'));
assert_equals(toastCloseButton.getAttribute('aria-label'), 'close');
}, 'the default closebutton attribute shows up with an aria-label="close" attribute');
test(() => {
const toast = document.createElement('std-toast');
toast.setAttribute('closebutton', 'dismiss');
const toastCloseButton = internals.shadowRoot(toast).querySelector('[part=closebutton]');
assert_false(toastCloseButton.hasAttribute('aria-label'));
}, 'a closebutton attribute with a value shows up without an aria-label attribute');
</script>
\ No newline at end of file
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