From fd32c9336260dfab28d3c5b8866ebf856e340599 Mon Sep 17 00:00:00 2001 From: sideshowbarker Date: Wed, 11 Dec 2024 11:34:41 +0900 Subject: [PATCH] LibWeb: Implement experimentally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In conformance with the requirements of the spec PR at https://github.com/whatwg/html/pull/9546, this change adds support for the “switch” attribute for type=checkbox “input” elements — which is shipping in Safari (since Safari 17.4). This change also implements support for exposing it to AT users with role=switch. --- Libraries/LibWeb/CSS/Default.css | 37 +++++++++ Libraries/LibWeb/CSS/SelectorEngine.cpp | 4 +- Libraries/LibWeb/HTML/AttributeNames.cpp | 2 + Libraries/LibWeb/HTML/AttributeNames.h | 1 + Libraries/LibWeb/HTML/HTMLInputElement.cpp | 5 +- Libraries/LibWeb/HTML/HTMLInputElement.idl | 2 + .../BindingsGenerator/IDLGenerators.cpp | 2 +- .../roles-dynamic-switch.tentative.window.txt | 11 +++ ...-type-checkbox-switch.tentative.window.txt | 7 ++ ...input-checkbox-switch.tentative.window.txt | 11 +++ ...roles-dynamic-switch.tentative.window.html | 10 +++ .../roles-dynamic-switch.tentative.window.js | 71 ++++++++++++++++++ ...type-checkbox-switch.tentative.window.html | 8 ++ ...t-type-checkbox-switch.tentative.window.js | 19 +++++ ...nput-checkbox-switch.tentative.window.html | 8 ++ .../input-checkbox-switch.tentative.window.js | 75 +++++++++++++++++++ 16 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html-aam/roles-dynamic-switch.tentative.window.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.txt create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.js diff --git a/Libraries/LibWeb/CSS/Default.css b/Libraries/LibWeb/CSS/Default.css index 256e641ff76bf..509a13f099cd0 100644 --- a/Libraries/LibWeb/CSS/Default.css +++ b/Libraries/LibWeb/CSS/Default.css @@ -857,3 +857,40 @@ progress { filter: invert(100%); } } + +/* https://github.com/whatwg/html/pull/9546 + */ +input[type=checkbox][switch] { + appearance: none; + height: 1em; + width: 1.8em; + border: 1px solid; + vertical-align: middle; + border-radius: 1em; + position: relative; + overflow: hidden; + font: inherit; + border-color: transparent; + background-color: ButtonFace; +} + +input[type=checkbox][switch]::before { + content: ''; + position: absolute; + height: 0; + width: 0; + border: .46em solid Field; + border-radius: 100%; + top: 0; + bottom: 0; + left: 0; + margin: auto; +} + +input[type=checkbox][switch]:checked::before { + left: calc(100% - .87em); +} + +input[type=checkbox]:checked { + background-color: AccentColor; +} diff --git a/Libraries/LibWeb/CSS/SelectorEngine.cpp b/Libraries/LibWeb/CSS/SelectorEngine.cpp index eef6bfc313f62..dbb6f89009c8c 100644 --- a/Libraries/LibWeb/CSS/SelectorEngine.cpp +++ b/Libraries/LibWeb/CSS/SelectorEngine.cpp @@ -201,7 +201,9 @@ static inline bool matches_indeterminate_pseudo_class(DOM::Element const& elemen auto const& input_element = static_cast(element); switch (input_element.type_state()) { case HTML::HTMLInputElement::TypeAttributeState::Checkbox: - return input_element.indeterminate(); + // https://whatpr.org/html-attr-input-switch/9546/semantics-other.html#selector-indeterminate + // input elements whose type attribute is in the Checkbox state, whose switch attribute is not set + return input_element.indeterminate() && !element.has_attribute(HTML::AttributeNames::switch_); default: return false; } diff --git a/Libraries/LibWeb/HTML/AttributeNames.cpp b/Libraries/LibWeb/HTML/AttributeNames.cpp index 2e73cb25113cc..5b0d4b0d06fcf 100644 --- a/Libraries/LibWeb/HTML/AttributeNames.cpp +++ b/Libraries/LibWeb/HTML/AttributeNames.cpp @@ -28,6 +28,7 @@ void initialize_strings() for_ = "for"_fly_string; default_ = "default"_fly_string; char_ = "char"_fly_string; + switch_ = "switch"_fly_string; // NOTE: Special cases for attributes with dashes in them. accept_charset = "accept-charset"_fly_string; @@ -81,6 +82,7 @@ bool is_boolean_attribute(FlyString const& attribute) || attribute.equals_ignoring_ascii_case(AttributeNames::reversed) || attribute.equals_ignoring_ascii_case(AttributeNames::seeking) || attribute.equals_ignoring_ascii_case(AttributeNames::selected) + || attribute.equals_ignoring_ascii_case(AttributeNames::switch_) || attribute.equals_ignoring_ascii_case(AttributeNames::truespeed) || attribute.equals_ignoring_ascii_case(AttributeNames::willvalidate); } diff --git a/Libraries/LibWeb/HTML/AttributeNames.h b/Libraries/LibWeb/HTML/AttributeNames.h index 439857945b1b0..949e0e2680fdb 100644 --- a/Libraries/LibWeb/HTML/AttributeNames.h +++ b/Libraries/LibWeb/HTML/AttributeNames.h @@ -276,6 +276,7 @@ namespace AttributeNames { __ENUMERATE_HTML_ATTRIBUTE(step) \ __ENUMERATE_HTML_ATTRIBUTE(style) \ __ENUMERATE_HTML_ATTRIBUTE(summary) \ + __ENUMERATE_HTML_ATTRIBUTE(switch_) \ __ENUMERATE_HTML_ATTRIBUTE(tabindex) \ __ENUMERATE_HTML_ATTRIBUTE(target) \ __ENUMERATE_HTML_ATTRIBUTE(text) \ diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index c8e9034664eb9..25d96221fa77a 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -2343,13 +2343,16 @@ void HTMLInputElement::set_custom_validity(String const& error) Optional HTMLInputElement::default_role() const { + // http://wpt.live/html-aam/roles-dynamic-switch.tentative.window.html "Disconnected " + if (!is_connected()) + return {}; // https://www.w3.org/TR/html-aria/#el-input-button if (type_state() == TypeAttributeState::Button) return ARIA::Role::button; // https://www.w3.org/TR/html-aria/#el-input-checkbox if (type_state() == TypeAttributeState::Checkbox) { // https://github.com/w3c/html-aam/issues/496 - if (has_attribute("switch"_string)) + if (has_attribute(HTML::AttributeNames::switch_)) return ARIA::Role::switch_; return ARIA::Role::checkbox; } diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.idl b/Libraries/LibWeb/HTML/HTMLInputElement.idl index c2824993ba71c..709d16c98d201 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.idl +++ b/Libraries/LibWeb/HTML/HTMLInputElement.idl @@ -38,6 +38,8 @@ interface HTMLInputElement : HTMLElement { [CEReactions] attribute unsigned long size; [CEReactions, Reflect, URL] attribute USVString src; [CEReactions, Reflect] attribute DOMString step; + // https://whatpr.org/html-attr-input-switch/9546/input.html#the-input-element:dom-input-switch + [CEReactions, Reflect] attribute boolean switch; [CEReactions] attribute DOMString type; [CEReactions, Reflect=value] attribute DOMString defaultValue; [CEReactions, LegacyNullToEmptyString] attribute DOMString value; diff --git a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp index 35dd36dcbbfda..aa174b698a7d3 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp @@ -288,7 +288,7 @@ CppType idl_type_name_to_cpp_type(Type const& type, Interface const& interface) static ByteString make_input_acceptable_cpp(ByteString const& input) { - if (input.is_one_of("class", "template", "for", "default", "char", "namespace", "delete", "inline", "register")) { + if (input.is_one_of("class", "template", "for", "default", "char", "namespace", "delete", "inline", "register", "switch")) { StringBuilder builder; builder.append(input); builder.append('_'); diff --git a/Tests/LibWeb/Text/expected/wpt-import/html-aam/roles-dynamic-switch.tentative.window.txt b/Tests/LibWeb/Text/expected/wpt-import/html-aam/roles-dynamic-switch.tentative.window.txt new file mode 100644 index 0000000000000..7325207a8fefd --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html-aam/roles-dynamic-switch.tentative.window.txt @@ -0,0 +1,11 @@ +Harness status: OK + +Found 6 tests + +6 Pass +Pass Disconnected +Pass Connected +Pass Connected : adding switch attribute +Pass Connected : removing switch attribute +Pass Connected : removing type attribute +Pass Connected : adding type attribute \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.txt new file mode 100644 index 0000000000000..3fd6388f6b9a1 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.txt @@ -0,0 +1,7 @@ +Harness status: OK + +Found 2 tests + +2 Pass +Pass switch IDL attribute, setter +Pass switch IDL attribute, getter \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.txt b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.txt new file mode 100644 index 0000000000000..6beb495271477 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.txt @@ -0,0 +1,11 @@ +Harness status: OK + +Found 6 tests + +6 Pass +Pass Switch control does not match :indeterminate +Pass Checkbox that is no longer a switch control does match :indeterminate +Pass Checkbox that becomes a switch control does not match :indeterminate +Pass Parent of a checkbox that becomes a switch control does not match :has(:indeterminate) +Pass Parent of a switch control that becomes a checkbox continues to match :has(:checked) +Pass A switch control that becomes a checkbox in a roundabout way \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.html b/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.html new file mode 100644 index 0000000000000..ff6dd4e2fa6be --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.html @@ -0,0 +1,10 @@ + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.js b/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.js new file mode 100644 index 0000000000000..2993c36764859 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html-aam/roles-dynamic-switch.tentative.window.js @@ -0,0 +1,71 @@ +// META: script=/resources/testdriver.js +// META: script=/resources/testdriver-vendor.js +// META: script=/resources/testdriver-actions.js + +promise_test(async () => { + const control = document.createElement("input"); + control.type = "checkbox"; + control.switch = true; + const role = await test_driver.get_computed_role(control); + assert_equals(role, ""); +}, `Disconnected `); + +promise_test(async t => { + const control = document.createElement("input"); + t.add_cleanup(() => control.remove()); + control.type = "checkbox"; + control.switch = true; + document.body.append(control); + const role = await test_driver.get_computed_role(control); + assert_equals(role, "switch"); +}, `Connected `); + +promise_test(async t => { + const control = document.createElement("input"); + t.add_cleanup(() => control.remove()); + control.type = "checkbox"; + document.body.append(control); + let role = await test_driver.get_computed_role(control); + assert_equals(role, "checkbox"); + control.switch = true; + role = await test_driver.get_computed_role(control); + assert_equals(role, "switch"); +}, `Connected : adding switch attribute`); + +promise_test(async t => { + const control = document.createElement("input"); + t.add_cleanup(() => control.remove()); + control.type = "checkbox"; + control.switch = true; + document.body.append(control); + let role = await test_driver.get_computed_role(control); + assert_equals(role, "switch"); + control.switch = false; + role = await test_driver.get_computed_role(control); + assert_equals(role, "checkbox"); +}, `Connected : removing switch attribute`); + +promise_test(async t => { + const control = document.createElement("input"); + t.add_cleanup(() => control.remove()); + control.type = "checkbox"; + document.body.append(control); + control.switch = true; + let role = await test_driver.get_computed_role(control); + assert_equals(role, "switch"); + control.removeAttribute("type"); + role = await test_driver.get_computed_role(control); + assert_equals(role, "textbox"); +}, `Connected : removing type attribute`); + +promise_test(async t => { + const control = document.createElement("input"); + t.add_cleanup(() => control.remove()); + control.switch = true; + document.body.append(control); + let role = await test_driver.get_computed_role(control); + assert_equals(role, "textbox"); + control.type = "checkbox"; + role = await test_driver.get_computed_role(control); + assert_equals(role, "switch"); +}, `Connected : adding type attribute`); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.html new file mode 100644 index 0000000000000..6cfe2938d2142 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.html @@ -0,0 +1,8 @@ + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.js b/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.js new file mode 100644 index 0000000000000..6128a62a0fb0d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.js @@ -0,0 +1,19 @@ +test(t => { + const input = document.createElement("input"); + input.switch = true; + + assert_true(input.hasAttribute("switch")); + assert_equals(input.getAttribute("switch"), ""); + assert_equals(input.type, "text"); +}, "switch IDL attribute, setter"); + +test(t => { + const container = document.createElement("div"); + container.innerHTML = ""; + const input = container.firstChild; + + assert_true(input.hasAttribute("switch")); + assert_equals(input.getAttribute("switch"), ""); + assert_equals(input.type, "checkbox"); + assert_true(input.switch); +}, "switch IDL attribute, getter"); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.html b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.html new file mode 100644 index 0000000000000..25f97e03540f4 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.html @@ -0,0 +1,8 @@ + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.js b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.js new file mode 100644 index 0000000000000..b5d9898a640e9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.js @@ -0,0 +1,75 @@ +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.switch = true; + input.indeterminate = true; + + assert_false(input.matches(":indeterminate")); +}, "Switch control does not match :indeterminate"); + +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.switch = true; + input.indeterminate = true; + + assert_false(input.matches(":indeterminate")); + + input.switch = false; + assert_true(input.matches(":indeterminate")); +}, "Checkbox that is no longer a switch control does match :indeterminate"); + +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.indeterminate = true; + + assert_true(input.matches(":indeterminate")); + + input.setAttribute("switch", "blah"); + assert_false(input.matches(":indeterminate")); +}, "Checkbox that becomes a switch control does not match :indeterminate"); + +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.indeterminate = true; + + assert_true(document.body.matches(":has(:indeterminate)")); + + input.switch = true; + assert_false(document.body.matches(":has(:indeterminate)")); +}, "Parent of a checkbox that becomes a switch control does not match :has(:indeterminate)"); + +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.switch = true + input.checked = true; + + assert_true(document.body.matches(":has(:checked)")); + + input.switch = false; + assert_true(document.body.matches(":has(:checked)")); + + input.checked = false; + assert_false(document.body.matches(":has(:checked)")); +}, "Parent of a switch control that becomes a checkbox continues to match :has(:checked)"); + +test(t => { + const input = document.body.appendChild(document.createElement("input")); + t.add_cleanup(() => input.remove()); + input.type = "checkbox"; + input.switch = true; + input.indeterminate = true; + assert_false(input.matches(":indeterminate")); + input.type = "text"; + input.removeAttribute("switch"); + input.type = "checkbox"; + assert_true(input.matches(":indeterminate")); +}, "A switch control that becomes a checkbox in a roundabout way");