Commit faf7d127 authored by Kent Tamura's avatar Kent Tamura Committed by Commit Bot

Form-associated custom elements: Support 'disabled' attribute behavior

Form-associated custom elements should support 'disabled' content
attribute and <fieldset disabled> ancestors. It affects form
submission, focusability, :disabled :enabled CSS selectors.

Implementation:
- HTMLElement::IsDisabledFormControl() is hooked to refer to
  ListedElement::IsActuallyDisabled().  This affects :disabled
  selector.
- HTMLElement::MatchesEnabledPseudoClass() is hooked to support
  :enabled selector.
- HTMLElement::AttributeChanged() handles 'disabled' attribute change.
- HTMLElement::SupportsFocus() takes into account
  IsDisabledFormControl()
- ElementInternals::AppendToFormData() aborts if
  IsDisabledFOrmControl().
- HTMLFieldSetElement should handle form-associated custom elements
  as well as HTMLFormControlElements.

- Form-associated checks in ElementInternals is updated so that
  SetFormValue() and 'form' work in constructors.

Note that 'disabledStateChangedCallback' will be implemented by
a following CL.

Bug: 905922
Bug: https://github.com/w3c/webcomponents/issues/187
Change-Id: If4a348b6f3fec5cd6faab6da04ac2de1c228caf7
Reviewed-on: https://chromium-review.googlesource.com/c/1354752Reviewed-by: default avatarHayato Ito <hayato@chromium.org>
Commit-Queue: Kent Tamura <tkent@chromium.org>
Cr-Commit-Position: refs/heads/master@{#612551}
parent c2c37f34
......@@ -7,6 +7,7 @@
#include "third_party/blink/renderer/core/dom/node_lists_node_data.h"
#include "third_party/blink/renderer/core/fileapi/file.h"
#include "third_party/blink/renderer/core/html/custom/custom_element.h"
#include "third_party/blink/renderer/core/html/custom/custom_element_registry.h"
#include "third_party/blink/renderer/core/html/forms/form_data.h"
#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
......@@ -33,7 +34,7 @@ void ElementInternals::setFormValue(const FileOrUSVString& value,
void ElementInternals::setFormValue(const FileOrUSVString& value,
FormData* entry_source,
ExceptionState& exception_state) {
if (!Target().IsFormAssociatedCustomElement()) {
if (!IsTargetFormAssociated()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The target element is not a form-associated custom element.");
......@@ -49,7 +50,7 @@ void ElementInternals::setFormValue(const FileOrUSVString& value,
}
HTMLFormElement* ElementInternals::form(ExceptionState& exception_state) const {
if (!Target().IsFormAssociatedCustomElement()) {
if (!IsTargetFormAssociated()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"The target element is not a form-associated custom element.");
......@@ -76,6 +77,22 @@ void ElementInternals::DidUpgrade() {
}
}
bool ElementInternals::IsTargetFormAssociated() const {
if (Target().IsFormAssociatedCustomElement())
return true;
if (Target().GetCustomElementState() != CustomElementState::kUndefined)
return false;
// An element is in "undefined" state in its constructor JavaScript code.
// ElementInternals needs to handle elements to be form-associated same as
// form-associated custom elements because web authors want to call
// form-related operations of ElementInternals in constructors.
CustomElementRegistry* registry = CustomElement::Registry(Target());
if (!registry)
return false;
auto* definition = registry->DefinitionForName(Target().localName());
return definition && definition->IsFormAssociated();
}
bool ElementInternals::IsFormControlElement() const {
return false;
}
......@@ -89,6 +106,8 @@ bool ElementInternals::IsEnumeratable() const {
}
void ElementInternals::AppendToFormData(FormData& form_data) {
if (Target().IsDisabledFormControl())
return;
const AtomicString& name = Target().FastGetAttribute(html_names::kNameAttr);
if (!entry_source_ || entry_source_->size() == 0u) {
if (name.IsNull())
......
......@@ -34,6 +34,8 @@ class ElementInternals : public ScriptWrappable, public ListedElement {
HTMLFormElement* form(ExceptionState& exception_state) const;
private:
bool IsTargetFormAssociated() const;
// ListedElement overrides:
bool IsFormControlElement() const override;
bool IsElementInternals() const override;
......
......@@ -27,6 +27,7 @@
#include "third_party/blink/renderer/core/dom/element_traversal.h"
#include "third_party/blink/renderer/core/dom/events/event_dispatch_forbidden_scope.h"
#include "third_party/blink/renderer/core/dom/node_lists_node_data.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_legend_element.h"
#include "third_party/blink/renderer/core/html/html_collection.h"
#include "third_party/blink/renderer/core/html_names.h"
......@@ -73,9 +74,13 @@ HTMLFieldSetElement::InvalidateDescendantDisabledStateAndFindFocusedOne(
bool should_blur = false;
{
EventDispatchForbiddenScope event_forbidden;
for (HTMLFormControlElement& element :
Traversal<HTMLFormControlElement>::DescendantsOf(base)) {
element.AncestorDisabledStateWasChanged();
for (HTMLElement& element : Traversal<HTMLElement>::DescendantsOf(base)) {
if (auto* control = ToHTMLFormControlElementOrNull(element))
control->AncestorDisabledStateWasChanged();
else if (element.IsFormAssociatedCustomElement())
element.EnsureElementInternals().AncestorDisabledStateWasChanged();
else
continue;
if (focused_element == &element && element.IsDisabledFormControl())
should_blur = true;
}
......
......@@ -89,6 +89,8 @@ class CORE_EXPORT ListedElement : public GarbageCollectedMixin {
bool Valid() const;
virtual void setCustomValidity(const String&);
virtual void DisabledAttributeChanged();
void FormAttributeTargetChanged();
void InsertedInto(ContainerNode&);
void RemovedFrom(ContainerNode&);
......@@ -119,8 +121,6 @@ class CORE_EXPORT ListedElement : public GarbageCollectedMixin {
String CustomValidationMessage() const;
virtual void DisabledAttributeChanged();
// False; There are no FIELDSET ancestors.
// True; There might be a FIELDSET ancestor, and thre might be no
// FIELDSET ancestors.
......
......@@ -589,6 +589,17 @@ const AtomicString& HTMLElement::EventNameForAttributeName(
void HTMLElement::AttributeChanged(const AttributeModificationParams& params) {
Element::AttributeChanged(params);
if (params.name == html_names::kDisabledAttr &&
params.old_value.IsNull() != params.new_value.IsNull()) {
if (IsFormAssociatedCustomElement()) {
EnsureElementInternals().DisabledAttributeChanged();
if (params.reason == AttributeModificationReason::kDirectly &&
IsDisabledFormControl() &&
AdjustedFocusedElementInTreeScope() == this)
blur();
}
return;
}
if (params.reason != AttributeModificationReason::kDirectly)
return;
// adjustedFocusedElementInTreeScope() is not trivial. We should check
......@@ -1463,6 +1474,24 @@ bool HTMLElement::IsFormAssociatedCustomElement() const {
GetCustomElementDefinition()->IsFormAssociated();
}
bool HTMLElement::SupportsFocus() const {
return Element::SupportsFocus() && !IsDisabledFormControl();
};
bool HTMLElement::IsDisabledFormControl() const {
if (!IsFormAssociatedCustomElement())
return false;
return const_cast<HTMLElement*>(this)
->EnsureElementInternals()
.IsActuallyDisabled();
}
bool HTMLElement::MatchesEnabledPseudoClass() const {
return IsFormAssociatedCustomElement() && !const_cast<HTMLElement*>(this)
->EnsureElementInternals()
.IsActuallyDisabled();
}
} // namespace blink
#ifndef NDEBUG
......
......@@ -116,6 +116,9 @@ class CORE_EXPORT HTMLElement : public Element {
static const AtomicString& EventNameForAttributeName(
const QualifiedName& attr_name);
bool SupportsFocus() const override;
bool IsDisabledFormControl() const override;
bool MatchesEnabledPseudoClass() const override;
bool MatchesReadOnlyPseudoClass() const override;
bool MatchesReadWritePseudoClass() const override;
......
<!DOCTYPE html>
<body>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
class MyControl extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.internals_ = this.attachInternals();
this.internals_.setFormValue('my-control-value');
this.disabledHistory_ = [];
}
disabledStateChangedCallback(isDisabled) {
this.disabledHistory_.push(isDisabled);
}
disabledHistory() {
return this.disabledHistory_;
}
}
customElements.define('my-control', MyControl);
test(() => {
const control = new MyControl();
assert_true(control.matches(':enabled'));
assert_false(control.matches(':disabled'));
control.setAttribute('disabled', '');
assert_false(control.matches(':enabled'));
assert_true(control.matches(':disabled'));
control.removeAttribute('disabled', '');
assert_true(control.matches(':enabled'));
assert_false(control.matches(':disabled'));
}, 'Adding/removing disabled content attribute');
test(() => {
const container = document.createElement('fieldset');
container.innerHTML = '<fieldset><fieldset><my-control></my-control></fieldset></fieldset>';
const middleFieldset = container.firstChild;
const control = container.querySelector('my-control');
assert_true(control.matches(':enabled'));
assert_false(control.matches(':disabled'));
middleFieldset.disabled = true;
assert_false(control.matches(':enabled'));
assert_true(control.matches(':disabled'));
middleFieldset.disabled = false;
assert_true(control.matches(':enabled'));
assert_false(control.matches(':disabled'));
container.disabled = true;
assert_false(control.matches(':enabled'));
assert_true(control.matches(':disabled'));
control.remove();
assert_true(control.matches(':enabled'));
assert_false(control.matches(':disabled'));
middleFieldset.appendChild(control);
assert_false(control.matches(':enabled'));
assert_true(control.matches(':disabled'));
}, 'Relationship with FIELDSET');
test(() => {
const form = document.createElement('form');
document.body.appendChild(form);
form.innerHTML = '<my-control name="n1" disabled></my-control><input name="n2">'
const formData = new FormData(form);
assert_equals(formData.get('n1'), null);
}, 'A disabled form-associated custom element should not submit its value');
test(() => {
const control = new MyControl();
document.body.appendChild(control);
control.setAttribute('tabindex', '0');
control.setAttribute('disabled', '');
control.focus();
assert_not_equals(document.activeElement, control);
control.removeAttribute('disabled');
control.focus();
assert_equals(document.activeElement, control);
}, 'Disabled attribute affects focus-capability');
</script>
</body>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment