diff --git a/change/@fluentui-web-components-7441a75d-cb13-41f1-a772-07b6ea6d14d4.json b/change/@fluentui-web-components-7441a75d-cb13-41f1-a772-07b6ea6d14d4.json
new file mode 100644
index 00000000000000..e3d0387fd61263
--- /dev/null
+++ b/change/@fluentui-web-components-7441a75d-cb13-41f1-a772-07b6ea6d14d4.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Fix `spellcheck` conversion and propagation for TextInput and TextArea.",
+ "packageName": "@fluentui/web-components",
+ "email": "863023+radium-v@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/web-components/src/text-input/text-input.base.ts b/packages/web-components/src/text-input/text-input.base.ts
index 4a2f08be10eb9c..b68106116930ab 100644
--- a/packages/web-components/src/text-input/text-input.base.ts
+++ b/packages/web-components/src/text-input/text-input.base.ts
@@ -270,8 +270,8 @@ export class BaseTextInput extends FASTElement {
*/
@attr({
converter: {
- fromView: value => (typeof value === 'string' ? ['true', ''].includes(value.trim().toLowerCase()) : null),
- toView: value => value.toString(),
+ fromView: value => (typeof value === 'string' ? ['true', ''].includes(value.trim().toLowerCase()) : value),
+ toView: value => (value === null || value === undefined ? null : value.toString()),
},
})
public spellcheck!: boolean;
diff --git a/packages/web-components/src/text-input/text-input.spec.ts b/packages/web-components/src/text-input/text-input.spec.ts
index 190f4966a94651..e72265b92caaf4 100644
--- a/packages/web-components/src/text-input/text-input.spec.ts
+++ b/packages/web-components/src/text-input/text-input.spec.ts
@@ -61,13 +61,101 @@ test.describe('TextInput', () => {
await expect(control).toHaveAttribute('required');
});
- test('should set the `spellcheck` attribute on the internal control', async ({ fastPage }) => {
- const { element } = fastPage;
- const control = element.locator('input');
+ test.describe('spellcheck', () => {
+ const attributeCases = [
+ { value: 'true', expected: true },
+ { value: 'false', expected: false },
+ { value: '', expected: true },
+ { value: 'anything', expected: false },
+ ] as const;
+
+ for (const { value, expected } of attributeCases) {
+ test(`should propagate \`spellcheck="${value}"\` to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName}>${tagName}>
+
+ `);
+
+ await element.evaluate((node, attributeValue) => {
+ node.setAttribute('spellcheck', attributeValue);
+ }, value);
+
+ await expect(element).toHaveAttribute('spellcheck', value);
+ await expect(element).toHaveJSProperty('spellcheck', expected);
+ await expect(control).toHaveAttribute('spellcheck', expected.toString());
+ await expect(control).toHaveJSProperty('spellcheck', expected);
+ });
+ }
+
+ for (const spellcheck of [true, false]) {
+ test(`should propagate a ${spellcheck} \`spellcheck\` property to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
- await fastPage.setTemplate({ attributes: { spellcheck: 'true' } });
+ await fastPage.setTemplate();
+
+ await element.evaluate((node: TextInput, value) => {
+ node.spellcheck = value;
+ }, spellcheck);
+
+ await expect(element).toHaveAttribute('spellcheck', spellcheck.toString());
+ await expect(element).toHaveJSProperty('spellcheck', spellcheck);
+ await expect(control).toHaveAttribute('spellcheck', spellcheck.toString());
+ await expect(control).toHaveJSProperty('spellcheck', spellcheck);
+ });
+ }
- await expect(control).toHaveAttribute('spellcheck', 'true');
+ for (const value of ['null', 'undefined'] as const) {
+ test(`should propagate a ${value} \`spellcheck\` property to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
+
+ await fastPage.setTemplate();
+
+ await element.evaluate((node, nullishValue) => {
+ Reflect.set(node, 'spellcheck', nullishValue === 'null' ? null : undefined);
+ }, value);
+
+ await expect(element).not.toHaveAttribute('spellcheck');
+ await expect(element).toHaveJSProperty('spellcheck', value === 'null' ? null : undefined);
+ await expect(control).not.toHaveAttribute('spellcheck');
+ });
+ }
+
+ test('should propagate removal of the host `spellcheck` attribute to the internal control', async ({
+ fastPage,
+ }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName} spellcheck="true">${tagName}>
+
+ `);
+
+ await element.evaluate(node => {
+ node.removeAttribute('spellcheck');
+ });
+
+ await expect(element).not.toHaveAttribute('spellcheck');
+ await expect(element).toHaveJSProperty('spellcheck', null);
+ await expect(control).not.toHaveAttribute('spellcheck');
+ });
+
+ test('should use the browser default when `spellcheck` is unset', async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('input');
+
+ await fastPage.setTemplate();
+
+ await expect(element).not.toHaveAttribute('spellcheck');
+ await expect(control).not.toHaveAttribute('spellcheck');
+ });
});
test('should set the `maxlength` attribute on the internal control', async ({ fastPage }) => {
diff --git a/packages/web-components/src/textarea/textarea.base.ts b/packages/web-components/src/textarea/textarea.base.ts
index e0cbb42deb7e7c..0e264e415d58bc 100644
--- a/packages/web-components/src/textarea/textarea.base.ts
+++ b/packages/web-components/src/textarea/textarea.base.ts
@@ -61,6 +61,8 @@ export class BaseTextArea extends FASTElement {
* @internal
*/
protected controlElChanged() {
+ this.controlEl.spellcheck = this.spellcheck;
+
this.controlElAttrObserver = new MutationObserver(() => {
this.setValidity();
});
@@ -316,7 +318,12 @@ export class BaseTextArea extends FASTElement {
* @remarks
* HTML Attribute: `spellcheck`
*/
- @attr({ mode: 'boolean' })
+ @attr({
+ converter: {
+ fromView: value => (typeof value === 'string' ? ['true', ''].includes(value.trim().toLowerCase()) : value),
+ toView: value => (value === null || value === undefined ? null : value.toString()),
+ },
+ })
public spellcheck = false;
/**
diff --git a/packages/web-components/src/textarea/textarea.spec.ts b/packages/web-components/src/textarea/textarea.spec.ts
index 30e41542c091bb..e92a9de7879824 100644
--- a/packages/web-components/src/textarea/textarea.spec.ts
+++ b/packages/web-components/src/textarea/textarea.spec.ts
@@ -51,7 +51,6 @@ test.describe('TextArea', () => {
required: true,
disabled: true,
readonly: true,
- spellcheck: true,
autocomplete: 'on',
maxlength: '100',
minlength: '10',
@@ -62,13 +61,111 @@ test.describe('TextArea', () => {
await expect(control).toHaveJSProperty('required', true);
await expect(control).toHaveJSProperty('disabled', true);
await expect(control).toHaveJSProperty('readOnly', true);
- await expect(control).toHaveJSProperty('spellcheck', true);
await expect(control).toHaveJSProperty('autocomplete', 'on');
await expect(control).toHaveJSProperty('maxLength', 100);
await expect(control).toHaveJSProperty('minLength', 10);
await expect(control).toHaveJSProperty('placeholder', 'Placeholder');
});
+ test.describe('spellcheck', () => {
+ const attributeCases = [
+ { value: 'true', expected: true },
+ { value: 'false', expected: false },
+ { value: '', expected: true },
+ { value: 'anything', expected: false },
+ ] as const;
+
+ for (const { value, expected } of attributeCases) {
+ test(`should propagate \`spellcheck="${value}"\` to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('textarea');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName}>${tagName}>
+
+ `);
+
+ await element.evaluate((node, attributeValue) => {
+ node.setAttribute('spellcheck', attributeValue);
+ }, value);
+
+ await expect(element).toHaveAttribute('spellcheck', value);
+ await expect(element).toHaveJSProperty('spellcheck', expected);
+ await expect(control).toHaveAttribute('spellcheck', expected.toString());
+ await expect(control).toHaveJSProperty('spellcheck', expected);
+ });
+ }
+
+ for (const spellcheck of [true, false]) {
+ test(`should propagate a ${spellcheck} \`spellcheck\` property to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('textarea');
+
+ await fastPage.setTemplate();
+
+ await element.evaluate((node: HTMLElement, value) => {
+ node.spellcheck = value;
+ }, spellcheck);
+
+ await expect(element).toHaveAttribute('spellcheck', spellcheck.toString());
+ await expect(element).toHaveJSProperty('spellcheck', spellcheck);
+ await expect(control).toHaveAttribute('spellcheck', spellcheck.toString());
+ await expect(control).toHaveJSProperty('spellcheck', spellcheck);
+ });
+ }
+
+ for (const value of ['null', 'undefined'] as const) {
+ test(`should propagate a ${value} \`spellcheck\` property to the internal control`, async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('textarea');
+
+ await fastPage.setTemplate();
+
+ await element.evaluate((node, nullishValue) => {
+ Reflect.set(node, 'spellcheck', nullishValue === 'null' ? null : undefined);
+ }, value);
+
+ await expect(element).not.toHaveAttribute('spellcheck');
+ await expect(element).toHaveJSProperty('spellcheck', value === 'null' ? null : undefined);
+ await expect(control).not.toHaveAttribute('spellcheck');
+ });
+ }
+
+ test('should propagate removal of the host `spellcheck` attribute to the internal control', async ({
+ fastPage,
+ }) => {
+ const { element } = fastPage;
+ const control = element.locator('textarea');
+
+ await fastPage.setTemplate(/* html */ `
+
+ <${tagName} spellcheck="true">${tagName}>
+
+ `);
+
+ await element.evaluate(node => {
+ node.removeAttribute('spellcheck');
+ });
+
+ await expect(element).not.toHaveAttribute('spellcheck');
+ await expect(element).toHaveJSProperty('spellcheck', null);
+ await expect(control).not.toHaveAttribute('spellcheck');
+ });
+
+ test('should use the browser default when `spellcheck` is unset', async ({ fastPage }) => {
+ const { element } = fastPage;
+ const control = element.locator('textarea');
+
+ await fastPage.setTemplate();
+
+ await expect(element).toHaveAttribute('spellcheck', 'false');
+ await expect(element).toHaveJSProperty('spellcheck', false);
+ await expect(control).toHaveAttribute('spellcheck', 'false');
+ await expect(control).toHaveJSProperty('spellcheck', false);
+ });
+ });
+
test('should be associated with the given external labels', async ({ fastPage, page }) => {
const { element } = fastPage;
diff --git a/packages/web-components/src/textarea/textarea.stories.ts b/packages/web-components/src/textarea/textarea.stories.ts
index ab90fef6d4a1a2..adfb5085a7d721 100644
--- a/packages/web-components/src/textarea/textarea.stories.ts
+++ b/packages/web-components/src/textarea/textarea.stories.ts
@@ -25,7 +25,7 @@ const storyTemplate = html>`
?readonly="${x => x.readOnly}"
?required="${x => x.required}"
size="${x => x.size}"
- ?spellcheck="${x => x.spellcheck}"
+ spellcheck="${x => x.spellcheck}"
resize="${x => x.resize}"
value="${x => x.value}"
>
diff --git a/packages/web-components/src/textarea/textarea.template.html b/packages/web-components/src/textarea/textarea.template.html
index 83216b4cecdf6e..5c8f55c73d546e 100644
--- a/packages/web-components/src/textarea/textarea.template.html
+++ b/packages/web-components/src/textarea/textarea.template.html
@@ -13,7 +13,7 @@
?required="{{required}}"
?disabled="{{disabled}}"
?readonly="{{readOnly}}"
- ?spellcheck="{{spellcheck}}"
+ spellcheck="{{spellcheck}}"
autocomplete="{{autocomplete}}"
maxlength="{{maxLength}}"
minlength="{{minLength}}"
diff --git a/packages/web-components/src/textarea/textarea.template.ts b/packages/web-components/src/textarea/textarea.template.ts
index f8bf158c9912cc..cc97b3a038aa14 100644
--- a/packages/web-components/src/textarea/textarea.template.ts
+++ b/packages/web-components/src/textarea/textarea.template.ts
@@ -21,7 +21,7 @@ export function textAreaTemplate(): ElementViewTemplate {
?required="${x => x.required}"
?disabled="${x => x.disabled}"
?readonly="${x => x.readOnly}"
- ?spellcheck="${x => x.spellcheck}"
+ spellcheck="${x => x.spellcheck}"
autocomplete="${x => x.autocomplete}"
maxlength="${x => x.maxLength}"
minlength="${x => x.minLength}"