LibWeb: Implement the autocapitalize attribute

This commit is contained in:
Callum Law 2025-05-26 16:31:43 +12:00 committed by Luke Wilde
commit a6fb7c84e9
Notes: github-actions[bot] 2025-08-29 14:48:31 +00:00
6 changed files with 808 additions and 1 deletions

View file

@ -26,6 +26,7 @@ namespace AttributeNames {
__ENUMERATE_HTML_ATTRIBUTE(archive, "archive") \ __ENUMERATE_HTML_ATTRIBUTE(archive, "archive") \
__ENUMERATE_HTML_ATTRIBUTE(as, "as") \ __ENUMERATE_HTML_ATTRIBUTE(as, "as") \
__ENUMERATE_HTML_ATTRIBUTE(async, "async") \ __ENUMERATE_HTML_ATTRIBUTE(async, "async") \
__ENUMERATE_HTML_ATTRIBUTE(autocapitalize, "autocapitalize") \
__ENUMERATE_HTML_ATTRIBUTE(autocomplete, "autocomplete") \ __ENUMERATE_HTML_ATTRIBUTE(autocomplete, "autocomplete") \
__ENUMERATE_HTML_ATTRIBUTE(autofocus, "autofocus") \ __ENUMERATE_HTML_ATTRIBUTE(autofocus, "autofocus") \
__ENUMERATE_HTML_ATTRIBUTE(autoplay, "autoplay") \ __ENUMERATE_HTML_ATTRIBUTE(autoplay, "autoplay") \

View file

@ -2232,4 +2232,97 @@ void HTMLElement::set_writing_suggestions(String const& given_value)
MUST(set_attribute(HTML::AttributeNames::writingsuggestions, given_value)); MUST(set_attribute(HTML::AttributeNames::writingsuggestions, given_value));
} }
// https://html.spec.whatwg.org/multipage/interaction.html#own-autocapitalization-hint
HTMLElement::AutocapitalizationHint HTMLElement::own_autocapitalization_hint() const
{
// The autocapitalization processing model is based on selecting among five autocapitalization hints, defined as follows:
//
// default
// The user agent and input method should make their own determination of whether or not to enable autocapitalization.
// none
// No autocapitalization should be applied (all letters should default to lowercase).
// sentences
// The first letter of each sentence should default to a capital letter; all other letters should default to lowercase.
// words
// The first letter of each word should default to a capital letter; all other letters should default to lowercase.
// characters
// All letters should default to uppercase.
// The autocapitalize attribute is an enumerated attribute whose states are the possible autocapitalization hints.
// The autocapitalization hint specified by the attribute's state combines with other considerations to form the
// used autocapitalization hint, which informs the behavior of the user agent. The keywords for this attribute and
// their state mappings are as follows:
// Keyword | State
// off | none
// none |
// on | sentences
// sentences |
// words | words
// characters | characters
// The attribute's missing value default is the default state, and its invalid value default is the sentences state.
// To compute the own autocapitalization hint of an element element, run the following steps:
// 1. If the autocapitalize content attribute is present on element, and its value is not the empty string, return the
// state of the attribute.
auto maybe_autocapitalize_attribute = attribute(HTML::AttributeNames::autocapitalize);
if (maybe_autocapitalize_attribute.has_value() && !maybe_autocapitalize_attribute.value().is_empty()) {
auto autocapitalize_attribute_string_view = maybe_autocapitalize_attribute.value().bytes_as_string_view();
if (autocapitalize_attribute_string_view.is_one_of_ignoring_ascii_case("off"sv, "none"sv))
return AutocapitalizationHint::None;
if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("words"sv))
return AutocapitalizationHint::Words;
if (autocapitalize_attribute_string_view.equals_ignoring_ascii_case("characters"sv))
return AutocapitalizationHint::Characters;
return AutocapitalizationHint::Sentences;
}
// If element is an autocapitalize-and-autocorrect inheriting element and has a non-null form owner, return the own autocapitalization hint of element's form owner.
auto const* form_associated_element = as_if<FormAssociatedElement>(this);
if (form_associated_element && form_associated_element->is_autocapitalize_and_autocorrect_inheriting() && form_associated_element->form())
return form_associated_element->form()->own_autocapitalization_hint();
// 3. Return default.
return AutocapitalizationHint::Default;
}
// https://html.spec.whatwg.org/multipage/interaction.html#attr-autocapitalize
String HTMLElement::autocapitalize() const
{
// The autocapitalize getter steps are to:
// 1. Let state be the own autocapitalization hint of this.
auto state = own_autocapitalization_hint();
// 2. If state is default, then return the empty string.
// 3. If state is none, then return "none".
// 4. If state is sentences, then return "sentences".
// 5. Return the keyword value corresponding to state.
switch (state) {
case AutocapitalizationHint::Default:
return String {};
case AutocapitalizationHint::None:
return "none"_string;
case AutocapitalizationHint::Sentences:
return "sentences"_string;
case AutocapitalizationHint::Words:
return "words"_string;
case AutocapitalizationHint::Characters:
return "characters"_string;
}
VERIFY_NOT_REACHED();
}
void HTMLElement::set_autocapitalize(String const& given_value)
{
// The autocapitalize setter steps are to set the autocapitalize content attribute to the given value.
MUST(set_attribute(HTML::AttributeNames::autocapitalize, given_value));
}
} }

