Skip to content

Commit

Permalink
LibWeb: Implement <input type=checkbox switch> experimentally
Browse files Browse the repository at this point in the history
In conformance with the requirements of the spec PR at
whatwg/html#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.
  • Loading branch information
sideshowbarker committed Dec 12, 2024
1 parent 68164aa commit fd32c93
Show file tree
Hide file tree
Showing 16 changed files with 270 additions and 3 deletions.
37 changes: 37 additions & 0 deletions Libraries/LibWeb/CSS/Default.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 3 additions & 1 deletion Libraries/LibWeb/CSS/SelectorEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
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;
}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/HTML/AttributeNames.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions Libraries/LibWeb/HTML/AttributeNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
5 changes: 4 additions & 1 deletion Libraries/LibWeb/HTML/HTMLInputElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2343,13 +2343,16 @@ void HTMLInputElement::set_custom_validity(String const& error)

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
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;
}
Expand Down
2 changes: 2 additions & 0 deletions Libraries/LibWeb/HTML/HTMLInputElement.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('_');
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Harness status: OK

Found 2 tests

2 Pass
Pass switch IDL attribute, setter
Pass switch IDL attribute, getter
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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`);
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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");

0 comments on commit fd32c93

Please sign in to comment.