mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-05-05 10:42:51 +00:00
LibWeb: Make input element placeholders look better
We now create a flex container inside the input element's UA shadow tree and add the placeholder and non-placeholder text as flex items (wrapped in elements whose style we can manipulate). This fixes the visual glitch where the placeholder would appear below the bounding box of the input element. It also allows us to align the text vertically inside the input element (like we're supposed to). In order to achieve this, I had to make two small architectural changes to layout tree building: - Elements can now report that they represent a given pseudo element. This allows us to instantiate the ::placeholder pseudo element as an actual DOM element inside the input element's UA shadow tree. - We no longer create a separate layout node for the shadow root itself. Instead, children of the shadow root are treated as if they were children of the DOM element itself for the purpose of layout tree building.
This commit is contained in:
parent
6fb661e781
commit
7d24c13d8b
Notes:
sideshowbarker
2024-07-16 23:08:48 +09:00
Author: https://github.com/awesomekling
Commit: 7d24c13d8b
Pull-request: https://github.com/SerenityOS/serenity/pull/19035
5 changed files with 109 additions and 47 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
|
||||
* Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
|
||||
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
|
@ -15,6 +15,7 @@
|
|||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/HTML/BrowsingContext.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
#include <LibWeb/HTML/HTMLDivElement.h>
|
||||
#include <LibWeb/HTML/HTMLFormElement.h>
|
||||
#include <LibWeb/HTML/HTMLInputElement.h>
|
||||
#include <LibWeb/HTML/Scripting/Environments.h>
|
||||
|
@ -56,7 +57,10 @@ JS::ThrowCompletionOr<void> HTMLInputElement::initialize(JS::Realm& realm)
|
|||
void HTMLInputElement::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
visitor.visit(m_text_node.ptr());
|
||||
visitor.visit(m_inner_text_element);
|
||||
visitor.visit(m_text_node);
|
||||
visitor.visit(m_placeholder_element);
|
||||
visitor.visit(m_placeholder_text_node);
|
||||
visitor.visit(m_legacy_pre_activation_behavior_checked_element_in_group.ptr());
|
||||
visitor.visit(m_selected_files);
|
||||
}
|
||||
|
@ -284,6 +288,8 @@ void HTMLInputElement::did_edit_text_node(Badge<BrowsingContext>)
|
|||
m_value = value_sanitization_algorithm(m_text_node->data());
|
||||
m_dirty_value = true;
|
||||
|
||||
update_placeholder_visibility();
|
||||
|
||||
// NOTE: This is a bit ad-hoc, but basically implements part of "4.10.5.5 Common event behaviors"
|
||||
// https://html.spec.whatwg.org/multipage/input.html#common-input-element-events
|
||||
queue_an_element_task(HTML::Task::Source::UserInteraction, [this] {
|
||||
|
@ -342,12 +348,26 @@ WebIDL::ExceptionOr<void> HTMLInputElement::set_value(DeprecatedString value)
|
|||
// 5. If the element's value (after applying the value sanitization algorithm) is different from oldValue,
|
||||
// and the element has a text entry cursor position, move the text entry cursor position to the end of the
|
||||
// text control, unselecting any selected text and resetting the selection direction to "none".
|
||||
if (m_text_node && (m_value != old_value))
|
||||
if (m_text_node && (m_value != old_value)) {
|
||||
m_text_node->set_data(m_value);
|
||||
update_placeholder_visibility();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void HTMLInputElement::update_placeholder_visibility()
|
||||
{
|
||||
if (!m_placeholder_element)
|
||||
return;
|
||||
auto placeholder_text = this->placeholder_value();
|
||||
if (placeholder_text.has_value()) {
|
||||
MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "block"sv));
|
||||
} else {
|
||||
MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Display, "none"sv));
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/input.html#the-input-element:attr-input-placeholder-3
|
||||
static bool is_allowed_to_have_placeholder(HTML::HTMLInputElement::TypeAttributeState state)
|
||||
{
|
||||
|
@ -389,6 +409,17 @@ Optional<DeprecatedString> HTMLInputElement::placeholder_value() const
|
|||
return placeholder;
|
||||
}
|
||||
|
||||
class PlaceholderElement final : public HTMLDivElement {
|
||||
JS_CELL(PlaceholderElement, HTMLDivElement);
|
||||
|
||||
public:
|
||||
PlaceholderElement(DOM::Document& document)
|
||||
: HTMLDivElement(document, DOM::QualifiedName { HTML::TagNames::div, ""sv, Namespace::HTML })
|
||||
{
|
||||
}
|
||||
virtual Optional<CSS::Selector::PseudoElement> pseudo_element() const override { return CSS::Selector::PseudoElement::Placeholder; }
|
||||
};
|
||||
|
||||
void HTMLInputElement::create_shadow_tree_if_needed()
|
||||
{
|
||||
if (shadow_root_internal())
|
||||
|
@ -412,7 +443,27 @@ void HTMLInputElement::create_shadow_tree_if_needed()
|
|||
if (initial_value.is_null())
|
||||
initial_value = DeprecatedString::empty();
|
||||
auto element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
|
||||
MUST(element->set_attribute(HTML::AttributeNames::style, "white-space: pre; padding-top: 1px; padding-bottom: 1px; padding-left: 2px; padding-right: 2px; height: 1lh;"));
|
||||
MUST(element->set_attribute(HTML::AttributeNames::style, R"~~~(
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
white-space: pre;
|
||||
border: none;
|
||||
padding: 1px 2px;
|
||||
)~~~"));
|
||||
|
||||
m_placeholder_element = heap().allocate<PlaceholderElement>(realm(), document()).release_allocated_value_but_fixme_should_propagate_errors();
|
||||
MUST(m_placeholder_element->style_for_bindings()->set_property(CSS::PropertyID::Height, "1lh"sv));
|
||||
|
||||
m_placeholder_text_node = heap().allocate<DOM::Text>(realm(), document(), initial_value).release_allocated_value_but_fixme_should_propagate_errors();
|
||||
m_placeholder_text_node->set_data(attribute(HTML::AttributeNames::placeholder));
|
||||
m_placeholder_text_node->set_owner_input_element({}, *this);
|
||||
MUST(m_placeholder_element->append_child(*m_placeholder_text_node));
|
||||
MUST(element->append_child(*m_placeholder_element));
|
||||
|
||||
m_inner_text_element = DOM::create_element(document(), HTML::TagNames::div, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
|
||||
MUST(m_inner_text_element->style_for_bindings()->set_property(CSS::PropertyID::Height, "1lh"sv));
|
||||
|
||||
m_text_node = heap().allocate<DOM::Text>(realm(), document(), initial_value).release_allocated_value_but_fixme_should_propagate_errors();
|
||||
m_text_node->set_always_editable(m_type != TypeAttributeState::FileUpload);
|
||||
m_text_node->set_owner_input_element({}, *this);
|
||||
|
@ -420,7 +471,8 @@ void HTMLInputElement::create_shadow_tree_if_needed()
|
|||
if (m_type == TypeAttributeState::Password)
|
||||
m_text_node->set_is_password_input({}, true);
|
||||
|
||||
MUST(element->append_child(*m_text_node));
|
||||
MUST(m_inner_text_element->append_child(*m_text_node));
|
||||
MUST(element->append_child(*m_inner_text_element));
|
||||
MUST(shadow_root->append_child(element));
|
||||
set_shadow_root(shadow_root);
|
||||
}
|
||||
|
@ -446,8 +498,13 @@ void HTMLInputElement::parse_attribute(DeprecatedFlyString const& name, Deprecat
|
|||
} else if (name == HTML::AttributeNames::type) {
|
||||
m_type = parse_type_attribute(value);
|
||||
} else if (name == HTML::AttributeNames::value) {
|
||||
if (!m_dirty_value)
|
||||
if (!m_dirty_value) {
|
||||
m_value = value_sanitization_algorithm(value);
|
||||
update_placeholder_visibility();
|
||||
}
|
||||
} else if (name == HTML::AttributeNames::placeholder) {
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->set_data(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,8 +531,13 @@ void HTMLInputElement::did_remove_attribute(DeprecatedFlyString const& name)
|
|||
if (!m_dirty_checkedness)
|
||||
set_checked(false, ChangeSource::Programmatic);
|
||||
} else if (name == HTML::AttributeNames::value) {
|
||||
if (!m_dirty_value)
|
||||
if (!m_dirty_value) {
|
||||
m_value = DeprecatedString::empty();
|
||||
update_placeholder_visibility();
|
||||
}
|
||||
} else if (name == HTML::AttributeNames::placeholder) {
|
||||
if (m_placeholder_text_node)
|
||||
m_placeholder_text_node->set_data({});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -785,8 +847,10 @@ void HTMLInputElement::reset_algorithm()
|
|||
|
||||
// and then invoke the value sanitization algorithm, if the type attribute's current state defines one.
|
||||
m_value = value_sanitization_algorithm(m_value);
|
||||
if (m_text_node)
|
||||
if (m_text_node) {
|
||||
m_text_node->set_data(m_value);
|
||||
update_placeholder_visibility();
|
||||
}
|
||||
}
|
||||
|
||||
void HTMLInputElement::form_associated_element_was_inserted()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue