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.
This commit is contained in:
Sam Atkins 2025-11-14 14:25:37 +00:00
parent c3d23a2487
commit d5d1b5351e
Notes: github-actions[bot] 2025-12-01 11:11:27 +00:00
5 changed files with 163 additions and 23 deletions

View file

@ -833,8 +833,6 @@ Appearance ComputedProperties::appearance() const
auto appearance = keyword_to_appearance(value.to_keyword()).release_value(); auto appearance = keyword_to_appearance(value.to_keyword()).release_value();
switch (appearance) { switch (appearance) {
// Note: All these compatibility values can be treated as 'auto' // Note: All these compatibility values can be treated as 'auto'
case Appearance::Textfield:
case Appearance::MenulistButton:
case Appearance::Searchfield: case Appearance::Searchfield:
case Appearance::Textarea: case Appearance::Textarea:
case Appearance::PushButton: case Appearance::PushButton:
@ -849,6 +847,10 @@ Appearance ComputedProperties::appearance() const
case Appearance::Button: case Appearance::Button:
appearance = Appearance::Auto; appearance = Appearance::Auto;
break; break;
// NB: <compat-special> values behave like auto but can also have an effect. Preserve them.
case Appearance::Textfield:
case Appearance::MenulistButton:
break;
default: default:
break; break;
} }

View file

