Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Support :user-invalid / :user-valid pseudo-classes
https://bugs.webkit.org/show_bug.cgi?id=222267
rdar://74866546

Reviewed by Aditya Keerthi.

https://w3c.github.io/csswg-drafts/selectors/#user-pseudos

> The :user-invalid and the :user-valid pseudo-classes represent an element with incorrect or correct input, respectively, but only after the user has significantly interacted with it.

"significant user interaction" here is implemented as the first time the change event is emitted for a control, which is in practice
represents when the user has finished entering a value then moves focus away from the element. This matches Firefox's behavior.

User-initiated form submissions reset the state as the CSSWG spec says.

Based on patch by Devin Rousso.

* LayoutTests/imported/w3c/web-platform-tests/css/selectors/user-invalid-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/css/selectors/user-valid-expected.txt:
* Source/WebCore/css/CSSSelector.cpp:
(WebCore::CSSSelector::selectorText const):
* Source/WebCore/css/CSSSelector.h:
* Source/WebCore/css/SelectorChecker.cpp:
(WebCore::SelectorChecker::checkOne const):
* Source/WebCore/css/SelectorCheckerTestFunctions.h:
(WebCore::matchesUserInvalidPseudoClass):
(WebCore::matchesUserValidPseudoClass):
* Source/WebCore/css/SelectorPseudoClassAndCompatibilityElementMap.in:
* Source/WebCore/cssjit/SelectorCompiler.cpp:
(WebCore::SelectorCompiler::JSC_DEFINE_JIT_OPERATION):
(WebCore::SelectorCompiler::addPseudoClassType):
* Source/WebCore/html/FileInputType.cpp:
(WebCore::FileInputType::setFiles):
* Source/WebCore/html/HTMLFormControlElement.cpp:
(WebCore::HTMLFormControlElement::HTMLFormControlElement):
(WebCore::HTMLFormControlElement::setInteractedWithSinceLastFormSubmitEvent):
(WebCore::HTMLFormControlElement::dispatchFormControlChangeEvent):
(WebCore::HTMLFormControlElement::updateValidity):
(WebCore::HTMLFormControlElement::matchesUserInvalidPseudoClass const):
(WebCore::HTMLFormControlElement::matchesUserValidPseudoClass const):
* Source/WebCore/html/HTMLFormControlElement.h:
(WebCore::HTMLFormControlElement::wasInteractedWithSinceLastFormSubmitEvent const):
* Source/WebCore/html/HTMLFormElement.cpp:
(WebCore::HTMLFormElement::submitIfPossible):
* Source/WebCore/html/HTMLTextFormControlElement.cpp:
(WebCore::HTMLTextFormControlElement::dispatchFormControlChangeEvent):

Canonical link: https://commits.webkit.org/257997@main
  • Loading branch information
nt1m committed Dec 16, 2022
1 parent 37a7d80 commit 9a503b3
Show file tree
Hide file tree
Showing 13 changed files with 102 additions and 3 deletions.
@@ -1,5 +1,5 @@


FAIL :user-invalid selector should be supported The string did not match the expected pattern.
PASS :user-invalid selector should be supported
PASS :user-error selector should not be supported

@@ -1,4 +1,4 @@


FAIL :user-valid selector should be supported The string did not match the expected pattern.
PASS :user-valid selector should be supported

6 changes: 6 additions & 0 deletions Source/WebCore/css/CSSSelector.cpp
Expand Up @@ -704,6 +704,12 @@ String CSSSelector::selectorText(StringView separator, StringView rightSide) con
case CSSSelector::PseudoClassTarget:
builder.append(":target");
break;
case CSSSelector::PseudoClassUserInvalid:
builder.append(":user-invalid");
break;
case CSSSelector::PseudoClassUserValid:
builder.append(":user-valid");
break;
case CSSSelector::PseudoClassValid:
builder.append(":valid");
break;
Expand Down
2 changes: 2 additions & 0 deletions Source/WebCore/css/CSSSelector.h
Expand Up @@ -185,6 +185,8 @@ struct PossiblyQuotedIdentifier {
PseudoClassHasAttachment,
#endif
PseudoClassModal,
PseudoClassUserInvalid,
PseudoClassUserValid,
};

