mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-29 20:29:18 +00:00
LibWeb: Implement <input type=checkbox switch> experimentally
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.
This commit is contained in:
parent
eee90f4aa2
commit
583ca6af89
Notes:
github-actions[bot]
2024-12-13 11:32:27 +00:00
Author: https://github.com/sideshowbarker
Commit: 583ca6af89
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2887
Reviewed-by: https://github.com/AtkinsSJ ✅
Reviewed-by: https://github.com/shannonbooth
16 changed files with 268 additions and 3 deletions
|
@ -857,3 +857,38 @@ progress {
|
||||||
filter: invert(100%);
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://github.com/whatwg/html/pull/9546
|
||||||
|
*/
|
||||||
|
input[type=checkbox][switch] {
|
||||||
|
appearance: none;
|
||||||
|
height: 1em;
|
||||||
|
width: 1.8em;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 1em;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
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][switch]:checked {
|
||||||
|
background-color: AccentColor;
|
||||||
|
}
|
||||||
|
|
|
@ -201,7 +201,9 @@ static inline bool matches_indeterminate_pseudo_class(DOM::Element const& elemen
|
||||||
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
|
auto const& input_element = static_cast<HTML::HTMLInputElement const&>(element);
|
||||||
switch (input_element.type_state()) {
|
switch (input_element.type_state()) {
|
||||||
case HTML::HTMLInputElement::TypeAttributeState::Checkbox:
|
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:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ void initialize_strings()
|
||||||
for_ = "for"_fly_string;
|
for_ = "for"_fly_string;
|
||||||
default_ = "default"_fly_string;
|
default_ = "default"_fly_string;
|
||||||
char_ = "char"_fly_string;
|
char_ = "char"_fly_string;
|
||||||
|
switch_ = "switch"_fly_string;
|
||||||
|
|
||||||
// NOTE: Special cases for attributes with dashes in them.
|
// NOTE: Special cases for attributes with dashes in them.
|
||||||
accept_charset = "accept-charset"_fly_string;
|
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::reversed)
|
||||||
|| attribute.equals_ignoring_ascii_case(AttributeNames::seeking)
|
|| attribute.equals_ignoring_ascii_case(AttributeNames::seeking)
|
||||||
|| attribute.equals_ignoring_ascii_case(AttributeNames::selected)
|
|| 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::truespeed)
|
||||||
|| attribute.equals_ignoring_ascii_case(AttributeNames::willvalidate);
|
|| attribute.equals_ignoring_ascii_case(AttributeNames::willvalidate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,6 +278,7 @@ namespace AttributeNames {
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(step) \
|
__ENUMERATE_HTML_ATTRIBUTE(step) \
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(style) \
|
__ENUMERATE_HTML_ATTRIBUTE(style) \
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(summary) \
|
__ENUMERATE_HTML_ATTRIBUTE(summary) \
|
||||||
|
__ENUMERATE_HTML_ATTRIBUTE(switch_) \
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(tabindex) \
|
__ENUMERATE_HTML_ATTRIBUTE(tabindex) \
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(target) \
|
__ENUMERATE_HTML_ATTRIBUTE(target) \
|
||||||
__ENUMERATE_HTML_ATTRIBUTE(text) \
|
__ENUMERATE_HTML_ATTRIBUTE(text) \
|
||||||
|
|
|
@ -2343,13 +2343,16 @@ void HTMLInputElement::set_custom_validity(String const& error)
|
||||||
|
|
||||||
Optional<ARIA::Role> HTMLInputElement::default_role() const
|
Optional<ARIA::Role> HTMLInputElement::default_role() const
|
||||||
{
|
{
|
||||||
|
// http://wpt.live/html-aam/roles-dynamic-switch.tentative.window.html "Disconnected <input type=checkbox switch>"
|
||||||
|
if (!is_connected())
|
||||||
|
return {};
|
||||||
// https://www.w3.org/TR/html-aria/#el-input-button
|
// https://www.w3.org/TR/html-aria/#el-input-button
|
||||||
if (type_state() == TypeAttributeState::Button)
|
if (type_state() == TypeAttributeState::Button)
|
||||||
return ARIA::Role::button;
|
return ARIA::Role::button;
|
||||||
// https://www.w3.org/TR/html-aria/#el-input-checkbox
|
// https://www.w3.org/TR/html-aria/#el-input-checkbox
|
||||||
if (type_state() == TypeAttributeState::Checkbox) {
|
if (type_state() == TypeAttributeState::Checkbox) {
|
||||||
// https://github.com/w3c/html-aam/issues/496
|
// 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::switch_;
|
||||||
return ARIA::Role::checkbox;
|
return ARIA::Role::checkbox;
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ interface HTMLInputElement : HTMLElement {
|
||||||
[CEReactions] attribute unsigned long size;
|
[CEReactions] attribute unsigned long size;
|
||||||
[CEReactions, Reflect, URL] attribute USVString src;
|
[CEReactions, Reflect, URL] attribute USVString src;
|
||||||
[CEReactions, Reflect] attribute DOMString step;
|
[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] attribute DOMString type;
|
||||||
[CEReactions, Reflect=value] attribute DOMString defaultValue;
|
[CEReactions, Reflect=value] attribute DOMString defaultValue;
|
||||||
[CEReactions, LegacyNullToEmptyString] attribute DOMString value;
|
[CEReactions, LegacyNullToEmptyString] attribute DOMString value;
|
||||||
|
|
|
@ -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)
|
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;
|
StringBuilder builder;
|
||||||
builder.append(input);
|
builder.append(input);
|
||||||
builder.append('_');
|
builder.append('_');
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 6 tests
|
||||||
|
|
||||||
|
6 Pass
|
||||||
|
Pass Disconnected <input type=checkbox switch>
|
||||||
|
Pass Connected <input type=checkbox switch>
|
||||||
|
Pass Connected <input type=checkbox switch>: adding switch attribute
|
||||||
|
Pass Connected <input type=checkbox switch>: removing switch attribute
|
||||||
|
Pass Connected <input type=checkbox switch>: removing type attribute
|
||||||
|
Pass Connected <input type=checkbox switch>: adding type attribute
|
|
@ -0,0 +1,7 @@
|
||||||
|
Harness status: OK
|
||||||
|
|
||||||
|
Found 2 tests
|
||||||
|
|
||||||
|
2 Pass
|
||||||
|
Pass switch IDL attribute, setter
|
||||||
|
Pass switch IDL attribute, getter
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<script src="../resources/testdriver-actions.js"></script>
|
||||||
|
<div id=log></div>
|
||||||
|
<script src="../html-aam/roles-dynamic-switch.tentative.window.js"></script>
|
|
@ -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 <input type=checkbox switch>`);
|
||||||
|
|
||||||
|
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 <input type=checkbox switch>`);
|
||||||
|
|
||||||
|
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 <input type=checkbox switch>: 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 <input type=checkbox switch>: 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 <input type=checkbox switch>: 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 <input type=checkbox switch>: adding type attribute`);
|
|
@ -0,0 +1,8 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
|
||||||
|
<div id=log></div>
|
||||||
|
<script src="../../../../html/semantics/forms/the-input-element/input-type-checkbox-switch.tentative.window.js"></script>
|
|
@ -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 = "<input type=checkbox switch>";
|
||||||
|
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");
|
|
@ -0,0 +1,8 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset=utf-8>
|
||||||
|
|
||||||
|
<script src="../../../../resources/testharness.js"></script>
|
||||||
|
<script src="../../../../resources/testharnessreport.js"></script>
|
||||||
|
|
||||||
|
<div id=log></div>
|
||||||
|
<script src="../../../../html/semantics/selectors/pseudo-classes/input-checkbox-switch.tentative.window.js"></script>
|
|
@ -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");
|
Loading…
Add table
Add a link
Reference in a new issue