@ -86,6 +86,8 @@ void HTMLInputElement::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_text_node); visitor.visit(m_text_node);
visitor.visit(m_placeholder_element); visitor.visit(m_placeholder_element);
visitor.visit(m_placeholder_text_node); 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_color_well_element);
visitor.visit(m_file_button); visitor.visit(m_file_button);
visitor.visit(m_file_label); visitor.visit(m_file_label);
@ -793,6 +795,31 @@ static GC::Ref<CSS::CSSStyleProperties> inner_text_style_when_hidden()
return *style; return *style;
} }
static GC::Ref<CSS::CSSStyleProperties> stepper_button_style_when_visible()
{
static GC::Root<CSS::CSSStyleProperties> 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<CSS::CSSStyleProperties> stepper_button_style_when_hidden()
{
static GC::Root<CSS::CSSStyleProperties> style;
if (!style) {
style = CSS::CSSStyleProperties::create(internal_css_realm(), {}, {});
style->set_declarations_from_text(R"~~~(
display: none;
)~~~"sv);
}
return *style;
}
static GC::Ref<CSS::CSSStyleProperties> placeholder_style_when_visible() static GC::Ref<CSS::CSSStyleProperties> placeholder_style_when_visible()
{ {
static GC::Root<CSS::CSSStyleProperties> style; static GC::Root<CSS::CSSStyleProperties> style;
@ -871,6 +898,17 @@ void HTMLInputElement::update_text_input_shadow_tree()
m_text_node->set_data(m_value); m_text_node->set_data(m_value);
update_placeholder_visibility(); 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 // 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<DOM::Text>(document(), Utf16String::from_utf8(placeholder())); m_placeholder_text_node = realm().create<DOM::Text>(document(), Utf16String::from_utf8(placeholder()));
MUST(m_placeholder_element->append_child(*m_placeholder_text_node)); MUST(m_placeholder_element->append_child(*m_placeholder_text_node));
update_placeholder_visibility();
if (type_state() == TypeAttributeState::Number) { if (type_state() == TypeAttributeState::Number) {
// Up button // Up button
auto up_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); m_up_button_element = 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);
auto up_button_svg = MUST(DOM::create_element(document(), SVG::TagNames::svg, Namespace::SVG)); 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(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::xmlns, Namespace::SVG.to_string());
up_button_svg->set_attribute_value(SVG::AttributeNames::viewBox, "0 0 24 24"_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)); 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::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); 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(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( auto mouseup_callback_function = JS::NativeFunction::create(
realm(), [this](JS::VM&) { realm(), [this](JS::VM&) {
@ -1137,28 +1168,24 @@ void HTMLInputElement::create_text_input_shadow_tree()
}, },
0, Utf16FlyString {}, &realm()); 0, Utf16FlyString {}, &realm());
auto step_up_callback = realm().heap().allocate<WebIDL::CallbackType>(*up_callback_function, realm()); auto step_up_callback = realm().heap().allocate<WebIDL::CallbackType>(*up_callback_function, realm());
up_button->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::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::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback));
// Down button // Down button
auto down_button = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML)); m_down_button_element = MUST(DOM::create_element(document(), HTML::TagNames::button, Namespace::HTML));
down_button->set_attribute_value(HTML::AttributeNames::style, R"~~~(
padding: 0;
cursor: default;
)~~~"_string);
auto down_button_svg = MUST(DOM::create_element(document(), SVG::TagNames::svg, Namespace::SVG)); 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(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::xmlns, Namespace::SVG.to_string());
down_button_svg->set_attribute_value(SVG::AttributeNames::viewBox, "0 0 24 24"_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)); 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::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); 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(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( auto down_callback_function = JS::NativeFunction::create(
realm(), [this](JS::VM&) { realm(), [this](JS::VM&) {
@ -1170,9 +1197,11 @@ void HTMLInputElement::create_text_input_shadow_tree()
}, },
0, Utf16FlyString {}, &realm()); 0, Utf16FlyString {}, &realm());
auto step_down_callback = realm().heap().allocate<WebIDL::CallbackType>(*down_callback_function, realm()); auto step_down_callback = realm().heap().allocate<WebIDL::CallbackType>(*down_callback_function, realm());
down_button->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::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::mouseup, DOM::IDLEventListener::create(realm(), mouseup_callback));
} }
update_text_input_shadow_tree();
} }
void HTMLInputElement::create_color_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 // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio):signal-a-type-change
void HTMLInputElement::signal_a_type_change() void HTMLInputElement::signal_a_type_change()
{ {

View file

@ -257,6 +257,7 @@ private:
HTMLInputElement(DOM::Document&, DOM::QualifiedName); HTMLInputElement(DOM::Document&, DOM::QualifiedName);
void type_attribute_changed(TypeAttributeState old_state, TypeAttributeState new_state); 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 bool is_presentational_hint(FlyString const&) const override;
virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override; virtual void apply_presentational_hints(GC::Ref<CSS::CascadedProperties>) const override;
@ -349,6 +350,8 @@ private:
GC::Ptr<DOM::Element> m_inner_text_element; GC::Ptr<DOM::Element> m_inner_text_element;
GC::Ptr<DOM::Text> m_text_node; GC::Ptr<DOM::Text> m_text_node;
bool m_checked { false }; bool m_checked { false };
GC::Ptr<DOM::Element> m_up_button_element;
GC::Ptr<DOM::Element> m_down_button_element;
void update_color_well_element(); void update_color_well_element();
GC::Ptr<DOM::Element> m_color_well_element; GC::Ptr<DOM::Element> m_color_well_element;

View file

@ -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 <html> at [0,0] [0+0+0 800 0+0+0] [0+0+0 40 0+0+0] [BFC] children: not-inline
BlockContainer <body> 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 <input> at [9,9] inline-block [0+1+0 200 0+1+0] [0+1+0 22 0+1+0] [BFC] children: not-inline
Box <div> 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 <div> 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 <button> at [172,11] flex-item [0+1+0 16 0+1+0] [0+1+0 18 0+1+0] [BFC] children: not-inline
BlockContainer <(anonymous)> at [172,11] flex-container(column) [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [FFC] children: not-inline
BlockContainer <(anonymous)> at [172,11] flex-item [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [172,11 16x16] baseline: 16
SVGSVGBox <svg> at [172,11] [0+0+0 16 0+0+0] [0+0+0 16 0+0+0] [SVG] children: not-inline
SVGGeometryBox <path> at [176,16.328125] [0+0+0 8 0+0+0] [0+0+0 4.9375 0+0+0] children: not-inline
BlockContainer <button> at [190,11] flex-item [0+1+0 16 0+1+0] [0+1+0 18 0+1+0] [BFC] children: not-inline
BlockContainer <(anonymous)> at [190,11] flex-container(column) [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [FFC] children: not-inline
BlockContainer <(anonymous)> at [190,11] flex-item [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [190,11 16x16] baseline: 16
SVGSVGBox <svg> at [190,11] [0+0+0 16 0+0+0] [0+0+0 16 0+0+0] [SVG] children: not-inline
SVGGeometryBox <path> at [194,16.71875] [0+0+0 8 0+0+0] [0+0+0 4.953125 0+0+0] children: not-inline
TextNode <#text> (not painted)
BlockContainer <input> at [219,9] inline-block [0+1+0 200 0+1+0] [0+1+0 22 0+1+0] [BFC] children: not-inline
Box <div> at [221,10] flex-container(row) [0+0+2 196 2+0+0] [0+0+1 20 1+0+0] [FFC] children: not-inline
BlockContainer <div> at [221,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: [221,11 8.8125x18] baseline: 13.796875
"2"
TextNode <#text> (not painted)
BlockContainer <button> at [382,11] flex-item [0+1+0 16 0+1+0] [0+1+0 18 0+1+0] [BFC] children: not-inline
BlockContainer <(anonymous)> at [382,11] flex-container(column) [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [FFC] children: not-inline
BlockContainer <(anonymous)> at [382,11] flex-item [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [382,11 16x16] baseline: 16
SVGSVGBox <svg> at [382,11] [0+0+0 16 0+0+0] [0+0+0 16 0+0+0] [SVG] children: not-inline
SVGGeometryBox <path> at [386,16.328125] [0+0+0 8 0+0+0] [0+0+0 4.9375 0+0+0] children: not-inline
BlockContainer <button> at [400,11] flex-item [0+1+0 16 0+1+0] [0+1+0 18 0+1+0] [BFC] children: not-inline
BlockContainer <(anonymous)> at [400,11] flex-container(column) [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [FFC] children: not-inline
BlockContainer <(anonymous)> at [400,11] flex-item [0+0+0 16 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline
frag 0 from SVGSVGBox start: 0, length: 0, rect: [400,11 16x16] baseline: 16
SVGSVGBox <svg> at [400,11] [0+0+0 16 0+0+0] [0+0+0 16 0+0+0] [SVG] children: not-inline
SVGGeometryBox <path> at [404,16.71875] [0+0+0 8 0+0+0] [0+0+0 4.953125 0+0+0] children: not-inline
TextNode <#text> (not painted)
BlockContainer <input> at [429,11] inline-block [0+1+0 200 0+1+0] [0+1+0 20 0+1+0] [BFC] children: not-inline
Box <div> at [431,12] flex-container(row) [0+0+2 196 2+0+0] [0+0+1 18 1+0+0] [FFC] children: not-inline
BlockContainer <div> at [431,12] flex-item [0+0+0 196 0+0+0] [0+0+0 18 0+0+0] [BFC] children: inline
frag 0 from TextNode start: 0, length: 1, rect: [431,12 9.09375x18] baseline: 13.796875
"3"
TextNode <#text> (not painted)
TextNode <#text> (not painted)
ViewportPaintable (Viewport<#document>) [0,0 800x600]
PaintableWithLines (BlockContainer<HTML>) [0,0 800x40]
PaintableWithLines (BlockContainer<BODY>) [8,8 784x24]
PaintableWithLines (BlockContainer<INPUT>) [8,8 202x24]
PaintableBox (Box<DIV>) [9,9 200x22]
PaintableWithLines (BlockContainer<DIV>) [11,11 160x18]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<BUTTON>) [171,10 18x20]
PaintableWithLines (BlockContainer(anonymous)) [172,11 16x18]
PaintableWithLines (BlockContainer(anonymous)) [172,11 16x18]
SVGSVGPaintable (SVGSVGBox<svg>) [172,11 16x16]
SVGPathPaintable (SVGGeometryBox<path>) [176,16.328125 8x4.9375]
PaintableWithLines (BlockContainer<BUTTON>) [189,10 18x20]
PaintableWithLines (BlockContainer(anonymous)) [190,11 16x18]
PaintableWithLines (BlockContainer(anonymous)) [190,11 16x18]
SVGSVGPaintable (SVGSVGBox<svg>) [190,11 16x16]
SVGPathPaintable (SVGGeometryBox<path>) [194,16.71875 8x4.953125]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<INPUT>) [218,8 202x24]
PaintableBox (Box<DIV>) [219,9 200x22]
PaintableWithLines (BlockContainer<DIV>) [221,11 160x18]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<BUTTON>) [381,10 18x20]
PaintableWithLines (BlockContainer(anonymous)) [382,11 16x18]
PaintableWithLines (BlockContainer(anonymous)) [382,11 16x18]
SVGSVGPaintable (SVGSVGBox<svg>) [382,11 16x16]
SVGPathPaintable (SVGGeometryBox<path>) [386,16.328125 8x4.9375]
PaintableWithLines (BlockContainer<BUTTON>) [399,10 18x20]
PaintableWithLines (BlockContainer(anonymous)) [400,11 16x18]
PaintableWithLines (BlockContainer(anonymous)) [400,11 16x18]
SVGSVGPaintable (SVGSVGBox<svg>) [400,11 16x16]
SVGPathPaintable (SVGGeometryBox<path>) [404,16.71875 8x4.953125]
TextPaintable (TextNode<#text>)
PaintableWithLines (BlockContainer<INPUT>) [428,10 202x22]
PaintableBox (Box<DIV>) [429,11 200x20]
PaintableWithLines (BlockContainer<DIV>) [431,12 196x18]
TextPaintable (TextNode<#text>)
SC for Viewport<#document> [0,0 800x600] [children: 1] (z-index: auto)
SC for BlockContainer<HTML> [0,0 800x40] [children: 0] (z-index: auto)

View file

@ -0,0 +1,4 @@
<!doctype html>
<input type="number" value="1" />
<input type="number" style="appearance: auto" value="2" />
<input type="number" style="appearance: textfield" value="3" />