Skip to content
Permalink
Browse files
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 9a503b34f22f8b58a6335bf7d7dea506e2515c2e
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

@@ -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;
@@ -185,6 +185,8 @@ struct PossiblyQuotedIdentifier {
PseudoClassHasAttachment,
#endif
PseudoClassModal,
PseudoClassUserInvalid,
PseudoClassUserValid,
};

enum PseudoElementType {
@@ -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;
@@ -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
@@ -67,6 +67,8 @@ scope
single-button
start
target
user-invalid
user-valid
valid
vertical
visited
@@ -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));
@@ -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();
@@ -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:
@@ -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)
@@ -73,6 +73,7 @@ HTMLFormControlElement::HTMLFormControlElement(const QualifiedName& tagName, Doc
, m_willValidate(true)
, m_isValid(true)
, m_wasChangedSinceLastFormControlChangeEvent(false)
, m_wasInteractedWithSinceLastFormSubmitEvent(false)
{
setHasCustomStyleResolveCallbacks();
}
@@ -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));
@@ -308,6 +325,7 @@ void HTMLFormControlElement::dispatchFormControlChangeEvent()
{
dispatchChangeEvent();
setChangedSinceLastFormControlChangeEvent(false);
setInteractedWithSinceLastFormSubmitEvent(true);
}

void HTMLFormControlElement::dispatchFormControlInputEvent()
@@ -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;

@@ -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));
@@ -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();
@@ -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(); }
@@ -218,6 +224,7 @@ class HTMLFormControlElement : public LabelableElement, public FormListedElement
unsigned m_isValid : 1;

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

class DelayedUpdateValidityScope {
@@ -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);
@@ -232,6 +232,7 @@ void HTMLTextFormControlElement::dispatchFormControlChangeEvent()
setTextAsOfLastFormControlChangeEvent(value());
}
setChangedSinceLastFormControlChangeEvent(false);
setInteractedWithSinceLastFormSubmitEvent(true);
}

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

0 comments on commit 9a503b3

Please sign in to comment.