Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/validation/dynamic-validation-message-dismissal-20260919.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Dynamic validation-message dismissal source contract

WebScene #710 completes the mutation-driven lifetime boundary for the
interactive validation message added by #708/#709. Programmatic text, numeric,
select, checkedness, reflected constraint, and attribute mutations retire an
active related message. Radio-group and select-option relationships are
included without scanning unrelated document controls.

Existing CSS recascade and inline-style checkpoints validate the retained
target and `ElementInternals` anchor after connected style changes. A valid or
barred target, stale node, or hidden, inert, disabled, or otherwise
nonfocusable anchor clears the state. Clearing is idempotent and uses the
existing scene generation, so a synchronous mutation burst causes at most one
message-state publication. No timer, polling task, animation frame, new DOM
node, selector pass, or presenter ABI is introduced.

Bounded wrapping, host-localizable formatting, and optional timed dismissal
remain follow-up work because they require separate text-layout and policy
contracts. Per task direction, no build, test, browser run, pixel check,
package gate, or CI job was run. Only `git diff --check` is used for this
commit; the source contract remains unexecuted acceptance evidence.
11 changes: 9 additions & 2 deletions docs/validation/interactive-constraint-validation-20260919.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ message. Navigation also retires any unconsumed semantic announcement. Stale
native IDs fail closed during scene construction, and a candidate removed by
an earlier `invalid` handler is skipped when focus is selected.

WebScene #710 also retires the message after related programmatic value and
constraint mutations. Existing recascade checkpoints recheck a connected
target and anchor after style/class/hidden/inert/disabled changes, dismissing
when the target becomes valid or barred or either element loses focus
eligibility. The retained-state clear is idempotent, so repeated synchronous
mutations request at most one additional scene publication and never poll.

## Validation status and deferred boundary

The behavior follows the HTML interactive constraint-validation ordering and
Expand All @@ -41,6 +48,6 @@ contracts. Per task direction, no build, native test, browser/WPT run, pixel
check, package gate, or CI job was run. Only `git diff --check` is used for this
commit, so the authored contract remains unexecuted source evidence.

Rich multi-line bubble layout, user-agent localization beyond the existing
validation messages, timed auto-dismissal, and structural selector
Rich multi-line bubble layout, host localization beyond the existing validation
messages, optional timed dismissal, and structural selector
invalidation remain outside #708.
Original file line number Diff line number Diff line change
Expand Up @@ -2432,6 +2432,8 @@
std::optional<bool> user_validity_interacted = std::nullopt,
std::optional<bool> value_changed_by_user = std::nullopt)
{
if(previous_value!=current_value)
dismiss_validation_message_after_constraint_mutation(subject);
if (previous_value != current_value)
mark_primary_selection_publication_dirty(&subject);
document.mark_dirty();
Expand Down Expand Up @@ -2681,6 +2683,7 @@
void recascade_select_validity_selector_change(
dom_node& select,std::optional<bool> previous_value_missing=std::nullopt)
{
dismiss_validation_message_after_constraint_mutation(select);
document.mark_dirty();
if(select.tag!="select" || !is_connected(select)) return;
const auto current_value_missing=forms::select_value_missing(select);
Expand Down Expand Up @@ -2904,6 +2907,7 @@
&& previous_cascade_root != node_cascade_root) {
activate_css_cascade(previous_cascade_root);
}
reconcile_validation_message_eligibility(node);
}

