From d5d1b5351e1ac5a0061c7df7d664f30194c373ad Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 14 Nov 2025 14:25:37 +0000 Subject: [PATCH] LibWeb: Hide stepper buttons on `appearance: textfield` number inputs The spec says: > For the purpose of this specification, they all have the same effect as auto. However, the host language may also take these values into account when defining the native appearance of the element. https://drafts.csswg.org/css-ui/#typedef-appearance-compat-special Firefox at least hides the stepper buttons when this is set. --- Libraries/LibWeb/CSS/ComputedProperties.cpp | 6 +- Libraries/LibWeb/HTML/HTMLInputElement.cpp | 77 +++++++++++---- Libraries/LibWeb/HTML/HTMLInputElement.h | 3 + .../expected/number-input-appearance.txt | 96 +++++++++++++++++++ .../Layout/input/number-input-appearance.html | 4 + 5 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/number-input-appearance.txt create mode 100644 Tests/LibWeb/Layout/input/number-input-appearance.html diff --git a/Libraries/LibWeb/CSS/ComputedProperties.cpp b/Libraries/LibWeb/CSS/ComputedProperties.cpp index 8cb35327fce..bcb1205aea9 100644 --- a/Libraries/LibWeb/CSS/ComputedProperties.cpp +++ b/Libraries/LibWeb/CSS/ComputedProperties.cpp @@ -833,8 +833,6 @@ Appearance ComputedProperties::appearance() const auto appearance = keyword_to_appearance(value.to_keyword()).release_value(); switch (appearance) { // Note: All these compatibility values can be treated as 'auto' - case Appearance::Textfield: - case Appearance::MenulistButton: case Appearance::Searchfield: case Appearance::Textarea: case Appearance::PushButton: @@ -849,6 +847,10 @@ Appearance ComputedProperties::appearance() const case Appearance::Button: appearance = Appearance::Auto; break; + // NB: values behave like auto but can also have an effect. Preserve them. + case Appearance::Textfield: + case Appearance::MenulistButton: + break; default: break; } diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.cpp b/Libraries/LibWeb/HTML/HTMLInputElement.cpp index ad459796c11..ca083de0542 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLInputElement.cpp @@ -86,6 +86,8 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor) visitor.visit(m_text_node); visitor.visit(m_placeholder_element); visitor.visit(m_placeholder_text_node); + visitor.visit(m_up_button_element); + visitor.visit(m_down_button_element); visitor.visit(m_color_well_element); visitor.visit(m_file_button); visitor.visit(m_file_label); @@ -793,6 +795,31 @@ static GC::Ref inner_text_style_when_hidden() return *style; } +static GC::Ref stepper_button_style_when_visible() +{ + static GC::Root style; + if (!style) { + style = CSS::CSSStyleProperties::create(internal_css_realm(), {}, {}); + style->set_declarations_from_text(R"~~~( + padding: 0; + cursor: default; + )~~~"sv); + } + return *style; +} + +static GC::Ref stepper_button_style_when_hidden() +{ + static GC::Root style; + if (!style) { + style = CSS::CSSStyleProperties::create(internal_css_realm(), {}, {}); + style->set_declarations_from_text(R"~~~( + display: none; + )~~~"sv); + } + return *style; +} + static GC::Ref placeholder_style_when_visible() { static GC::Root style; @@ -871,6 +898,17 @@ void HTMLInputElement::update_text_input_shadow_tree() m_text_node->set_data(m_value); update_placeholder_visibility(); } + + if (m_type == TypeAttributeState::Number) { + // The `textfield` appearance is used to hide the stepper buttons. + if (auto style = computed_properties(); style && style->appearance() == CSS::Appearance::Textfield) { + m_up_button_element->set_inline_style(stepper_button_style_when_hidden()); + m_down_button_element->set_inline_style(stepper_button_style_when_hidden()); + } else { + m_up_button_element->set_inline_style(stepper_button_style_when_visible()); + m_down_button_element->set_inline_style(stepper_button_style_when_visible()); + } + } } // https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-readonly-3 @@ -1093,29 +1131,22 @@ void HTMLInputElement::create_text_input_shadow_tree() m_placeholder_text_node = realm().create(document(), Utf16String::from_utf8(placeholder())); MUST(m_placeholder_element->append_child(*m_placeholder_text_node)); - update_placeholder_visibility(); - if (type_state() == TypeAttributeState::Number) { // Up button - auto up_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); - // FIXME: This cursor property doesn't work - up_button->set_attribute_value(HTML::AttributeNames::style, R"~~~( - padding: 0; - cursor: default; - )~~~"_string); + m_up_button_element = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); auto up_button_svg = MUST(DOM::create_element(document(), SVG::TagNames::svg, Namespace::SVG)); up_button_svg->set_attribute_value(HTML::AttributeNames::style, "width: 1em; height: 1em;"_string); up_button_svg->set_attribute_value(SVG::AttributeNames::xmlns, Namespace::SVG.to_string()); up_button_svg->set_attribute_value(SVG::AttributeNames::viewBox, "0 0 24 24"_string); - MUST(up_button->append_child(up_button_svg)); + MUST(m_up_button_element->append_child(up_button_svg)); auto up_button_svg_path = MUST(DOM::create_element(document(), SVG::TagNames::path, Namespace::SVG)); up_button_svg_path->set_attribute_value(SVG::AttributeNames::fill, "currentColor"_string); up_button_svg_path->set_attribute_value(SVG::AttributeNames::d, "M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z"_string); MUST(up_button_svg->append_child(up_button_svg_path)); - MUST(element->append_child(up_button)); + MUST(element->append_child(*m_up_button_element)); auto mouseup_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { @@ -1137,28 +1168,24 @@ void HTMLInputElement::create_text_input_shadow_tree() }, 0, Utf16FlyString {}, &realm()); auto step_up_callback = realm().heap().allocate(*up_callback_function, realm()); - up_button->add_event_listener_without_options(UIEvents::EventNames::mousedown, DOM::IDLEventListener::create(realm(), step_up_callback)); - up_button->add_event_listener_without_options(UIEvents::EventNames::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback)); + m_up_button_element->add_event_listener_without_options(UIEvents::EventNames::mousedown, DOM::IDLEventListener::create(realm(), step_up_callback)); + m_up_button_element->add_event_listener_without_options(UIEvents::EventNames::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback)); // Down button - auto down_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); - down_button->set_attribute_value(HTML::AttributeNames::style, R"~~~( - padding: 0; - cursor: default; - )~~~"_string); + m_down_button_element = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); auto down_button_svg = MUST(DOM::create_element(document(), SVG::TagNames::svg, Namespace::SVG)); down_button_svg->set_attribute_value(HTML::AttributeNames::style, "width: 1em; height: 1em;"_string); down_button_svg->set_attribute_value(SVG::AttributeNames::xmlns, Namespace::SVG.to_string()); down_button_svg->set_attribute_value(SVG::AttributeNames::viewBox, "0 0 24 24"_string); - MUST(down_button->append_child(down_button_svg)); + MUST(m_down_button_element->append_child(down_button_svg)); auto down_button_svg_path = MUST(DOM::create_element(document(), SVG::TagNames::path, Namespace::SVG)); down_button_svg_path->set_attribute_value(SVG::AttributeNames::fill, "currentColor"_string); down_button_svg_path->set_attribute_value(SVG::AttributeNames::d, "M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"_string); MUST(down_button_svg->append_child(down_button_svg_path)); - MUST(element->append_child(down_button)); + MUST(element->append_child(*m_down_button_element)); auto down_callback_function = JS::NativeFunction::create( realm(), [this](JS::VM&) { @@ -1170,9 +1197,11 @@ void HTMLInputElement::create_text_input_shadow_tree() }, 0, Utf16FlyString {}, &realm()); auto step_down_callback = realm().heap().allocate(*down_callback_function, realm()); - down_button->add_event_listener_without_options(UIEvents::EventNames::mousedown, DOM::IDLEventListener::create(realm(), step_down_callback)); - down_button->add_event_listener_without_options(UIEvents::EventNames::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback)); + m_down_button_element->add_event_listener_without_options(UIEvents::EventNames::mousedown, DOM::IDLEventListener::create(realm(), step_down_callback)); + m_down_button_element->add_event_listener_without_options(UIEvents::EventNames::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback)); } + + update_text_input_shadow_tree(); } void HTMLInputElement::create_color_input_shadow_tree() @@ -1531,6 +1560,12 @@ void HTMLInputElement::type_attribute_changed(TypeAttributeState old_state, Type } } +void HTMLInputElement::computed_properties_changed() +{ + create_shadow_tree_if_needed(); + update_shadow_tree(); +} + // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio):signal-a-type-change void HTMLInputElement::signal_a_type_change() { diff --git a/Libraries/LibWeb/HTML/HTMLInputElement.h b/Libraries/LibWeb/HTML/HTMLInputElement.h index 88bc61bb415..c2f8c899a0d 100644 --- a/Libraries/LibWeb/HTML/HTMLInputElement.h +++ b/Libraries/LibWeb/HTML/HTMLInputElement.h @@ -257,6 +257,7 @@ private: HTMLInputElement(DOM::Document&, DOM::QualifiedName); void type_attribute_changed(TypeAttributeState old_state, TypeAttributeState new_state); + virtual void computed_properties_changed() override; virtual bool is_presentational_hint(FlyString const&) const override; virtual void apply_presentational_hints(GC::Ref) const override; @@ -349,6 +350,8 @@ private: GC::Ptr m_inner_text_element; GC::Ptr m_text_node; bool m_checked { false }; + GC::Ptr m_up_button_element; + GC::Ptr m_down_button_element; void update_color_well_element(); GC::Ptr m_color_well_element; diff --git a/Tests/LibWeb/Layout/expected/number-input-appearance.txt b/Tests/LibWeb/Layout/expected/number-input-appearance.txt new file mode 100644 index 00000000000..ac304ad860b --- /dev/null +++ b/Tests/LibWeb/Layout/expected/number-input-appearance.txt @@ -0,0 +1,96 @@ +Viewport <#document> at [0,0] [0+0+0 800 0+0+0] [0+0+0 600 0+0+0] children: not-inline + BlockContainer at [0,0] [0+0+0 800 0+0+0] [0+0+0 40 0+0+0] [BFC] children: not-inline + BlockContainer at [8,8] [8+0+0 784 0+0+8] [8+0+0 24 0+0+8] children: inline + frag 0 from BlockContainer start: 0, length: 0, rect: [9,9 200x22] baseline: 17 + frag 1 from TextNode start: 0, length: 1, rect: [210,11 8x18] baseline: 13.796875 + " " + frag 2 from BlockContainer start: 0, length: 0, rect: [219,9 200x22] baseline: 17 + frag 3 from TextNode start: 0, length: 1, rect: [420,11 8x18] baseline: 13.796875 + " " + frag 4 from BlockContainer start: 0, length: 0, rect: [429,11 200x20] baseline: 14.796875 + BlockContainer at [9,9] inline-block [0+1+0 200 0+1+0] [0+1+0 22 0+1+0] [BFC] children: not-inline + Box
at [11,10] flex-container(row) [0+0+2 196 2+0+0] [0+0+1 20 1+0+0] [FFC] children: not-inline + BlockContainer
at [11,11] flex-item [0+0+0 160 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline + frag 0 from TextNode start: 0, length: 1, rect: [11,11 6.34375x18] baseline: 13.796875 + "1" + TextNode <#text> (not painted) + BlockContainer