enum PseudoElementType {
Expand Down
6 changes: 6 additions & 0 deletions Source/WebCore/css/SelectorChecker.cpp
Expand Up @@ -1128,6 +1128,12 @@ bool SelectorChecker::checkOne(CheckingContext& checkingContext, const LocalCont
case CSSSelector::PseudoClassModal:
return matchesModalPseudoClass(element);

case CSSSelector::PseudoClassUserInvalid:
return matchesUserInvalidPseudoClass(element);

case CSSSelector::PseudoClassUserValid:
return matchesUserValidPseudoClass(element);

case CSSSelector::PseudoClassUnknown:
ASSERT_NOT_REACHED();
break;
Expand Down
14 changes: 14 additions & 0 deletions Source/WebCore/css/SelectorCheckerTestFunctions.h
Expand Up @@ -573,4 +573,18 @@ ALWAYS_INLINE bool matchesModalPseudoClass(const Element& element)
#endif
}

ALWAYS_INLINE bool matchesUserInvalidPseudoClass(const Element& element)
{
if (const auto* formControlElement = dynamicDowncast<HTMLFormControlElement>(element))
return formControlElement->matchesUserInvalidPseudoClass();
return false;
}

ALWAYS_INLINE bool matchesUserValidPseudoClass(const Element& element)
{
if (const auto* formControlElement = dynamicDowncast<HTMLFormControlElement>(element))
return formControlElement->matchesUserValidPseudoClass();
return false;
}

} // namespace WebCore
Expand Up @@ -67,6 +67,8 @@ scope
single-button
start
target
user-invalid
user-valid
valid
vertical
visited
Expand Down
20 changes: 20 additions & 0 deletions Source/WebCore/cssjit/SelectorCompiler.cpp
Expand Up @@ -129,6 +129,8 @@ static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationMatchesVolumeLock
static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationHasAttachment, bool, (const Element&));
#endif
static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationMatchesModalPseudoClass, bool, (const Element&));
static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationIsUserInvalid, bool, (const Element&));
static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationIsUserValid, bool, (const Element&));

