Bug 1685926 - Group disconnected radio buttons together. r=saschanaz,smaug

Differential Revision: https://phabricator.services.mozilla.com/D162349
This commit is contained in:
Adam Vandolder
2023-09-28 06:35:44 +00:00
parent 54728736bb
commit aee8834b7a
20 changed files with 389 additions and 423 deletions

View File

@@ -36,8 +36,6 @@
#include "nsNetUtil.h"
#include "nsQueryObject.h"
#include "nsIRadioVisitor.h"
#include "HTMLDataListElement.h"
#include "HTMLFormSubmissionConstants.h"
#include "mozilla/Telemetry.h"
@@ -85,7 +83,9 @@
#include <algorithm>
// input type=radio
#include "nsIRadioGroupContainer.h"
#include "mozilla/dom/RadioGroupContainer.h"
#include "nsIRadioVisitor.h"
#include "nsRadioVisitor.h"
// input type=file
#include "mozilla/dom/FileSystemEntry.h"
@@ -106,7 +106,6 @@
#include "nsContentCreatorFunctions.h"
#include "nsContentUtils.h"
#include "mozilla/dom/DirectionalityUtils.h"
#include "nsRadioVisitor.h"
#include "mozilla/LookAndFeel.h"
#include "mozilla/Preferences.h"
@@ -1014,7 +1013,8 @@ HTMLInputElement::HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo,
mPickerRunning(false),
mIsPreviewEnabled(false),
mHasBeenTypePassword(false),
mHasPatternAttribute(false) {
mHasPatternAttribute(false),
mRadioGroupContainer(nullptr) {
// If size is above 512, mozjemalloc allocates 1kB, see
// memory/build/mozjemalloc.cpp
static_assert(sizeof(HTMLInputElement) <= 512,
@@ -1186,9 +1186,9 @@ void HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName,
if (mType == FormControlType::InputRadio) {
if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
(mForm || mDoneCreating)) {
WillRemoveFromRadioGroup();
RemoveFromRadioGroup();
} else if (aName == nsGkAtoms::required) {
nsCOMPtr<nsIRadioGroupContainer> container = GetRadioGroupContainer();
auto* container = GetCurrentRadioGroupContainer();
if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) ||
(!aValue && HasAttr(aNameSpaceID, aName)))) {
@@ -1284,7 +1284,7 @@ void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
// If we are not done creating the radio, we also should not do it.
if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) &&
mType == FormControlType::InputRadio && (mForm || mDoneCreating)) {
AddedToRadioGroup();
AddToRadioGroup();
UpdateValueMissingValidityStateForRadio(false);
needValidityUpdate = true;
}
@@ -1416,7 +1416,7 @@ void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName,
void HTMLInputElement::BeforeSetForm(bool aBindToTree) {
// No need to remove from radio group if we are just binding to tree.
if (mType == FormControlType::InputRadio && !aBindToTree) {
WillRemoveFromRadioGroup();
RemoveFromRadioGroup();
}
}
@@ -1424,8 +1424,9 @@ void HTMLInputElement::AfterClearForm(bool aUnbindOrDelete) {
MOZ_ASSERT(!mForm);
// Do not add back to radio group if we are releasing or unbinding from tree.
if (mType == FormControlType::InputRadio && !aUnbindOrDelete) {
AddedToRadioGroup();
if (mType == FormControlType::InputRadio && !aUnbindOrDelete &&
!GetCurrentRadioGroupContainer()) {
AddToRadioGroup();
UpdateValueMissingValidityStateForRadio(false);
}
}
@@ -2861,8 +2862,7 @@ void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify,
return;
}
nsIRadioGroupContainer* container = GetRadioGroupContainer();
if (container) {
if (auto* container = GetCurrentRadioGroupContainer()) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->SetCurrentRadioButton(name, nullptr);
@@ -2885,8 +2885,7 @@ void HTMLInputElement::RadioSetChecked(bool aNotify) {
}
// Let the group know that we are now the One True Radio Button
nsIRadioGroupContainer* container = GetRadioGroupContainer();
if (container) {
if (auto* container = GetCurrentRadioGroupContainer()) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->SetCurrentRadioButton(name, this);
@@ -2897,38 +2896,39 @@ void HTMLInputElement::RadioSetChecked(bool aNotify) {
SetCheckedInternal(true, aNotify);
}
nsIRadioGroupContainer* HTMLInputElement::GetRadioGroupContainer() const {
RadioGroupContainer* HTMLInputElement::GetCurrentRadioGroupContainer() const {
NS_ASSERTION(
mType == FormControlType::InputRadio,
"GetRadioGroupContainer should only be called when type='radio'");
return mRadioGroupContainer;
}
RadioGroupContainer* HTMLInputElement::FindTreeRadioGroupContainer() const {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
if (name.IsEmpty()) {
return nullptr;
}
if (mForm) {
return mForm;
return &mForm->OwnedRadioGroupContainer();
}
if (IsInNativeAnonymousSubtree()) {
return nullptr;
}
DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot();
if (!docOrShadow) {
return nullptr;
if (Document* doc = GetUncomposedDoc()) {
return &doc->OwnedRadioGroupContainer();
}
return &static_cast<FragmentOrElement*>(SubtreeRoot())
->OwnedRadioGroupContainer();
}
nsCOMPtr<nsIRadioGroupContainer> container =
do_QueryInterface(&(docOrShadow->AsNode()));
return container;
void HTMLInputElement::DisconnectRadioGroupContainer() {
mRadioGroupContainer = nullptr;
}
HTMLInputElement* HTMLInputElement::GetSelectedRadioButton() const {
nsIRadioGroupContainer* container = GetRadioGroupContainer();
auto* container = GetCurrentRadioGroupContainer();
if (!container) {
return nullptr;
}
@@ -4140,7 +4140,7 @@ nsresult HTMLInputElement::MaybeHandleRadioButtonNavigation(
}
// Arrow key pressed, focus+select prev/next radio button
RefPtr<HTMLInputElement> selectedRadioButton;
if (nsIRadioGroupContainer* container = GetRadioGroupContainer()) {
if (auto* container = GetCurrentRadioGroupContainer()) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->GetNextRadioButton(name, move == RadioButtonMove::Back, this,
@@ -4270,6 +4270,12 @@ void HTMLInputElement::MaybeLoadImage() {
}
nsresult HTMLInputElement::BindToTree(BindContext& aContext, nsINode& aParent) {
// If we are currently bound to a disconnected subtree root, remove
// ourselves from it first.
if (!mForm && mType == FormControlType::InputRadio) {
RemoveFromRadioGroup();
}
nsresult rv =
nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent);
NS_ENSURE_SUCCESS(rv, rv);
@@ -4292,9 +4298,8 @@ nsresult HTMLInputElement::BindToTree(BindContext& aContext, nsINode& aParent) {
// Add radio to document if we don't have a form already (if we do it's
// already been added into that group)
if (!mForm && mType == FormControlType::InputRadio &&
GetUncomposedDocOrConnectedShadowRoot()) {
AddedToRadioGroup();
if (!mForm && mType == FormControlType::InputRadio) {
AddToRadioGroup();
}
// Set direction based on value if dir=auto
@@ -4346,7 +4351,7 @@ void HTMLInputElement::UnbindFromTree(bool aNullParent) {
// of the case where we're removing from the document and we don't
// have a form
if (!mForm && mType == FormControlType::InputRadio) {
WillRemoveFromRadioGroup();
RemoveFromRadioGroup();
}
if (CreatesDateTimeWidget() && IsInComposedDoc()) {
@@ -4356,6 +4361,12 @@ void HTMLInputElement::UnbindFromTree(bool aNullParent) {
nsImageLoadingContent::UnbindFromTree(aNullParent);
nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent);
// If we are contained within a disconnected subtree, attempt to add
// ourselves to the subtree root's radio group.
if (!mForm && mType == FormControlType::InputRadio) {
AddToRadioGroup();
}
// GetCurrentDoc is returning nullptr so we can update the value
// missing validity state to reflect we are no longer into a doc.
UpdateValueMissingValidityState();
@@ -6220,16 +6231,27 @@ bool HTMLInputElement::RestoreState(PresState* aState) {
* Radio group stuff
*/
void HTMLInputElement::AddedToRadioGroup() {
// If the element is neither in a form nor a document, there is no group so we
// should just stop here.
if (!mForm && (!GetUncomposedDocOrConnectedShadowRoot() ||
IsInNativeAnonymousSubtree())) {
void HTMLInputElement::AddToRadioGroup() {
MOZ_ASSERT(!mRadioGroupContainer,
"Radio button must be removed from previous radio group container "
"before being added to another!");
// If the element has no radio group container we can stop here.
auto* container = FindTreeRadioGroupContainer();
if (!container) {
return;
}
// Make sure not to notify if we're still being created
bool notify = mDoneCreating;
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
// If we are part of a radio group, the element must have a name.
MOZ_ASSERT(!name.IsEmpty());
//
// Add the radio to the radio group container.
//
container->AddToRadioGroup(name, this, mForm);
mRadioGroupContainer = container;
//
// If the input element is checked, and we add it to the group, it will
@@ -6242,8 +6264,9 @@ void HTMLInputElement::AddedToRadioGroup() {
// radio button, but as adding a checked radio button into the group
// should not be that common an occurrence, I think we can live with
// that.
// Make sure not to notify if we're still being created.
//
RadioSetChecked(notify);
RadioSetChecked(mDoneCreating);
}
//
@@ -6258,24 +6281,14 @@ void HTMLInputElement::AddedToRadioGroup() {
SetCheckedChangedInternal(checkedChanged);
//
// Add the radio to the radio group container.
//
nsCOMPtr<nsIRadioGroupContainer> container = GetRadioGroupContainer();
if (container) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
container->AddToRadioGroup(name, this);
// We initialize the validity of the element to the validity of the group
// because we assume UpdateValueMissingState() will be called after.
SetValidityState(VALIDITY_STATE_VALUE_MISSING,
container->GetValueMissingState(name));
}
// We initialize the validity of the element to the validity of the group
// because we assume UpdateValueMissingState() will be called after.
SetValidityState(VALIDITY_STATE_VALUE_MISSING,
container->GetValueMissingState(name));
}
void HTMLInputElement::WillRemoveFromRadioGroup() {
nsIRadioGroupContainer* container = GetRadioGroupContainer();
void HTMLInputElement::RemoveFromRadioGroup() {
auto* container = GetCurrentRadioGroupContainer();
if (!container) {
return;
}
@@ -6296,6 +6309,7 @@ void HTMLInputElement::WillRemoveFromRadioGroup() {
// the group validity is updated (with this element being ignored).
UpdateValueMissingValidityStateForRadio(true);
container->RemoveFromRadioGroup(name, this);
mRadioGroupContainer = nullptr;
}
bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
@@ -6352,7 +6366,7 @@ bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
// Current radio button is not selected.
// But make it tabbable if nothing in group is selected.
nsIRadioGroupContainer* container = GetRadioGroupContainer();
auto* container = GetCurrentRadioGroupContainer();
if (!container) {
*aIsFocusable = defaultFocusable;
return false;
@@ -6369,8 +6383,7 @@ bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable,
}
nsresult HTMLInputElement::VisitGroup(nsIRadioVisitor* aVisitor) {
nsIRadioGroupContainer* container = GetRadioGroupContainer();
if (container) {
if (auto* container = GetCurrentRadioGroupContainer()) {
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
return container->WalkRadioGroup(name, aVisitor);
@@ -6661,21 +6674,15 @@ void HTMLInputElement::UpdateValueMissingValidityStateForRadio(
bool selected = selection || (!aIgnoreSelf && mChecked);
bool required = !aIgnoreSelf && IsRequired();
nsCOMPtr<nsIRadioGroupContainer> container = GetRadioGroupContainer();
auto* container = GetCurrentRadioGroupContainer();
if (!container) {
SetValidityState(VALIDITY_STATE_VALUE_MISSING, false);
return;
}
nsAutoString name;
GetAttr(nsGkAtoms::name, name);
if (!container) {
// As per the spec, a radio button not within a radio button group cannot
// suffer from being missing; however, we currently are failing to get a
// radio group in the case of a single, named radio button that has no
// form owner, forcing us to check for validity in that case here.
SetValidityState(VALIDITY_STATE_VALUE_MISSING,
required && !selected && !name.IsEmpty());
return;
}
// If the current radio is required and not ignored, we can assume the entire
// group is required.
if (!required) {