From 2a24dc04d0316d6a051fb7749e0ab94f8e05b2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 19 Sep 2026 18:52:35 +0200 Subject: [PATCH] Dismiss stale validation feedback after mutations (#710) --- ...c-validation-message-dismissal-20260919.md | 21 ++++++ ...eractive-constraint-validation-20260919.md | 11 +++- .../webscene_v8_runtime_css_cascade.inc | 4 ++ .../native/webscene_v8_runtime_document.inc | 19 ++++++ .../webscene_v8_runtime_dom_properties.inc | 64 +++++++++++++++++++ .../native/webscene_v8_runtime_style.inc | 2 + .../native/webscene_v8_runtime_tasks.inc | 1 + 7 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 docs/validation/dynamic-validation-message-dismissal-20260919.md diff --git a/docs/validation/dynamic-validation-message-dismissal-20260919.md b/docs/validation/dynamic-validation-message-dismissal-20260919.md new file mode 100644 index 00000000..d3e5a9a7 --- /dev/null +++ b/docs/validation/dynamic-validation-message-dismissal-20260919.md @@ -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. diff --git a/docs/validation/interactive-constraint-validation-20260919.md b/docs/validation/interactive-constraint-validation-20260919.md index a9581358..a20cbc2a 100644 --- a/docs/validation/interactive-constraint-validation-20260919.md +++ b/docs/validation/interactive-constraint-validation-20260919.md @@ -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 @@ -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. diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc index bf38d5be..365a5ee3 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc @@ -2432,6 +2432,8 @@ std::optional user_validity_interacted = std::nullopt, std::optional 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(); @@ -2681,6 +2683,7 @@ void recascade_select_validity_selector_change( dom_node& select,std::optional 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); @@ -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( diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_document.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_document.inc index 2b8bcb28..45e6ec25 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_document.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_document.inc @@ -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); } @@ -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); } @@ -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); } @@ -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); } @@ -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, diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc index 071e8285..eedd9c7d 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc @@ -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( @@ -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); } @@ -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( @@ -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) @@ -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, const v8::PropertyCallbackInfo& info) @@ -4347,6 +4410,7 @@ old_value, next?std::optional{std::string{}} :std::optional{}); + self->dismiss_validation_message_after_constraint_mutation(*node); } static void get_disabled(v8::Local, const v8::PropertyCallbackInfo& info) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc index 87c2034e..e6935f97 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_style.inc @@ -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& info) @@ -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& info) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc index 035e0237..9550b1c2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_tasks.inc @@ -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);