View file

@ -123,6 +123,18 @@ public:
String writing_suggestions() const; String writing_suggestions() const;
void set_writing_suggestions(String const&); void set_writing_suggestions(String const&);
enum class AutocapitalizationHint {
Default,
None,
Sentences,
Words,
Characters
};
AutocapitalizationHint own_autocapitalization_hint() const;
String autocapitalize() const;
void set_autocapitalize(String const&);
bool fire_a_synthetic_pointer_event(FlyString const& type, DOM::Element& target, bool not_trusted); bool fire_a_synthetic_pointer_event(FlyString const& type, DOM::Element& target, bool not_trusted);
// https://html.spec.whatwg.org/multipage/forms.html#category-label // https://html.spec.whatwg.org/multipage/forms.html#category-label

View file

@ -26,7 +26,7 @@ interface HTMLElement : Element {
[CEReactions] attribute boolean draggable; [CEReactions] attribute boolean draggable;
[CEReactions] attribute boolean spellcheck; [CEReactions] attribute boolean spellcheck;
[CEReactions] attribute DOMString writingSuggestions; [CEReactions] attribute DOMString writingSuggestions;
[FIXME, CEReactions] attribute DOMString autocapitalize; [CEReactions] attribute DOMString autocapitalize;
[FIXME, CEReactions] attribute boolean autocorrect; [FIXME, CEReactions] attribute boolean autocorrect;
[LegacyNullToEmptyString, CEReactions] attribute Utf16DOMString innerText; [LegacyNullToEmptyString, CEReactions] attribute Utf16DOMString innerText;

View file

@ -0,0 +1,13 @@
Harness status: OK
Found 8 tests
8 Pass
Pass Test that the autocapitalize is available on HTMLInputElement.
Pass Test that the autocapitalize is available on HTMLTextAreaElement.
Pass Test that the autocapitalize is available on div.
Pass Test deprecated values of autocapitalize.
Pass Test reflection of autocapitalize.
Pass Test that the IDL attribute returns the empty string if the content attribute is not set.
Pass Test inheriting values from a form.
Pass Verify that even input types that are never autocapitalized support the IDL interface.

View file

@ -0,0 +1,688 @@
<!DOCTYPE html>
<html>
<link rel="help" href="https://html.spec.whatwg.org/multipage/interaction.html#autocapitalization">
<body>
<script src="../../../../resources/testharness.js"></script>
<script src="../../../../resources/testharnessreport.js"></script>
<script>
test(function() {
assert_true('autocapitalize' in document.createElement('input'));
}, "Test that the autocapitalize is available on HTMLInputElement.")
test(function() {
assert_true('autocapitalize' in document.createElement('textarea'));
}, "Test that the autocapitalize is available on HTMLTextAreaElement.")
test(function() {
assert_true('autocapitalize' in document.createElement('div'));
}, "Test that the autocapitalize is available on div.")
test(function() {
var elements = [ document.createElement('input'),
document.createElement('textarea'),
document.createElement('div') ];
elements.forEach(function(e) {
e.autocapitalize = 'on';
assert_equals(e.autocapitalize, 'sentences');
e.autocapitalize = 'off';
assert_equals(e.autocapitalize, 'none');
});
}, "Test deprecated values of autocapitalize.");
test(function() {
var elements = [ document.createElement('input'),
document.createElement('textarea'),
document.createElement('div') ];
var knownValues = [ 'none', 'characters', 'words', 'sentences' ];
elements.forEach(function(e) {
// Default value.
assert_equals(e.autocapitalize, '');
// Empty value.
e.autocapitalize = '';
assert_equals(e.autocapitalize, '');
assert_equals(e.getAttribute('autocapitalize'), '');
e.setAttribute('autocapitalize', '');
assert_equals(e.autocapitalize, '');
assert_equals(e.getAttribute('autocapitalize'), '');
assert_equals(e.autocapitalize, '');
// Invalid value.
e.autocapitalize = 'foo';
assert_equals(e.autocapitalize, 'sentences');
assert_equals(e.getAttribute('autocapitalize'), 'foo');
e.setAttribute('autocapitalize', 'bar');
assert_equals(e.autocapitalize, 'sentences');
assert_equals(e.getAttribute('autocapitalize'), 'bar');
// Default value.
e.removeAttribute('autocapitalize');
assert_equals(e.autocapitalize, '');
assert_equals(e.getAttribute('autocapitalize'), null);
// Case insensitive.
e.setAttribute('autocapitalize', 'NoNe');
assert_equals(e.autocapitalize, 'none');
assert_equals(e.getAttribute('autocapitalize'), 'NoNe');
e.autocapitalize = 'WORDS';
assert_equals(e.autocapitalize, 'words');
assert_equals(e.getAttribute('autocapitalize'), 'WORDS');
knownValues.forEach(function(value) {
e.setAttribute('autocapitalize', value);
assert_equals(e.autocapitalize, value);
assert_equals(e.getAttribute('autocapitalize'), value);
e.removeAttribute('autocapitalize');
e.autocapitalize = value;
assert_equals(e.autocapitalize, value);
assert_equals(e.getAttribute('autocapitalize'), value);
e.removeAttribute('autocapitalize');
});
});
}, "Test reflection of autocapitalize.");
test(function() {
var testData = [ 'text',
'search',
'email',
'url',
'tel',
'number',
'date',
'color',
'password' ];
testData.forEach(function(data) {
const input = document.createElement('input');
input.type = data;
assert_equals(input.autocapitalize, '');
// Verify that wrapping the input element in a form doesn't change the
// defaults.
const form = document.createElement('form');
form.appendChild(input);
assert_equals(input.autocapitalize, '');
});
}, "Test that the IDL attribute returns the empty string if the content "
+ "attribute is not set.")
test(function() {
const testData = [
{
formValue: null,
formElementValue: null,
inheritedResult: '',
uninheritedResult: '',
},
{
formValue: null,
formElementValue: '',
inheritedResult: '',
uninheritedResult: '',
},
{
formValue: null,
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: null,
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: null,
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: null,
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: null,
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: null,
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: null,
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: '',
formElementValue: null,
inheritedResult: '',
uninheritedResult: '',
},
{
formValue: '',
formElementValue: '',
inheritedResult: '',
uninheritedResult: '',
},
{
formValue: '',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: '',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: '',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: '',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: '',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: '',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: '',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'on',
formElementValue: null,
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'on',
formElementValue: '',
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'on',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'on',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'on',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'on',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'on',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'on',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'on',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'off',
formElementValue: null,
inheritedResult: 'none',
uninheritedResult: '',
},
{
formValue: 'off',
formElementValue: '',
inheritedResult: 'none',
uninheritedResult: '',
},
{
formValue: 'off',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'off',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'off',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'off',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'off',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'off',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'off',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'none',
formElementValue: null,
inheritedResult: 'none',
uninheritedResult: '',
},
{
formValue: 'none',
formElementValue: '',
inheritedResult: 'none',
uninheritedResult: '',
},
{
formValue: 'none',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'none',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'none',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'none',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'none',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'none',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'none',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'characters',
formElementValue: null,
inheritedResult: 'characters',
uninheritedResult: '',
},
{
formValue: 'characters',
formElementValue: '',
inheritedResult: 'characters',
uninheritedResult: '',
},
{
formValue: 'characters',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'characters',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'characters',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'characters',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'characters',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'characters',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'characters',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'words',
formElementValue: null,
inheritedResult: 'words',
uninheritedResult: '',
},
{
formValue: 'words',
formElementValue: '',
inheritedResult: 'words',
uninheritedResult: '',
},
{
formValue: 'words',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'words',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'words',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'words',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'words',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'words',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'words',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'sentences',
formElementValue: null,
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'sentences',
formElementValue: '',
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'sentences',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'sentences',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'sentences',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'sentences',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'sentences',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'sentences',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'sentences',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'foo',
formElementValue: null,
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'foo',
formElementValue: '',
inheritedResult: 'sentences',
uninheritedResult: '',
},
{
formValue: 'foo',
formElementValue: 'on',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'foo',
formElementValue: 'off',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'foo',
formElementValue: 'none',
inheritedResult: 'none',
uninheritedResult: 'none',
},
{
formValue: 'foo',
formElementValue: 'characters',
inheritedResult: 'characters',
uninheritedResult: 'characters',
},
{
formValue: 'foo',
formElementValue: 'words',
inheritedResult: 'words',
uninheritedResult: 'words',
},
{
formValue: 'foo',
formElementValue: 'sentences',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
{
formValue: 'foo',
formElementValue: 'foo',
inheritedResult: 'sentences',
uninheritedResult: 'sentences',
},
];
const formElements = [
{element: 'button', inherits: true},
{element: 'fieldset', inherits: true},
{element: 'img', inherits: false},
{element: 'input', inherits: true},
{element: 'object', inherits: false},
{element: 'output', inherits: true},
{element: 'select', inherits: true},
{element: 'textarea', inherits: true},
];
const form = document.createElement('form');
form.id = 'form';
document.body.appendChild(form);
testData.forEach(data => {
form.removeAttribute('autocapitalize');
if (data.formValue !== null) {
form.setAttribute('autocapitalize', data.formValue);
}
formElements.forEach(elementData => {
const element = document.createElement(elementData.element);
form.appendChild(element);
const element2 = document.createElement(elementData.element);
element2.setAttribute('form', 'form');
document.body.appendChild(element2);
if (data.formElementValue !== null) {
element.setAttribute('autocapitalize', data.formElementValue);
element2.setAttribute('autocapitalize', data.formElementValue);
}
const descriptionSuffix = 'with "' + data.formValue
+ '" and form element with "'+ data.formElementValue + '"';
if (elementData.inherits) {
assert_equals(element.autocapitalize, data.inheritedResult,
`${elementData.element} element with form parent `
+ `${descriptionSuffix}`);
assert_equals(element2.autocapitalize, data.inheritedResult,
`${elementData.element} element with form owner attribute`
+ ` set ${descriptionSuffix}`);
} else {
assert_equals(element.autocapitalize, data.uninheritedResult,
`${elementData.element} element with form parent `
+ `${descriptionSuffix}`);
assert_equals(element2.autocapitalize, data.uninheritedResult,
`${elementData.element} element with form owner attribute`
+ `set ${descriptionSuffix}`);
}
});
});
}, "Test inheriting values from a form.")
test(function() {
const testData = [ 'text',
'search',
'email',
'url',
'tel',
'number',
'date',
'color',
'password' ];
testData.forEach(function(data) {
const form = document.createElement('form');
form.setAttribute('autocapitalize', 'sentences');
const input = document.createElement('input');
input.setAttribute('type', data);
form.appendChild(input);
assert_equals(input.autocapitalize, 'sentences');
});
}, "Verify that even input types that are never autocapitalized support the "
+ "IDL interface.")
</script>
</body>
</html>