Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Comment thread
radium-v marked this conversation as resolved.
"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"
}
4 changes: 2 additions & 2 deletions packages/web-components/src/text-input/text-input.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Comment thread
radium-v marked this conversation as resolved.
toView: value => (value === null || value === undefined ? null : value.toString()),
},
})
public spellcheck!: boolean;
Expand Down
98 changes: 93 additions & 5 deletions packages/web-components/src/text-input/text-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */ `
<div spellcheck="false">
<${tagName}></${tagName}>
</div>
`);

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 */ `
<div spellcheck="false">
<${tagName} spellcheck="true"></${tagName}>
</div>
`);

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 }) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/web-components/src/textarea/textarea.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export class BaseTextArea extends FASTElement {
* @internal
*/
protected controlElChanged() {
this.controlEl.spellcheck = this.spellcheck;

this.controlElAttrObserver = new MutationObserver(() => {
this.setValidity();
});
Expand Down Expand Up @@ -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;

/**
Expand Down
101 changes: 99 additions & 2 deletions packages/web-components/src/textarea/textarea.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ test.describe('TextArea', () => {
required: true,
disabled: true,
readonly: true,
spellcheck: true,
autocomplete: 'on',
maxlength: '100',
minlength: '10',
Expand All @@ -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 */ `
<div spellcheck="false">
<${tagName}></${tagName}>
</div>
`);

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 */ `
<div spellcheck="false">
<${tagName} spellcheck="true"></${tagName}>
</div>
`);

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;

Expand Down
2 changes: 1 addition & 1 deletion packages/web-components/src/textarea/textarea.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const storyTemplate = html<StoryArgs<FluentTextArea>>`
?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}"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
?required="{{required}}"
?disabled="{{disabled}}"
?readonly="{{readOnly}}"
?spellcheck="{{spellcheck}}"
spellcheck="{{spellcheck}}"
autocomplete="{{autocomplete}}"
maxlength="{{maxLength}}"
minlength="{{minLength}}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function textAreaTemplate<T extends TextArea>(): ElementViewTemplate<T> {
?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}"
Expand Down
Loading