static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationAttributeValueBeginsWithCaseSensitive, bool, (const Attribute* attribute, AtomStringImpl* expectedString));
static JSC_DECLARE_JIT_OPERATION_WITHOUT_WTF_INTERNAL(operationAttributeValueBeginsWithCaseInsensitive, bool, (const Attribute* attribute, AtomStringImpl* expectedString));
Expand Down Expand Up @@ -826,6 +828,16 @@ JSC_DEFINE_JIT_OPERATION(operationMatchesModalPseudoClass, bool, (const Element&
return matchesModalPseudoClass(element);
}

JSC_DEFINE_JIT_OPERATION(operationIsUserInvalid, bool, (const Element& element))
{
return matchesUserInvalidPseudoClass(element);
}

JSC_DEFINE_JIT_OPERATION(operationIsUserValid, bool, (const Element& element))
{
return matchesUserValidPseudoClass(element);
}

static inline FunctionType addPseudoClassType(const CSSSelector& selector, SelectorFragment& fragment, SelectorContext selectorContext, FragmentsLevel fragmentLevel, FragmentPositionInRootFragments positionInRootFragments, bool visitedMatchEnabled, VisitedMode& visitedMode, PseudoElementMatchingBehavior pseudoElementMatchingBehavior)
{
CSSSelector::PseudoClassType type = selector.pseudoClassType();
Expand Down Expand Up @@ -969,6 +981,14 @@ static inline FunctionType addPseudoClassType(const CSSSelector& selector, Selec
fragment.unoptimizedPseudoClasses.append(CodePtr<JSC::OperationPtrTag>(operationMatchesModalPseudoClass));
return FunctionType::SimpleSelectorChecker;

case CSSSelector::PseudoClassUserInvalid:
fragment.unoptimizedPseudoClasses.append(CodePtr<JSC::OperationPtrTag>(operationIsUserInvalid));
return FunctionType::SimpleSelectorChecker;

case CSSSelector::PseudoClassUserValid:
fragment.unoptimizedPseudoClasses.append(CodePtr<JSC::OperationPtrTag>(operationIsUserValid));
return FunctionType::SimpleSelectorChecker;

// These pseudo-classes only have meaning with scrollbars.
case CSSSelector::PseudoClassHorizontal:
case CSSSelector::PseudoClassVertical:
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/html/FileInputType.cpp
Expand Up @@ -405,6 +405,7 @@ void FileInputType::setFiles(RefPtr<FileList>&& files, RequestIcon shouldRequest
protectedInputElement->dispatchCancelEvent();

protectedInputElement->setChangedSinceLastFormControlChangeEvent(false);
protectedInputElement->setInteractedWithSinceLastFormSubmitEvent(true);
}

void FileInputType::filesChosen(const Vector<FileChooserFileInfo>& paths, const String& displayString, Icon* icon)
Expand Down
35 changes: 34 additions & 1 deletion Source/WebCore/html/HTMLFormControlElement.cpp
Expand Up @@ -73,6 +73,7 @@ HTMLFormControlElement::HTMLFormControlElement(const QualifiedName& tagName, Doc
, m_willValidate(true)
, m_isValid(true)
, m_wasChangedSinceLastFormControlChangeEvent(false)
, m_wasInteractedWithSinceLastFormSubmitEvent(false)
{
setHasCustomStyleResolveCallbacks();
}
Expand Down Expand Up @@ -294,6 +295,22 @@ void HTMLFormControlElement::setChangedSinceLastFormControlChangeEvent(bool chan
m_wasChangedSinceLastFormControlChangeEvent = changed;
}

void HTMLFormControlElement::setInteractedWithSinceLastFormSubmitEvent(bool interactedWith)
{
if (m_wasInteractedWithSinceLastFormSubmitEvent == interactedWith)
return;

auto isInvalid = matchesInvalidPseudoClass();
auto isValid = matchesValidPseudoClass();

Style::PseudoClassChangeInvalidation styleInvalidation(*this, {
{ CSSSelector::PseudoClassUserInvalid, isInvalid && interactedWith },
{ CSSSelector::PseudoClassUserValid, isValid && interactedWith },
});

m_wasInteractedWithSinceLastFormSubmitEvent = interactedWith;
}

void HTMLFormControlElement::dispatchChangeEvent()
{
dispatchScopedEvent(Event::create(eventNames().changeEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
Expand All @@ -308,6 +325,7 @@ void HTMLFormControlElement::dispatchFormControlChangeEvent()
{
dispatchChangeEvent();
setChangedSinceLastFormControlChangeEvent(false);
setInteractedWithSinceLastFormSubmitEvent(true);
}

void HTMLFormControlElement::dispatchFormControlInputEvent()
Expand Down Expand Up @@ -544,7 +562,12 @@ void HTMLFormControlElement::updateValidity()
bool newIsValid = this->computeValidity();

if (newIsValid != m_isValid) {
Style::PseudoClassChangeInvalidation styleInvalidation(*this, { { CSSSelector::PseudoClassValid, newIsValid }, { CSSSelector::PseudoClassInvalid, !newIsValid } });
Style::PseudoClassChangeInvalidation styleInvalidation(*this, {
{ CSSSelector::PseudoClassValid, newIsValid },
{ CSSSelector::PseudoClassInvalid, !newIsValid },
{ CSSSelector::PseudoClassUserValid, newIsValid && m_wasInteractedWithSinceLastFormSubmitEvent },
{ CSSSelector::PseudoClassUserInvalid, !newIsValid && m_wasInteractedWithSinceLastFormSubmitEvent },
});

m_isValid = newIsValid;

Expand Down Expand Up @@ -582,6 +605,16 @@ bool HTMLFormControlElement::validationMessageShadowTreeContains(const Node& nod
return m_validationMessage && m_validationMessage->shadowTreeContains(node);
}

bool HTMLFormControlElement::matchesUserInvalidPseudoClass() const
{
return m_wasInteractedWithSinceLastFormSubmitEvent && matchesInvalidPseudoClass();
}

bool HTMLFormControlElement::matchesUserValidPseudoClass() const
{
return m_wasInteractedWithSinceLastFormSubmitEvent && matchesValidPseudoClass();
}

void HTMLFormControlElement::dispatchBlurEvent(RefPtr<Element>&& newFocusedElement)
{
HTMLElement::dispatchBlurEvent(WTFMove(newFocusedElement));
Expand Down
7 changes: 7 additions & 0 deletions Source/WebCore/html/HTMLFormControlElement.h
Expand Up @@ -68,6 +68,9 @@ class HTMLFormControlElement : public LabelableElement, public FormListedElement
bool wasChangedSinceLastFormControlChangeEvent() const { return m_wasChangedSinceLastFormControlChangeEvent; }
void setChangedSinceLastFormControlChangeEvent(bool);

bool wasInteractedWithSinceLastFormSubmitEvent() const { return m_wasInteractedWithSinceLastFormSubmitEvent; }
void setInteractedWithSinceLastFormSubmitEvent(bool);

virtual void dispatchFormControlChangeEvent();
void dispatchChangeEvent();
void dispatchCancelEvent();
Expand Down Expand Up @@ -114,6 +117,9 @@ class HTMLFormControlElement : public LabelableElement, public FormListedElement
void updateValidity();
void setCustomValidity(const String&) override;

bool matchesUserInvalidPseudoClass() const;
bool matchesUserValidPseudoClass() const;

virtual bool supportsReadOnly() const { return false; }
bool isReadOnly() const { return supportsReadOnly() && m_hasReadOnlyAttribute; }
bool isMutable() const { return !isDisabledFormControl() && !isReadOnly(); }
Expand Down Expand Up @@ -218,6 +224,7 @@ class HTMLFormControlElement : public LabelableElement, public FormListedElement
unsigned m_isValid : 1;

unsigned m_wasChangedSinceLastFormControlChangeEvent : 1;
unsigned m_wasInteractedWithSinceLastFormSubmitEvent : 1;
};

class DelayedUpdateValidityScope {
Expand Down
7 changes: 7 additions & 0 deletions Source/WebCore/html/HTMLFormElement.cpp
Expand Up @@ -295,6 +295,13 @@ void HTMLFormElement::submitIfPossible(Event* event, HTMLFormControlElement* sub
m_isSubmittingOrPreparingForSubmission = true;
m_shouldSubmit = false;

if (UserGestureIndicator::processingUserGesture()) {
for (auto& element : m_listedElements) {
if (auto* formControlElement = dynamicDowncast<HTMLFormControlElement>(*element))
formControlElement->setInteractedWithSinceLastFormSubmitEvent(false);
}
}

bool shouldValidate = document().page() && document().page()->settings().interactiveFormValidationEnabled() && !noValidate();
if (shouldValidate) {
RefPtr submitElement = submitter ? submitter : findSubmitter(event);
Expand Down
1 change: 1 addition & 0 deletions Source/WebCore/html/HTMLTextFormControlElement.cpp
Expand Up @@ -232,6 +232,7 @@ void HTMLTextFormControlElement::dispatchFormControlChangeEvent()
setTextAsOfLastFormControlChangeEvent(value());
}
setChangedSinceLastFormControlChangeEvent(false);
setInteractedWithSinceLastFormSubmitEvent(true);
}

ExceptionOr<void> HTMLTextFormControlElement::setRangeText(StringView replacement)
Expand Down

0 comments on commit 9a503b3

Please sign in to comment.