From c0b6aef0279274b40c004d1ab1c2c2b238819aff Mon Sep 17 00:00:00 2001 From: Luke Warlow <luke@warlow.dev> Date: Fri, 29 Sep 2023 17:30:04 -0700 Subject: [PATCH] Add showPicker() method to HTMLSelectElement interface This CL adds an experimental method to the HTMLSelectElement interface to allow web developers to show the browser picker for select elements. It requires a user gesture and is disallowed for cross-origin iframes. Intent to prototype: https://groups.google.com/a/chromium.org/g/blink-dev/c/6MUAqY2r3Hg Demo: https://select-show-picker.glitch.me/ Spec: https://github.com/whatwg/html/pull/9754 Bug: 1485010 Change-Id: I822bff7b313dfa840b2a72edb66fcb4a2c7cb58d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4875550 Commit-Queue: Luke <lukewarlow156@gmail.com> Reviewed-by: Mason Freed <masonf@chromium.org> Cr-Commit-Position: refs/heads/main@{#1203564} --- .../resources/show-picker-child-iframe.html | 20 +++++ ...-picker-cross-origin-iframe.tentative.html | 79 +++++++++++++++++++ .../show-picker-disabled.tentative.html | 14 ++++ .../show-picker-multiple.tentative.html | 17 ++++ .../show-picker-size.tentative.html | 17 ++++ .../show-picker-user-gesture.tentative.html | 23 ++++++ 6 files changed, 170 insertions(+) create mode 100644 html/semantics/forms/the-select-element/resources/show-picker-child-iframe.html create mode 100644 html/semantics/forms/the-select-element/show-picker-cross-origin-iframe.tentative.html create mode 100644 html/semantics/forms/the-select-element/show-picker-disabled.tentative.html create mode 100644 html/semantics/forms/the-select-element/show-picker-multiple.tentative.html create mode 100644 html/semantics/forms/the-select-element/show-picker-size.tentative.html create mode 100644 html/semantics/forms/the-select-element/show-picker-user-gesture.tentative.html diff --git a/html/semantics/forms/the-select-element/resources/show-picker-child-iframe.html b/html/semantics/forms/the-select-element/resources/show-picker-child-iframe.html new file mode 100644 index 00000000000000..bba39898249d50 --- /dev/null +++ b/html/semantics/forms/the-select-element/resources/show-picker-child-iframe.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<title>Test showPicker() in an iframe</title> +<script type=module> + const urlParams = new URLSearchParams(location.search); + const documentDomain = urlParams.get('documentDomain'); + if (documentDomain) { + document.domain = documentDomain; + } + + let securityErrors = []; + const select = document.createElement("select"); + try { + select.showPicker(); + } catch (error) { + if (error instanceof DOMException && error.name == 'SecurityError') { + securityErrors.push("select"); + } + } + parent.postMessage(securityErrors.join(','), "*"); +</script> \ No newline at end of file diff --git a/html/semantics/forms/the-select-element/show-picker-cross-origin-iframe.tentative.html b/html/semantics/forms/the-select-element/show-picker-cross-origin-iframe.tentative.html new file mode 100644 index 00000000000000..3f710b39c60beb --- /dev/null +++ b/html/semantics/forms/the-select-element/show-picker-cross-origin-iframe.tentative.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<title>Test showPicker() called from cross-origin iframe</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<iframe id="iframe1"></iframe> +<iframe id="iframe2"></iframe> +<iframe id="iframe3"></iframe> +<iframe id="iframe4"></iframe> +</body> +<script> + function waitForSecurityErrors() { + return new Promise((resolve) => { + window.addEventListener("message", (event) => resolve(event.data), { + once: true, + }); + }); + } + + promise_test(async (t) => { + iframe1.src = + new URL("resources/", self.location).pathname + + "show-picker-child-iframe.html"; + + // Wait for the iframe to report security errors when calling showPicker(). + const securityErrors = await waitForSecurityErrors(); + assert_equals( + securityErrors, + "", + "In same-origin iframes, showPicker() does not throw a SecurityError." + ); + }); + + promise_test(async (t) => { + iframe2.src = + get_host_info().HTTP_NOTSAMESITE_ORIGIN + + new URL("resources/", self.location).pathname + + "show-picker-child-iframe.html"; + + // Wait for the iframe to report security errors when calling showPicker(). + const securityErrors = await waitForSecurityErrors(); + assert_equals( + securityErrors, + "select", + "In cross-origin iframes, showPicker() throws a SecurityError." + ); + }); + + promise_test(async (t) => { + iframe3.src = + new URL("resources/", self.location).pathname + + "show-picker-child-iframe.html?documentDomain=" + get_host_info().ORIGINAL_HOST; + + // Wait for the iframe to report security errors when calling showPicker(). + const securityErrors = await waitForSecurityErrors(); + assert_equals( + securityErrors, + "", + "In same-origin but cross-origin-domain iframes, showPicker() does not throw a SecurityError." + ); + }); + + promise_test(async (t) => { + document.domain = get_host_info().ORIGINAL_HOST; + iframe4.src = + get_host_info().HTTP_REMOTE_ORIGIN + + new URL("resources/", self.location).pathname + + "show-picker-child-iframe.html?documentDomain=" + get_host_info().ORIGINAL_HOST; + + // Wait for the iframe to report security errors when calling showPicker(). + const securityErrors = await waitForSecurityErrors(); + assert_equals( + securityErrors, + "select", + "In cross-origin but same-origin-domain iframes, showPicker() throws a SecurityError." + ); + }); +</script> \ No newline at end of file diff --git a/html/semantics/forms/the-select-element/show-picker-disabled.tentative.html b/html/semantics/forms/the-select-element/show-picker-disabled.tentative.html new file mode 100644 index 00000000000000..f20bc2cf61b254 --- /dev/null +++ b/html/semantics/forms/the-select-element/show-picker-disabled.tentative.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<title>Test showPicker() disabled requirement</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> +<select id="select" disabled> + <option>Item 1</option> +</select> +<script> + test(() => { + assert_throws_dom('InvalidStateError', () => { select.showPicker(); }); + }, 'select showPicker() throws when disabled'); +</script> \ No newline at end of file diff --git a/html/semantics/forms/the-select-element/show-picker-multiple.tentative.html b/html/semantics/forms/the-select-element/show-picker-multiple.tentative.html new file mode 100644 index 00000000000000..c38e98ee4e95aa --- /dev/null +++ b/html/semantics/forms/the-select-element/show-picker-multiple.tentative.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Test showPicker() on multiple selects</title> +<meta name="timeout" content="long"> +<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> +<select id="select" multiple> + <option>Item 1</option> +</select> +<script> +promise_test(async t => { + await test_driver.bless('show picker'); + select.showPicker(); + select.blur(); +}, `select showPicker() does not throw when called on a <select multiple>`); +</script> diff --git a/html/semantics/forms/the-select-element/show-picker-size.tentative.html b/html/semantics/forms/the-select-element/show-picker-size.tentative.html new file mode 100644 index 00000000000000..b9b10c42a9126d --- /dev/null +++ b/html/semantics/forms/the-select-element/show-picker-size.tentative.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Test showPicker() on sized selects</title> +<meta name="timeout" content="long"> +<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> +<select id="select" size="4"> + <option>Item 1</option> +</select> +<script> +promise_test(async t => { + await test_driver.bless('show picker'); + select.showPicker(); + select.blur(); +}, `select showPicker() does not throw when called on a <select size="4">`); +</script> diff --git a/html/semantics/forms/the-select-element/show-picker-user-gesture.tentative.html b/html/semantics/forms/the-select-element/show-picker-user-gesture.tentative.html new file mode 100644 index 00000000000000..24ccd72c8ae83d --- /dev/null +++ b/html/semantics/forms/the-select-element/show-picker-user-gesture.tentative.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Test showPicker() user gesture requirement</title> +<meta name="timeout" content="long"> +<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> +<body></body> +<script type=module> + test(() => { + const select = document.createElement("select"); + + assert_throws_dom('NotAllowedError', () => { select.showPicker(); }); + }, `select showPicker() requires a user gesture`); + + promise_test(async t => { + const select = document.createElement("select"); + + await test_driver.bless('show picker'); + select.showPicker(); + select.blur(); + }, `select showPicker() does not throw when user activation is active`); +</script> \ No newline at end of file