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