void schedule_style_recascade(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,9 @@
self->recascade_radio_group_selector_state_changes(radio_group_state);
if(node->tag=="option" && (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
self->notify_custom_element_attribute(*node, name, old_value, value);
}

Expand Down Expand Up @@ -920,6 +923,11 @@
if((!namespace_uri || namespace_uri->empty()) && node->tag=="option"
&& (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(!namespace_uri || namespace_uri->empty()) {
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
}
self->notify_custom_element_attribute(
*node, name, old_value, value, namespace_uri);
}
Expand Down Expand Up @@ -976,6 +984,9 @@
self->recascade_radio_group_selector_state_changes(radio_group_state);
if(node->tag=="option" && (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
self->notify_custom_element_attribute(
*node, name, old_value, std::nullopt);
}
Expand Down Expand Up @@ -1077,6 +1088,11 @@
if((!namespace_uri || namespace_uri->empty()) && node->tag=="option"
&& (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(!namespace_uri || namespace_uri->empty()) {
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
}
self->notify_custom_element_attribute(
*node, name, old_value, std::nullopt, namespace_uri);
}
Expand Down Expand Up @@ -1144,6 +1160,9 @@
self->recascade_radio_group_selector_state_changes(radio_group_state);
if(node->tag=="option" && (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
// Queue invalidation before author callbacks: they may synchronously
// read styles or replace this attribute with another value.
self->notify_custom_element_attribute(*node, name, old_value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2696,6 +2696,7 @@
}
}
self->recascade_radio_group_selector_state_changes(radio_group_state);
self->dismiss_validation_message_after_constraint_mutation(*node);
}

static void get_reflected_string_attribute(
Expand Down Expand Up @@ -2741,6 +2742,9 @@
self->recascade_radio_group_selector_state_changes(radio_group_state);
if(node->tag=="option" && (name=="selected" || name=="value"))
self->recascade_containing_select_validity(*node);
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
self->notify_custom_element_attribute(*node, name, old_value, text);
}

Expand Down Expand Up @@ -2827,6 +2831,7 @@
node->attributes["maxlength"] = std::to_string(
value->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(-1));
self->document.mark_dirty();
self->dismiss_validation_message_after_constraint_mutation(*node);
}

static void get_read_only(
Expand Down Expand Up @@ -3629,6 +3634,61 @@
if(document.clear_validation_message()) pending_validation_announcement.reset();
}

bool validation_mutation_affects_active_message(const dom_node& subject)
{
auto* target=document.find_by_native_id(
document.validation_message_target_id());
auto* anchor=document.find_by_native_id(
document.validation_message_anchor_id());
if(target==nullptr || anchor==nullptr) {
dismiss_validation_message();
return false;
}
return subtree_contains(subject,target)
|| subtree_contains(subject,anchor)
|| subtree_contains(*target,&subject)
|| subtree_contains(*anchor,&subject)
|| forms::same_radio_group(document,*target,subject);
}

void dismiss_validation_message_after_constraint_mutation(
const dom_node& subject)
{
if(document.validation_message_target_id()!=0U
&& validation_mutation_affects_active_message(subject))
dismiss_validation_message();
}

void reconcile_validation_message_eligibility(const dom_node& changed_root)
{
if(document.validation_message_target_id()==0U) return;
auto* target=document.find_by_native_id(
document.validation_message_target_id());
auto* anchor=document.find_by_native_id(
document.validation_message_anchor_id());
if(target==nullptr || anchor==nullptr) {
dismiss_validation_message();
return;
}
if(!subtree_contains(changed_root,target)
&& !subtree_contains(changed_root,anchor)) return;
if(!is_connected(*target) || !is_connected(*anchor)
|| !forms::will_validate(document,*target)
|| forms::constraint_validity_state(document,*target).valid()
|| !is_programmatically_focusable(anchor))
dismiss_validation_message();
}

static bool validation_constraint_attribute(std::string_view name)
{
return name=="value" || name=="required" || name=="pattern"
|| name=="minlength" || name=="maxlength" || name=="min"
|| name=="max" || name=="step" || name=="type"
|| name=="multiple" || name=="checked" || name=="selected"
|| name=="name" || name=="form" || name=="disabled"
|| name=="readonly";
}

bool present_validation_message(dom_node& target, dom_node& anchor)
{
if(!is_connected(target) || !is_connected(anchor)
Expand Down Expand Up @@ -4137,6 +4197,9 @@
else node->attributes.erase(name);
self->recascade_connected_subtree(*node);
self->recascade_radio_group_selector_state_changes(radio_group_state);
if(validation_constraint_attribute(name))
self->dismiss_validation_message_after_constraint_mutation(*node);
else self->reconcile_validation_message_eligibility(*node);
}

static void get_checked(v8::Local<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info)
Expand Down Expand Up @@ -4347,6 +4410,7 @@
old_value,
next?std::optional<std::string>{std::string{}}
:std::optional<std::string>{});
self->dismiss_validation_message_after_constraint_mutation(*node);
}

static void get_disabled(v8::Local<v8::Name>, const v8::PropertyCallbackInfo<v8::Value>& info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,7 @@
self->detect_css_compositions(*node);
self->mark_cssom_layout_dirty(*node, canonical_name);
}
self->reconcile_validation_message_eligibility(*node);
}

static void style_get_property(const v8::FunctionCallbackInfo<v8::Value>& info)
Expand Down Expand Up @@ -2718,6 +2719,7 @@
self->detect_css_compositions(*node);
self->mark_cssom_layout_dirty(*node, canonical_name);
}
self->reconcile_validation_message_eligibility(*node);
}

static void get_computed_style(const v8::FunctionCallbackInfo<v8::Value>& info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3417,6 +3417,7 @@
target.mutable_form_control().checkedness_initialized = true;
target.mutable_form_control().checkedness = checked;
if (previous != checked) {
dismiss_validation_message_after_constraint_mutation(target);
if (is_connected(target)) {
recascade_form_checkedness_selector_transition(
target,previous,checked);
Expand Down
Loading