${this.renderPrefix()}
${this.renderInput()}
@@ -200,20 +197,16 @@ export abstract class IgcInputBaseComponent extends FormAssociatedRequiredMixin(
${this.renderSuffix()}
-
+
${this.renderPrefix()} ${this.renderInput()} ${this.renderSuffix()}
-
-
-
`;
+ ${this.renderValidatorContainer()}`;
}
protected override render() {
diff --git a/src/components/input/input.spec.ts b/src/components/input/input.spec.ts
index 2fb37726d..b692f712b 100644
--- a/src/components/input/input.spec.ts
+++ b/src/components/input/input.spec.ts
@@ -1,8 +1,20 @@
-import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
+import {
+ elementUpdated,
+ expect,
+ fixture,
+ html,
+ nextFrame,
+} from '@open-wc/testing';
import { spy } from 'sinon';
+import type { TemplateResult } from 'lit';
+import { configureTheme } from '../../theming/config.js';
import { defineComponents } from '../common/definitions/defineComponents.js';
-import { FormAssociatedTestBed, simulateInput } from '../common/utils.spec.js';
+import {
+ FormAssociatedTestBed,
+ checkValidationSlots,
+ simulateInput,
+} from '../common/utils.spec.js';
import IgcInputComponent from './input.js';
describe('Input component', () => {
@@ -10,269 +22,361 @@ describe('Input component', () => {
defineComponents(IgcInputComponent);
});
- let el: IgcInputComponent;
+ let element: IgcInputComponent;
let input: HTMLInputElement;
+ async function createFixture(template: TemplateResult) {
+ element = await fixture
(template);
+ input = element.renderRoot.querySelector('input')!;
+ }
+
describe('', () => {
- beforeEach(async () => {
- el = await fixture(html``);
- input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
- });
+ describe('Default state', () => {
+ it('is initialized with the proper default values', async () => {
+ await createFixture(html``);
- it('is initialized with the proper default values', async () => {
- expect(el.size).to.equal('medium');
- expect(el.type).to.equal('text');
- expect(el.value).to.be.empty;
- expect(el.invalid).to.be.false;
- expect(el.required).to.be.false;
- expect(el.readonly).to.be.false;
- expect(el.disabled).to.be.false;
- expect(el.name).to.be.undefined;
- expect(el.pattern).to.be.undefined;
- expect(el.label).to.be.undefined;
- expect(el.autocomplete).to.be.undefined;
- });
+ expect(element.size).to.equal('medium');
+ expect(element.type).to.equal('text');
+ expect(element.value).to.be.empty;
+ expect(element.invalid).to.be.false;
+ expect(element.required).to.be.false;
+ expect(element.readonly).to.be.false;
+ expect(element.disabled).to.be.false;
+ expect(element.name).to.be.undefined;
+ expect(element.pattern).to.be.undefined;
+ expect(element.label).to.be.undefined;
+ expect(element.autocomplete).to.be.undefined;
+ });
+
+ it('is accessible', async () => {
+ await createFixture(html``);
+
+ await expect(element).to.be.accessible();
+ await expect(element).shadowDom.to.be.accessible();
+ });
- it('sets the type property successfully', async () => {
- const type = 'email';
+ it('material variant layout', async () => {
+ configureTheme('material');
+ await createFixture(html``);
- el.type = type;
- expect(el.type).to.equal(type);
- await elementUpdated(el);
- expect(input.type).to.equal(type);
+ expect(element.renderRoot.querySelector('[part="notch"]')).to.exist;
+
+ // Reset theme
+ configureTheme('bootstrap');
+ await nextFrame();
+ });
});
- it('sets the value property successfully', async () => {
- const value1 = 'value1';
- const value2 = 'value2';
+ describe('Properties', () => {
+ it('sets the type property', async () => {
+ await createFixture(html``);
- const el = await fixture(
- html``
- );
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
+ expect(element.type).to.equal('email');
+ expect(input.type).to.equal('email');
- expect(el.value).to.equal(value1);
- await elementUpdated(el);
- expect(input.value).to.equal(value1);
+ element.type = 'search';
+ await elementUpdated(element);
- el.value = value2;
- expect(el.value).to.equal(value2);
- await elementUpdated(el);
- expect(input.value).to.equal(value2);
- });
+ expect(element.type).to.equal('search');
+ expect(input.type).to.equal('search');
+ });
- it('sets the name property successfully', async () => {
- const name = 'fruit';
+ it('sets the disabled property', async () => {
+ await createFixture(html``);
- const el = await fixture(
- html``
- );
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
+ expect(element.disabled).to.be.true;
+ expect(input.disabled).to.be.true;
- expect(el.name).to.equal(name);
- await elementUpdated(el);
- expect(input.name).to.equal(name);
- });
+ element.disabled = false;
+ await elementUpdated(element);
- it('sets the placeholder property successfully', async () => {
- const placeholder = 'fruit';
+ expect(element.disabled).to.be.false;
+ expect(input.disabled).to.be.false;
+ });
- const el = await fixture(
- html``
- );
- const input = el.shadowRoot?.querySelector('input') as HTMLInputElement;
+ it('sets the label property', async () => {
+ await createFixture(html``);
- expect(el.placeholder).to.equal(placeholder);
- await elementUpdated(el);
- expect(input.placeholder).to.equal(placeholder);
- });
+ expect(element.label).to.equal('Label');
+ expect(input.labels?.item(0).textContent?.trim()).to.equal('Label');
- it('sets the label property successfully', async () => {
- const text = 'Label';
- const el = await fixture(
- html``
- );
- const label = el.shadowRoot?.querySelector('label') as HTMLLabelElement;
- expect(el.label).to.equal(text);
- expect(label.innerText).to.equal(text);
- });
+ element.label = 'Changed';
+ await elementUpdated(element);
- it('sets the min and max properties successfully', async () => {
- el.type = 'number';
- el.min = '5';
- el.max = '10';
+ expect(element.label).to.equal('Changed');
+ expect(input.labels?.item(0).textContent?.trim()).to.equal('Changed');
+ });
- await elementUpdated(el);
- expect(input.min).to.equal(el.min);
- expect(input.max).to.equal(el.max);
- });
+ it('sets the name property', async () => {
+ await createFixture(html``);
- it('sets the minlength and maxlength properties successfully', async () => {
- el.type = 'number';
- el.minLength = 5;
- el.maxLength = 20;
+ expect(element.name).to.equal('input');
+ expect(input.name).to.equal('input');
- await elementUpdated(el);
- expect(input.minLength).to.equal(el.minLength);
- expect(input.maxLength).to.equal(el.maxLength);
- });
+ element.name = 'tupni';
+ await elementUpdated(element);
- it('sets the pattern property successfully', async () => {
- expect(input.pattern).to.be.empty;
- el.pattern = '123';
+ expect(element.name).to.equal('tupni');
+ expect(input.name).to.equal('tupni');
+ });
- await elementUpdated(el);
- expect(input.pattern).to.equal(el.pattern);
- });
+ it('sets the placeholder property', async () => {
+ await createFixture(
+ html``
+ );
- it('sets the required property successfully', async () => {
- el.required = true;
- expect(el.required).to.be.true;
- await elementUpdated(el);
- expect(input.required).to.be.true;
+ expect(element.placeholder).to.equal('placeholder');
+ expect(input.placeholder).to.equal('placeholder');
- el.required = false;
- expect(el.required).to.be.false;
- await elementUpdated(el);
- expect(input.required).to.be.false;
- });
+ element.placeholder = 'another';
+ await elementUpdated(element);
- it('sets the readonly property successfully', async () => {
- el.readonly = true;
- expect(el.readonly).to.be.true;
- await elementUpdated(el);
- expect(input.readOnly).to.be.true;
+ expect(element.placeholder).to.equal('another');
+ expect(input.placeholder).to.equal('another');
+ });
- el.readonly = false;
- expect(el.readonly).to.be.false;
- await elementUpdated(el);
- expect(input.readOnly).to.be.false;
- });
+ it('sets the min and max properties', async () => {
+ await createFixture(
+ html``
+ );
- it('sets the autofocus property successfully', async () => {
- el.autofocus = true;
- expect(el.autofocus).to.be.true;
- await elementUpdated(el);
- expect((input as any).autofocus).to.be.true;
+ expect(element.min).to.equal('3');
+ expect(element.max).to.equal('6');
+ expect(input.min).to.equal('3');
+ expect(input.max).to.equal('6');
- el.autofocus = false;
- expect(el.autofocus).to.be.false;
- await elementUpdated(el);
- expect((input as any).autofocus).to.be.false;
- });
+ expect(element.checkValidity()).to.be.false;
- it('sets the autocomplete property successfully', async () => {
- el.autocomplete = 'email';
- await elementUpdated(el);
- expect(input.autocomplete).to.equal(el.autocomplete);
- });
+ Object.assign(element, { min: '1', max: '2' });
+ await elementUpdated(element);
- it('sets the disabled property successfully', async () => {
- el.disabled = true;
- expect(el.disabled).to.be.true;
- await elementUpdated(el);
- expect(input.disabled).to.be.true;
+ expect(element.min).to.equal('1');
+ expect(element.max).to.equal('2');
+ expect(input.min).to.equal('1');
+ expect(input.max).to.equal('2');
- el.disabled = false;
- expect(el.disabled).to.be.false;
- await elementUpdated(el);
- expect(input.disabled).to.be.false;
- });
+ expect(element.checkValidity()).to.be.false;
+ });
- it('changes size property values successfully', async () => {
- el.size = 'medium';
- expect(el.size).to.equal('medium');
- await elementUpdated(el);
+ it('sets the minLength and maxLength properties', async () => {
+ await createFixture(
+ html``
+ );
- el.size = 'small';
- expect(el.size).to.equal('small');
- await elementUpdated(el);
+ expect(element.minLength).to.equal(2);
+ expect(element.maxLength).to.equal(4);
+ expect(input.minLength).to.equal(2);
+ expect(input.maxLength).to.equal(4);
- el.size = 'large';
- expect(el.size).to.equal('large');
- await elementUpdated(el);
- });
+ expect(element.checkValidity()).to.be.false;
- it('should increment/decrement the value by calling the stepUp and stepDown methods', async () => {
- el.type = 'number';
- el.value = '10';
- el.step = 5;
- await elementUpdated(el);
+ Object.assign(element, { minLength: 1, maxLength: 2 });
+ await elementUpdated(element);
- el.stepUp();
- expect(el.value).to.equal('15');
- el.stepDown();
- expect(el.value).to.equal('10');
+ expect(element.minLength).to.equal(1);
+ expect(element.maxLength).to.equal(2);
+ expect(input.minLength).to.equal(1);
+ expect(input.maxLength).to.equal(2);
- el.stepUp(2);
- expect(el.value).to.equal('20');
- el.stepDown(2);
- expect(el.value).to.equal('10');
- });
+ expect(element.checkValidity()).to.be.false;
+ });
- it('should set text within selection range', async () => {
- el.type = 'text';
- el.value = 'the quick brown fox';
- await elementUpdated(el);
+ it('sets the pattern property', async () => {
+ await createFixture(html``);
- el.setRangeText('slow', 4, 9, 'select');
- expect(el.value).to.equal('the slow brown fox');
- });
+ expect(element.pattern).to.equal('d{3}');
+ expect(input.pattern).to.equal('d{3}');
+
+ expect(element.checkValidity()).to.be.false;
+
+ element.pattern = '';
+ await elementUpdated(element);
+
+ expect(element.pattern).to.be.empty;
+ expect(input.pattern).to.be.empty;
+
+ expect(element.checkValidity()).to.be.true;
+ });
+
+ it('sets the required property', async () => {
+ await createFixture(html``);
- it('should focus/blur the wrapped base element when the methods are called', () => {
- const eventSpy = spy(el, 'emitEvent');
- el.focus();
+ expect(element.required).to.be.true;
+ expect(input.required).to.be.true;
+ expect(element.checkValidity()).to.be.false;
- expect(el.shadowRoot?.activeElement).to.equal(input);
- expect(eventSpy).calledOnceWithExactly('igcFocus');
+ element.required = false;
+ await elementUpdated(element);
- el.blur();
+ expect(element.required).to.be.false;
+ expect(input.required).to.be.false;
+ expect(element.checkValidity()).to.be.true;
+ });
- expect(el.shadowRoot?.activeElement).to.be.null;
- expect(eventSpy).calledTwice;
- expect(eventSpy).calledWithExactly('igcBlur');
+ it('sets the value property', async () => {
+ await createFixture(html``);
+
+ expect(element.value).to.equal('123');
+ expect(input.value).to.equal('123');
+
+ element.value = '';
+ await elementUpdated(element);
+
+ expect(element.value).to.be.empty;
+ expect(input.value).to.be.empty;
+ });
+
+ it('issue #1026 - passing undefined sets the underlying input value to undefined', async () => {
+ await createFixture(html``);
+
+ expect(element.value).to.equal('a');
+ expect(input.value).to.equal('a');
+
+ element.value = undefined as any;
+ await elementUpdated(element);
+
+ expect(element.value).to.be.empty;
+ expect(input.value).to.be.empty;
+ });
});
- it('should emit focus/blur events when methods are called', () => {
- const eventSpy = spy(el, 'emitEvent');
- el.focus();
+ describe('Methods', () => {
+ it('should increment/decrement value by calling stepUp/stepDown', async () => {
+ await createFixture(
+ html``
+ );
+
+ element.stepUp();
+ expect(element.value).to.equal('15');
+
+ element.stepDown();
+ expect(element.value).to.equal('10');
+
+ element.stepUp(2);
+ expect(element.value).to.equal('20');
- expect(el.shadowRoot?.activeElement).to.equal(input);
- expect(eventSpy).calledOnceWithExactly('igcFocus');
+ element.stepDown(2);
+ expect(element.value).to.equal('10');
+ });
- el.blur();
+ it('setRangeText()', async () => {
+ await createFixture(
+ html``
+ );
- expect(el.shadowRoot?.activeElement).to.be.null;
- expect(eventSpy).calledTwice;
- expect(eventSpy).calledWithExactly('igcBlur');
+ element.setRangeText('slow', 4, 9, 'select');
+ expect(element.value).to.equal('the slow brown fox');
+ });
+
+ it('focus() and blur()', async () => {
+ await createFixture(html``);
+
+ element.focus();
+ expect(element.matches(':focus')).to.be.true;
+ expect(input.matches(':focus')).to.be.true;
+
+ element.blur();
+ expect(element.matches(':focus')).to.be.false;
+ expect(input.matches(':focus')).to.be.false;
+ });
});
- it('issue #1026 - passing undefined sets the underlying input value to undefined', async () => {
- el.value = 'a';
- await elementUpdated(el);
+ describe('Events', () => {
+ beforeEach(async () => {
+ await createFixture(html``);
+ });
+
+ it('emits igcFocus on focus', async () => {
+ const eventSpy = spy(element, 'emitEvent');
+
+ element.focus();
+ expect(eventSpy).calledWithExactly('igcFocus');
+ });
+
+ it('emits igcBlur on blur', async () => {
+ element.focus();
+ const eventSpy = spy(element, 'emitEvent');
+
+ element.blur();
+ expect(eventSpy).calledOnceWithExactly('igcBlur');
+ });
+
+ it('emits igcInput', async () => {
+ const eventSpy = spy(element, 'emitEvent');
+
+ simulateInput(input, { value: '123' });
+ await elementUpdated(element);
+
+ expect(eventSpy).calledOnceWithExactly('igcInput', { detail: '123' });
+ });
- expect(el.value).to.equal('a');
- expect(input.value).to.equal('a');
+ it('emits igcChange', async () => {
+ simulateInput(input, { value: '123' });
+ await elementUpdated(element);
- el.value = undefined as any;
- await elementUpdated(el);
+ const eventSpy = spy(element, 'emitEvent');
+ input.dispatchEvent(new Event('change'));
- expect(el.value).to.be.empty;
- expect(input.value).to.be.empty;
+ expect(eventSpy).calledOnceWithExactly('igcChange', { detail: '123' });
+ });
});
});
- it('should reflect validation state when updating through attribute', async () => {
- el = await fixture(
- html``
+ describe('issue-1066', () => {
+ const _expectedValidation = Symbol();
+ type TestBedInput = IgcInputComponent & { [_expectedValidation]: boolean };
+
+ function validateInput(event: CustomEvent) {
+ const element = event.target as TestBedInput;
+ expect(element.checkValidity()).to.equal(element[_expectedValidation]);
+ }
+
+ function getInternalInput(element: IgcInputComponent) {
+ return element.shadowRoot!.querySelector('input')!;
+ }
+
+ function setExpectedValidationState(
+ state: boolean,
+ element: IgcInputComponent
+ ) {
+ (element as TestBedInput)[_expectedValidation] = state;
+ }
+
+ const spec = new FormAssociatedTestBed(
+ html``
);
- expect(el.reportValidity()).to.equal(false);
+ beforeEach(async () => {
+ await spec.setup(IgcInputComponent.tagName);
+ });
- el.value = '1';
- await elementUpdated(el);
+ it('synchronously validates component', async () => {
+ const input = getInternalInput(spec.element);
+
+ // Invalid email
+ setExpectedValidationState(false, spec.element);
+ simulateInput(input, { value: '1' });
+ await elementUpdated(spec.element);
+
+ // Invalid email
+ simulateInput(input, { value: '1@' });
+ await elementUpdated(spec.element);
+
+ // Valid email
+ setExpectedValidationState(true, spec.element);
+ simulateInput(input, { value: '1@1' });
+ await elementUpdated(spec.element);
- expect(el.reportValidity()).to.equal(true);
+ // Valid email, required invalidates
+ setExpectedValidationState(false, spec.element);
+ simulateInput(input);
+ await elementUpdated(spec.element);
+ });
});
describe('Form integration', () => {
@@ -302,8 +406,8 @@ describe('Input component', () => {
it('is correctly reset on form reset', async () => {
spec.element.value = 'abc';
-
spec.reset();
+
expect(spec.element.value).to.be.empty;
});
@@ -432,60 +536,121 @@ describe('Input component', () => {
});
});
- describe('issue-1066', () => {
- const _expectedValidation = Symbol();
- type TestBedInput = IgcInputComponent & { [_expectedValidation]: boolean };
-
- function validateInput(event: CustomEvent) {
- const element = event.target as TestBedInput;
- expect(element.checkValidity()).to.equal(element[_expectedValidation]);
+ describe('Validation message slots', () => {
+ async function createFixture(template: TemplateResult) {
+ element = await fixture(template);
}
- function getInternalInput(element: IgcInputComponent) {
- return element.shadowRoot!.querySelector('input')!;
- }
+ it('renders value-missing slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
- function setExpectedValidationState(
- state: boolean,
- element: IgcInputComponent
- ) {
- (element as TestBedInput)[_expectedValidation] = state;
- }
+ await checkValidationSlots(element, 'valueMissing');
+ });
- const spec = new FormAssociatedTestBed(
- html``
- );
+ it('renders type-mismatch slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
- beforeEach(async () => {
- await spec.setup(IgcInputComponent.tagName);
+ await checkValidationSlots(element, 'typeMismatch');
});
- it('synchronously validates component', async () => {
- const input = getInternalInput(spec.element);
+ it('renders pattern-mismatch slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
- // Invalid email
- setExpectedValidationState(false, spec.element);
- simulateInput(input, '1');
- await elementUpdated(spec.element);
+ await checkValidationSlots(element, 'patternMismatch');
+ });
- // Invalid email
- simulateInput(input, '1@');
- await elementUpdated(spec.element);
+ it('renders too-long slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
- // Valid email
- setExpectedValidationState(true, spec.element);
- simulateInput(input, '1@1');
- await elementUpdated(spec.element);
+ await checkValidationSlots(element, 'tooLong');
+ });
- // Valid email, required invalidates
- setExpectedValidationState(false, spec.element);
- simulateInput(input, '');
- await elementUpdated(spec.element);
+ it('renders too-short slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'tooShort');
+ });
+
+ it('renders range-overflow slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'rangeOverflow');
+ });
+
+ it('renders range-underflow slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'rangeUnderflow');
+ });
+
+ it('renders step-mismatch slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'stepMismatch');
+ });
+
+ it('renders custom-error slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ element.setCustomValidity('invalid');
+ await checkValidationSlots(element, 'customError');
+ });
+
+ it('renders invalid slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'invalid');
+ });
+
+ it('renders multiple validation slots', async () => {
+ await createFixture(html`
+
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'typeMismatch', 'tooShort');
});
});
});
diff --git a/src/components/input/input.ts b/src/components/input/input.ts
index 4bbff4f7c..83d359815 100644
--- a/src/components/input/input.ts
+++ b/src/components/input/input.ts
@@ -14,11 +14,11 @@ import {
minLengthValidator,
minValidator,
patternValidator,
- requiredNumberValidator,
requiredValidator,
stepValidator,
urlValidator,
} from '../common/validators.js';
+import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import { IgcInputBaseComponent } from './input-base.js';
/**
@@ -27,6 +27,16 @@ import { IgcInputBaseComponent } from './input-base.js';
* @slot prefix - Renders content before the input.
* @slot suffix - Renders content after input.
* @slot helper-text - Renders content below the input.
+ * @slot value-missing - Renders content when the required validation fails.
+ * @slot type-mismatch - Renders content when the a type url/email input pattern validation fails.
+ * @slot pattern-mismatch - Renders content when the pattern validation fails.
+ * @slot too-long - Renders content when the maxlength validation fails.
+ * @slot too-short - Renders content when the minlength validation fails.
+ * @slot range-overflow - Renders content when the max validation fails.
+ * @slot range-underflow - Renders content when the min validation fails.
+ * @slot step-mismatch - Renders content when the step validation fails.
+ * @slot custom-error - Renders content when setCustomValidity(message) is set.
+ * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
*
* @fires igcInput - Emitted when the control input receives user input.
* @fires igcChange - Emitted when the control's checked state changes.
@@ -45,7 +55,7 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
/* blazorSuppress */
public static register() {
- registerComponent(IgcInputComponent);
+ registerComponent(IgcInputComponent, IgcValidationContainerComponent);
}
private get isStringType() {
@@ -53,13 +63,7 @@ export default class IgcInputComponent extends IgcInputBaseComponent {
}
protected override validators: Validator[] = [
- {
- ...requiredValidator,
- isValid: () =>
- this.isStringType
- ? requiredValidator.isValid(this)
- : requiredNumberValidator.isValid(this),
- },
+ requiredValidator,
{
...minLengthValidator,
isValid: () =>
diff --git a/src/components/mask-input/mask-input.spec.ts b/src/components/mask-input/mask-input.spec.ts
index 6fd18a777..b9b217bd5 100644
--- a/src/components/mask-input/mask-input.spec.ts
+++ b/src/components/mask-input/mask-input.spec.ts
@@ -1,610 +1,621 @@
import { elementUpdated, expect, fixture } from '@open-wc/testing';
-import { html } from 'lit';
+import { type TemplateResult, html } from 'lit';
import { spy } from 'sinon';
-import { defineComponents } from '../../index.js';
-import { FormAssociatedTestBed } from '../common/utils.spec.js';
-import IgcFormComponent from '../form/form.js';
+import { defineComponents } from '../common/definitions/defineComponents.js';
+import {
+ FormAssociatedTestBed,
+ checkValidationSlots,
+ simulateInput,
+ simulateKeyboard,
+} from '../common/utils.spec.js';
import IgcMaskInputComponent from './mask-input.js';
import { MaskParser } from './mask-parser.js';
describe('Masked input', () => {
- before(() => defineComponents(IgcMaskInputComponent, IgcFormComponent));
+ before(() => defineComponents(IgcMaskInputComponent));
const parser = new MaskParser();
const defaultPrompt = '_';
const defaultMask = 'CCCCCCCCCC';
+ let element: IgcMaskInputComponent;
+ let input: HTMLInputElement;
+
const syncParser = () => {
- parser.mask = masked.mask;
- parser.prompt = masked.prompt;
+ parser.mask = element.mask;
+ parser.prompt = element.prompt;
};
- const input = () =>
- masked.shadowRoot!.querySelector('input') as HTMLInputElement;
-
- let masked: IgcMaskInputComponent;
describe('Generic properties', async () => {
beforeEach(async () => {
- masked = await fixture(
+ element = await fixture(
html``
);
+ input = element.renderRoot.querySelector('input')!;
});
it('sensible default values', async () => {
- expect(masked.prompt).to.equal(defaultPrompt);
- expect(masked.mask).to.equal(defaultMask);
- expect(masked.value).to.equal('');
- expect(input().placeholder).to.equal(parser.escapedMask);
+ expect(element.prompt).to.equal(defaultPrompt);
+ expect(element.mask).to.equal(defaultMask);
+ expect(element.value).to.equal('');
+ expect(input.placeholder).to.equal(parser.escapedMask);
});
it('prompt character change (no value)', async () => {
- masked.prompt = '*';
+ element.prompt = '*';
syncParser();
- await elementUpdated(masked);
- expect(input().placeholder).to.equal(parser.escapedMask);
+ await elementUpdated(element);
+ expect(input.placeholder).to.equal(parser.escapedMask);
});
it('prompt character change (value)', async () => {
- masked.value = '777';
- await elementUpdated(masked);
+ element.value = '777';
+ await elementUpdated(element);
- masked.prompt = '*';
+ element.prompt = '*';
syncParser();
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('mask change (no value)', async () => {
- masked.mask = 'CCCC';
+ element.mask = 'CCCC';
syncParser();
- await elementUpdated(masked);
- expect(input().placeholder).to.equal(parser.escapedMask);
+ await elementUpdated(element);
+ expect(input.placeholder).to.equal(parser.escapedMask);
});
it('mask change (value)', async () => {
- masked.value = '1111';
- await elementUpdated(masked);
+ element.value = '1111';
+ await elementUpdated(element);
- masked.mask = 'CC CC';
+ element.mask = 'CC CC';
syncParser();
- await elementUpdated(masked);
- expect(input().value).to.equal(parser.apply(masked.value));
+ await elementUpdated(element);
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('placeholder is updated correctly', async () => {
const placeholder = 'Enter payment info';
syncParser();
- masked.placeholder = placeholder;
- await elementUpdated(masked);
+ element.placeholder = placeholder;
+ await elementUpdated(element);
- expect(input().placeholder).to.equal(placeholder);
+ expect(input.placeholder).to.equal(placeholder);
- masked.placeholder = '';
- await elementUpdated(masked);
+ element.placeholder = '';
+ await elementUpdated(element);
- expect(input().placeholder).to.equal('');
+ expect(input.placeholder).to.equal('');
- masked.placeholder = null as any;
- await elementUpdated(masked);
+ element.placeholder = null as any;
+ await elementUpdated(element);
- expect(input().placeholder).to.equal(parser.escapedMask);
+ expect(input.placeholder).to.equal(parser.escapedMask);
});
it('empty value without literals', async () => {
- expect(masked.value).to.equal('');
+ expect(element.value).to.equal('');
});
it('empty value with literals', async () => {
- masked.valueMode = 'withFormatting';
+ element.valueMode = 'withFormatting';
syncParser();
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(masked.value).to.equal('');
+ expect(element.value).to.equal('');
});
it('empty value and readonly on focus', async () => {
- masked.readonly = true;
+ element.readonly = true;
syncParser();
- await elementUpdated(masked);
+ await elementUpdated(element);
- masked.focus();
- await elementUpdated(masked);
+ element.focus();
+ await elementUpdated(element);
- expect(input().value).to.equal('');
+ expect(input.value).to.equal('');
});
it('get value without literals', async () => {
- masked.mask = '(CC) (CC)';
- masked.value = '1234';
+ element.mask = '(CC) (CC)';
+ element.value = '1234';
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(masked.value).to.equal('1234');
+ expect(element.value).to.equal('1234');
});
it('value with literals then value without', async () => {
- masked.mask = '(CC) (CC)';
- masked.value = '1234';
- masked.valueMode = 'withFormatting';
+ element.mask = '(CC) (CC)';
+ element.value = '1234';
+ element.valueMode = 'withFormatting';
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(masked.value).to.equal('(12) (34)');
+ expect(element.value).to.equal('(12) (34)');
- masked.valueMode = 'raw';
- await elementUpdated(masked);
+ element.valueMode = 'raw';
+ await elementUpdated(element);
- expect(masked.value).to.equal('1234');
+ expect(element.value).to.equal('1234');
});
it('invalid state is correctly reflected', async () => {
- masked.required = true;
- await elementUpdated(masked);
+ element.required = true;
+ await elementUpdated(element);
- expect(masked.reportValidity()).to.be.false;
- expect(masked.invalid).to.be.true;
+ expect(element.reportValidity()).to.be.false;
+ expect(element.invalid).to.be.true;
- masked.required = false;
- await elementUpdated(masked);
+ element.required = false;
+ await elementUpdated(element);
- expect(masked.reportValidity()).to.be.true;
- expect(masked.invalid).to.be.false;
+ expect(element.reportValidity()).to.be.true;
+ expect(element.invalid).to.be.false;
// Disabled inputs are always valid
- masked.required = true;
- masked.disabled = true;
- await elementUpdated(masked);
+ element.required = true;
+ element.disabled = true;
+ await elementUpdated(element);
- expect(masked.reportValidity()).to.be.true;
- expect(masked.invalid).to.be.false;
+ expect(element.reportValidity()).to.be.true;
+ expect(element.invalid).to.be.false;
});
it('valid/invalid state based on mask pattern', async () => {
- masked.mask = '(####)';
- await elementUpdated(masked);
+ element.mask = '(####)';
+ await elementUpdated(element);
- masked.value = '111';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.false;
+ element.value = '111';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.false;
- masked.value = '2222';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.true;
+ element.value = '2222';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.true;
- masked.mask = 'CCC';
- masked.value = '';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.true;
+ element.mask = 'CCC';
+ element.value = '';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.true;
- masked.mask = 'CC &';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.true;
+ element.mask = 'CC &';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.true;
- masked.value = 'R';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.false;
+ element.value = 'R';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.false;
- masked.value = ' R';
- await elementUpdated(masked);
- expect(masked.checkValidity()).to.be.true;
+ element.value = ' R';
+ await elementUpdated(element);
+ expect(element.checkValidity()).to.be.true;
});
it('setCustomValidity', async () => {
- masked.setCustomValidity('Fill in the value');
- await elementUpdated(masked);
+ element.setCustomValidity('Fill in the value');
+ element.reportValidity();
+ await elementUpdated(element);
- expect(masked.invalid).to.be.true;
+ expect(element.invalid).to.be.true;
- masked.setCustomValidity('');
- await elementUpdated(masked);
+ element.setCustomValidity('');
+ element.reportValidity();
+ await elementUpdated(element);
- expect(masked.invalid).to.be.false;
+ expect(element.invalid).to.be.false;
});
it('setRangeText() method', async () => {
const checkSelectionRange = (start: number, end: number) =>
- expect([start, end]).to.eql([
- input().selectionStart,
- input().selectionEnd,
- ]);
+ expect([start, end]).to.eql([input.selectionStart, input.selectionEnd]);
- masked.mask = '(CC) (CC)';
- masked.value = '1111';
+ element.mask = '(CC) (CC)';
+ element.value = '1111';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
// No boundaries, from current user selection
- masked.setSelectionRange(2, 2);
- await elementUpdated(masked);
- masked.setRangeText('22'); // (12) (21)
- await elementUpdated(masked);
+ element.setSelectionRange(2, 2);
+ await elementUpdated(element);
+ element.setRangeText('22'); // (12) (21)
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
- expect(masked.value).to.equal('1221');
+ expect(input.value).to.equal(parser.apply(element.value));
+ expect(element.value).to.equal('1221');
checkSelectionRange(2, 2);
// Keep passed selection range
- masked.value = '1111';
- masked.setRangeText('22', 0, 2, 'select'); // (22) (11)
- await elementUpdated(masked);
+ element.value = '1111';
+ element.setRangeText('22', 0, 2, 'select'); // (22) (11)
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
- expect(masked.value).to.equal('2211');
+ expect(input.value).to.equal(parser.apply(element.value));
+ expect(element.value).to.equal('2211');
checkSelectionRange(0, 2);
// Collapse range to start
- masked.value = '';
- masked.setRangeText('xx', 0, 4, 'start');
- await elementUpdated(masked);
+ element.value = '';
+ element.setRangeText('xx', 0, 4, 'start');
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
- expect(masked.value).to.equal('xx');
+ expect(input.value).to.equal(parser.apply(element.value));
+ expect(element.value).to.equal('xx');
checkSelectionRange(0, 0);
// Collapse range to end
- masked.value = 'xx';
- masked.setRangeText('yy', 2, 5, 'end');
- await elementUpdated(masked);
+ element.value = 'xx';
+ element.setRangeText('yy', 2, 5, 'end');
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
- expect(masked.value).to.equal('xyy');
+ expect(input.value).to.equal(parser.apply(element.value));
+ expect(element.value).to.equal('xyy');
checkSelectionRange(5, 5);
});
it('igcChange event', async () => {
syncParser();
- const eventSpy = spy(masked, 'emitEvent');
- masked.value = 'abc';
- await elementUpdated(masked);
+ const eventSpy = spy(element, 'emitEvent');
+ element.value = 'abc';
+ await elementUpdated(element);
- input().dispatchEvent(new Event('change'));
+ input.dispatchEvent(new Event('change'));
expect(eventSpy).calledWith('igcChange', { detail: 'abc' });
});
it('igcChange event with literals', async () => {
syncParser();
- const eventSpy = spy(masked, 'emitEvent');
- masked.value = 'abc';
- masked.valueMode = 'withFormatting';
- await elementUpdated(masked);
+ const eventSpy = spy(element, 'emitEvent');
+ element.value = 'abc';
+ element.valueMode = 'withFormatting';
+ await elementUpdated(element);
- input().dispatchEvent(new Event('change'));
+ input.dispatchEvent(new Event('change'));
expect(eventSpy).calledWith('igcChange', {
- detail: parser.apply(masked.value),
+ detail: parser.apply(element.value),
});
});
it('igcInput event', async () => {
- masked.mask = 'CCC';
- await elementUpdated(masked);
+ element.mask = 'CCC';
+ await elementUpdated(element);
syncParser();
- const eventSpy = spy(masked, 'emitEvent');
- masked.value = '111';
- masked.setSelectionRange(2, 3);
- await elementUpdated(masked);
+ const eventSpy = spy(element, 'emitEvent');
+ element.value = '111';
+ element.setSelectionRange(2, 3);
+ await elementUpdated(element);
- fireInputEvent(input(), 'insertText');
+ // fireInputEvent(input, 'insertText');
+ simulateInput(input, {
+ inputType: 'insertText',
+ skipValueProperty: true,
+ });
expect(eventSpy).calledWith('igcInput', { detail: '111' });
});
it('igInput event (end of pattern)', async () => {
- masked.mask = 'CCC';
- await elementUpdated(masked);
+ element.mask = 'CCC';
+ await elementUpdated(element);
syncParser();
- const eventSpy = spy(masked, 'emitEvent');
- masked.value = '111';
- masked.setSelectionRange(3, 3);
- await elementUpdated(masked);
+ const eventSpy = spy(element, 'emitEvent');
+ element.value = '111';
+ element.setSelectionRange(3, 3);
+ await elementUpdated(element);
- fireInputEvent(input(), 'insertText');
+ simulateInput(input, {
+ inputType: 'insertText',
+ skipValueProperty: true,
+ });
expect(eventSpy).not.calledWith('igcInput', { detail: '111' });
});
- it('is accessible', async () => await expect(masked).to.be.accessible());
+ it('is accessible', async () => {
+ await expect(element).to.be.accessible();
+ });
it('focus updates underlying input mask', async () => {
- masked.focus();
+ element.focus();
syncParser();
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply());
+ expect(input.value).to.equal(parser.apply());
});
it('blur updates underlying input mask (empty)', async () => {
syncParser();
- masked.focus();
- await elementUpdated(masked);
- masked.blur();
- await elementUpdated(masked);
+ element.focus();
+ await elementUpdated(element);
+ element.blur();
+ await elementUpdated(element);
- expect(input().value).to.equal('');
+ expect(input.value).to.equal('');
});
it('blur updates underlying input mask (non-empty)', async () => {
- masked.mask = '[CC] CC CC';
+ element.mask = '[CC] CC CC';
syncParser();
- masked.value;
- masked.focus();
- await elementUpdated(masked);
- masked.value = '654321';
- masked.blur();
- await elementUpdated(masked);
+ element.value;
+ element.focus();
+ await elementUpdated(element);
+ element.value = '654321';
+ element.blur();
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply('654321'));
+ expect(input.value).to.equal(parser.apply('654321'));
});
it('drag enter without focus', async () => {
syncParser();
- input().dispatchEvent(new DragEvent('dragenter'));
- await elementUpdated(masked);
+ input.dispatchEvent(new DragEvent('dragenter'));
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply());
+ expect(input.value).to.equal(parser.apply());
});
it('drag enter with focus', async () => {
syncParser();
- masked.focus();
- await elementUpdated(masked);
+ element.focus();
+ await elementUpdated(element);
- input().dispatchEvent(new DragEvent('dragenter'));
- await elementUpdated(masked);
+ input.dispatchEvent(new DragEvent('dragenter'));
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('drag leave without focus', async () => {
syncParser();
- input().dispatchEvent(new DragEvent('dragleave'));
- await elementUpdated(masked);
+ input.dispatchEvent(new DragEvent('dragleave'));
+ await elementUpdated(element);
- expect(input().value).to.equal('');
+ expect(input.value).to.equal('');
});
it('drag leave with focus', async () => {
- masked.focus();
- await elementUpdated(masked);
+ element.focus();
+ await elementUpdated(element);
- input().dispatchEvent(new DragEvent('dragleave'));
- await elementUpdated(masked);
+ input.dispatchEvent(new DragEvent('dragleave'));
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Delete key behavior', async () => {
- masked.value = '1234';
- await elementUpdated(masked);
- masked.setSelectionRange(3, 4);
-
- fireKeyboardEvent(input(), 'keydown', { key: 'Delete' });
- fireInputEvent(input(), 'deleteContentForward');
- await elementUpdated(masked);
+ element.value = '1234';
+ await elementUpdated(element);
+ element.setSelectionRange(3, 4);
+
+ simulateKeyboard(input, 'Delete');
+ simulateInput(input, {
+ inputType: 'deleteContentForward',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(masked.value).to.equal('123');
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(element.value).to.equal('123');
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Delete key behavior - skip literals', async () => {
- masked.mask = 'CC--CCC---CC';
- masked.value = '1234567';
+ element.mask = 'CC--CCC---CC';
+ element.value = '1234567';
// value: 12--345---67
- await elementUpdated(masked);
+ await elementUpdated(element);
// value: 12--345---67
- masked.setSelectionRange(1, 1);
- fireKeyboardEvent(input(), 'keydown', { key: 'Delete' });
- fireInputEvent(input(), 'deleteContentForward');
+ element.setSelectionRange(1, 1);
+ simulateKeyboard(input, 'Delete');
+ simulateInput(input, {
+ inputType: 'deleteContentForward',
+ skipValueProperty: true,
+ });
// value: 1_--345---67
- await elementUpdated(masked);
+ await elementUpdated(element);
- fireKeyboardEvent(input(), 'keydown', { key: 'Delete' });
- fireInputEvent(input(), 'deleteContentForward');
+ simulateKeyboard(input, 'Delete');
+ simulateInput(input, {
+ inputType: 'deleteContentForward',
+ skipValueProperty: true,
+ });
// value: 1_--_45---67
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(input().value).to.equal('1_--_45---67');
- expect(masked.value).to.equal('14567');
+ expect(input.value).to.equal('1_--_45---67');
+ expect(element.value).to.equal('14567');
});
it('Backspace key behavior', async () => {
- masked.value = '1234';
- await elementUpdated(masked);
- masked.setSelectionRange(0, 1);
-
- fireKeyboardEvent(input(), 'keydown', { key: 'Backspace' });
- fireInputEvent(input(), 'deleteContentBackward');
- await elementUpdated(masked);
+ element.value = '1234';
+ await elementUpdated(element);
+ element.setSelectionRange(0, 1);
+
+ simulateKeyboard(input, 'Backspace');
+ simulateInput(input, {
+ inputType: 'deleteContentBackward',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(masked.value).to.equal('234');
- expect(input().value).to.equal(parser.apply(input().value));
+ expect(element.value).to.equal('234');
+ expect(input.value).to.equal(parser.apply(input.value));
});
it('Backspace key behavior - skip literals', async () => {
- masked.mask = 'CC--CCC---CC';
- masked.value = '1234567';
+ element.mask = 'CC--CCC---CC';
+ element.value = '1234567';
// value: 12--345---67
- await elementUpdated(masked);
+ await elementUpdated(element);
- masked.setSelectionRange(4, 5);
- fireKeyboardEvent(input(), 'keydown', { key: 'Backspace' });
- fireInputEvent(input(), 'deleteContentBackward');
+ element.setSelectionRange(4, 5);
+ simulateKeyboard(input, 'Backspace');
+ simulateInput(input, {
+ inputType: 'deleteContentBackward',
+ skipValueProperty: true,
+ });
// value: 12--_45---67
- await elementUpdated(masked);
+ await elementUpdated(element);
// Emulate range shift on multiple backspace presses as
// it is not correctly reflected in test environment
- masked.setSelectionRange(3, 4);
- fireKeyboardEvent(input(), 'keydown', { key: 'Backspace' });
- fireInputEvent(input(), 'deleteContentBackward');
+ element.setSelectionRange(3, 4);
+ simulateKeyboard(input, 'Backspace');
+ simulateInput(input, {
+ inputType: 'deleteContentBackward',
+ skipValueProperty: true,
+ });
// value: 1_--_45---67
- await elementUpdated(masked);
+ await elementUpdated(element);
- expect(input().value).to.equal('1_--_45---67');
- expect(masked.value).to.equal('14567');
+ expect(input.value).to.equal('1_--_45---67');
+ expect(element.value).to.equal('14567');
});
it('Backspace key behavior with composition', async () => {
- masked.value = '1234';
- await elementUpdated(masked);
+ element.value = '1234';
+ await elementUpdated(element);
- masked.setSelectionRange(0, 1);
+ element.setSelectionRange(0, 1);
- fireKeyboardEvent(input(), 'keydown', { key: 'Backspace' });
- fireInputEvent(input(), 'deleteContentBackward', { isComposing: true });
- await elementUpdated(masked);
+ simulateKeyboard(input, 'Backspace');
+ simulateInput(input, {
+ inputType: 'deleteContentBackward',
+ isComposing: true,
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(masked.value).to.equal('1234');
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(element.value).to.equal('1234');
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Default input behavior', async () => {
- masked.value = 'xxxx';
- await elementUpdated(masked);
-
- masked.setSelectionRange(4, 4);
- input().value = `${masked.value}zz`;
- fireInputEvent(input(), 'insertText');
- await elementUpdated(masked);
+ element.value = 'xxxx';
+ await elementUpdated(element);
+
+ element.setSelectionRange(4, 4);
+ input.value = `${element.value}zz`;
+ simulateInput(input, {
+ inputType: 'insertText',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(masked.value).to.equal('xxxxzz');
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(element.value).to.equal('xxxxzz');
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Composition behavior', async () => {
const data = 'ときょお12や';
- masked.value = ' ';
- masked.mask = 'CCCC::###';
+ element.value = ' ';
+ element.mask = 'CCCC::###';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
- masked.setSelectionRange(0, 0);
- fireCompositionEvent(input(), 'compositionstart');
- input().setSelectionRange(0, data.length);
- fireCompositionEvent(input(), 'compositionend', { data });
- await elementUpdated(masked);
+ element.setSelectionRange(0, 0);
+ fireCompositionEvent(input, 'compositionstart');
+ input.setSelectionRange(0, data.length);
+ fireCompositionEvent(input, 'compositionend', { data });
+ await elementUpdated(element);
- expect(masked.value).to.equal('ときょお12');
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(element.value).to.equal('ときょお12');
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Cut behavior', async () => {
- masked.value = 'xxxyyyxxx';
- masked.mask = 'CCC-CCC-CCC';
+ element.value = 'xxxyyyxxx';
+ element.mask = 'CCC-CCC-CCC';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
- masked.setSelectionRange(4, 7);
- input().dispatchEvent(new ClipboardEvent('cut'));
- fireInputEvent(input(), 'deleteByCut');
- await elementUpdated(masked);
+ element.setSelectionRange(4, 7);
+ input.dispatchEvent(new ClipboardEvent('cut'));
+
+ simulateInput(input, {
+ inputType: 'deleteByCut',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(masked.value).to.equal('xxxxxx');
- expect(input().value).to.equal('xxx-___-xxx');
+ expect(element.value).to.equal('xxxxxx');
+ expect(input.value).to.equal('xxx-___-xxx');
});
it('Paste behavior', async () => {
- masked.value = '111111';
- masked.mask = 'CCC::CCC';
+ element.value = '111111';
+ element.mask = 'CCC::CCC';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
// Emulate paste behavior
- input().value = '112222';
- input().setSelectionRange(2, 8);
+ input.value = '112222';
+ input.setSelectionRange(2, 8);
- fireInputEvent(input(), 'insertFromPaste');
- await elementUpdated(masked);
- expect(input().value).to.equal(parser.apply(masked.value));
+ simulateInput(input, {
+ inputType: 'insertFromPaste',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
+ expect(input.value).to.equal(parser.apply(element.value));
});
it('Drop behavior', async () => {
- masked.mask = 'CCC::CCC';
- masked.value = '123456';
+ element.mask = 'CCC::CCC';
+ element.value = '123456';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
// Emulate drop behavior
- input().value = ' abc';
- input().setSelectionRange(3, 8);
+ input.value = ' abc';
+ input.setSelectionRange(3, 8);
- fireInputEvent(input(), 'insertFromDrop');
- await elementUpdated(masked);
+ simulateInput(input, {
+ inputType: 'insertFromDrop',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(input().value).to.equal(parser.apply(masked.value));
+ expect(input.value).to.equal(parser.apply(element.value));
// https://github.com/IgniteUI/igniteui-webcomponents/issues/383
- masked.mask = 'CC-CC';
- masked.value = 'xxyy';
+ element.mask = 'CC-CC';
+ element.value = 'xxyy';
- await elementUpdated(masked);
+ await elementUpdated(element);
syncParser();
- input().value = 'xx-basic-yy';
- input().setSelectionRange(3, 3 + 'basic'.length);
- fireInputEvent(input(), 'insertFromDrop');
- await elementUpdated(masked);
-
- expect(masked.value).to.equal('xxba');
- expect(input().value).to.equal(parser.apply(masked.value));
- });
- });
-
- // TODO: Remove after igc-form removal
- describe('igc-form interaction', async () => {
- let form: IgcFormComponent;
-
- beforeEach(async () => {
- form = await fixture(html`
-
-
-
- `);
- masked = form.querySelector('igc-mask-input') as IgcMaskInputComponent;
- });
-
- it('empty non-required mask with required pattern position', async () => {
- masked.mask = '&&&';
- await elementUpdated(masked);
-
- expect(form.submit()).to.equal(true);
- });
-
- it('empty required mask with required pattern position', async () => {
- masked.mask = '&&&';
- masked.required = true;
- await elementUpdated(masked);
-
- expect(form.submit()).to.equal(false);
- expect(masked.invalid).to.equal(true);
- });
-
- it('non-empty non-required mask with required pattern positions', async () => {
- masked.mask = '&&CC';
- masked.value = 'F';
- await elementUpdated(masked);
+ input.value = 'xx-basic-yy';
+ input.setSelectionRange(3, 3 + 'basic'.length);
+ simulateInput(input, {
+ inputType: 'insertFromDrop',
+ skipValueProperty: true,
+ });
+ await elementUpdated(element);
- expect(form.submit()).to.equal(false);
- expect(masked.invalid).to.equal(true);
+ expect(element.value).to.equal('xxba');
+ expect(input.value).to.equal(parser.apply(element.value));
});
});
@@ -745,16 +756,54 @@ describe('Masked input', () => {
spec.submitValidates();
});
});
-});
-type KeyboardEventType = 'keydown' | 'keypress' | 'keyup';
-type InputEventType =
- | 'insertText'
- | 'insertFromDrop'
- | 'insertFromPaste'
- | 'deleteContentForward'
- | 'deleteContentBackward'
- | 'deleteByCut';
+ describe('Validation message slots', () => {
+ async function createFixture(template: TemplateResult) {
+ element = await fixture(template);
+ }
+
+ it('renders bad-input slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'badInput');
+ });
+
+ it('renders value-missing slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'valueMissing');
+ });
+
+ it('renders invalid slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'invalid');
+ });
+
+ it('renders custom-error slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ element.setCustomValidity('invalid');
+ await checkValidationSlots(element, 'customError');
+ });
+ });
+});
type CompositionEventType = 'compositionstart' | 'compositionend';
@@ -763,21 +812,3 @@ const fireCompositionEvent = (
type: CompositionEventType,
options: Partial = {}
) => target.dispatchEvent(new CompositionEvent(type, { ...options }));
-
-const fireKeyboardEvent = (
- target: HTMLElement,
- type: KeyboardEventType,
- options: Partial = {}
-) => {
- target.dispatchEvent(new KeyboardEvent(type, { ...options }));
-};
-
-const fireInputEvent = (
- target: HTMLElement,
- type: InputEventType,
- options: Partial = {}
-) => {
- target.dispatchEvent(
- new InputEvent('input', { inputType: type, ...options })
- );
-};
diff --git a/src/components/mask-input/mask-input.ts b/src/components/mask-input/mask-input.ts
index a69f4c259..44fa43224 100644
--- a/src/components/mask-input/mask-input.ts
+++ b/src/components/mask-input/mask-input.ts
@@ -9,6 +9,7 @@ import { registerComponent } from '../common/definitions/register.js';
import messages from '../common/localization/validation-en.js';
import { partNameMap } from '../common/util.js';
import { type Validator, requiredValidator } from '../common/validators.js';
+import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import {
IgcMaskInputBaseComponent,
type MaskRange,
@@ -23,6 +24,10 @@ import {
* @slot prefix - Renders content before the input
* @slot suffix - Renders content after the input
* @slot helper-text - Renders content below the input
+ * @slot value-missing - Renders content when the required validation fails.
+ * @slot bad-input - Renders content when a required mask pattern validation fails.
+ * @slot custom-error - Renders content when setCustomValidity(message) is set.
+ * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
*
* @fires igcInput - Emitted when the control receives user input
* @fires igcChange - Emitted when an alteration of the control's value is committed by the user
@@ -41,7 +46,7 @@ export default class IgcMaskInputComponent extends IgcMaskInputBaseComponent {
/* blazorSuppress */
public static register() {
- registerComponent(IgcMaskInputComponent);
+ registerComponent(IgcMaskInputComponent, IgcValidationContainerComponent);
}
protected override validators: Validator[] = [
diff --git a/src/components/radio/radio.spec.ts b/src/components/radio/radio.spec.ts
index 7b56ff5c3..85be8e7e1 100644
--- a/src/components/radio/radio.spec.ts
+++ b/src/components/radio/radio.spec.ts
@@ -1,14 +1,13 @@
-import {
- elementUpdated,
- expect,
- fixture,
- html,
- unsafeStatic,
-} from '@open-wc/testing';
+import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
+import type { TemplateResult } from 'lit';
import { spy } from 'sinon';
-import { IgcRadioComponent, defineComponents } from '../../index.js';
-import { FormAssociatedTestBed } from '../common/utils.spec.js';
+import { defineComponents } from '../common/definitions/defineComponents.js';
+import {
+ FormAssociatedTestBed,
+ checkValidationSlots,
+} from '../common/utils.spec.js';
+import IgcRadioComponent from './radio.js';
describe('Radio Component', () => {
before(() => {
@@ -33,8 +32,10 @@ describe('Radio Component', () => {
describe('', () => {
beforeEach(async () => {
- radio = await createRadioComponent();
- input = radio.renderRoot.querySelector('input') as HTMLInputElement;
+ radio = await fixture(
+ html`${label}`
+ );
+ input = radio.renderRoot.querySelector('input')!;
});
it('is initialized with the proper default values', async () => {
@@ -210,12 +211,6 @@ describe('Radio Component', () => {
});
});
- const createRadioComponent = (
- template = `${label}`
- ) => {
- return fixture(html`${unsafeStatic(template)}`);
- };
-
describe('Form integration', () => {
const values = [1, 2, 3];
let radios: IgcRadioComponent[] = [];
@@ -348,4 +343,41 @@ describe('Radio Component', () => {
spec.submitValidates();
});
});
+
+ describe('Validation message slots', () => {
+ async function createFixture(template: TemplateResult) {
+ radio = await fixture(template);
+ }
+
+ it('renders value-missing slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(radio, 'valueMissing');
+ });
+
+ it('renders custom-error slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ radio.setCustomValidity('invalid');
+ await checkValidationSlots(radio, 'customError');
+ });
+
+ it('renders invalid slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(radio, 'invalid');
+ });
+ });
});
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index ec938b0ca..c799bc748 100644
--- a/src/components/radio/radio.ts
+++ b/src/components/radio/radio.ts
@@ -1,4 +1,4 @@
-import { LitElement, html } from 'lit';
+import { LitElement, type TemplateResult, html } from 'lit';
import { property, query, queryAssignedNodes, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
@@ -22,6 +22,7 @@ import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
import { FormAssociatedRequiredMixin } from '../common/mixins/form-associated-required.js';
import { createCounter, isLTR, partNameMap, wrap } from '../common/util.js';
import type { Validator } from '../common/validators.js';
+import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import { styles } from './themes/radio.base.css.js';
import { styles as shared } from './themes/shared/radio.common.css.js';
import { all } from './themes/themes.js';
@@ -37,6 +38,10 @@ export interface IgcRadioEventMap {
* @element igc-radio
*
* @slot - The radio label.
+ * @slot helper-text - Renders content below the input.
+ * @slot value-missing - Renders content when the required validation fails.
+ * @slot custom-error - Renders content when setCustomValidity(message) is set.
+ * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
*
* @fires igcChange - Emitted when the control's checked state changes.
* @fires igcFocus - Emitted when the control gains focus.
@@ -55,7 +60,7 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
/* blazorSuppress */
public static register() {
- registerComponent(IgcRadioComponent);
+ registerComponent(IgcRadioComponent, IgcValidationContainerComponent);
}
private static readonly increment = createCounter();
@@ -208,7 +213,7 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
const radios = this._radios;
for (const radio of radios) {
- radio.updateValidity(message);
+ radio.updateValidity(message, true);
radio.setInvalidState();
}
}
@@ -292,6 +297,10 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
radio.emitEvent('igcChange', { detail: radio.checked });
}
+ protected renderValidatorContainer(): TemplateResult {
+ return IgcValidationContainerComponent.create(this);
+ }
+
protected override render() {
const labelledBy = this.getAttribute('aria-labelledby');
const checked = this.checked;
@@ -336,6 +345,7 @@ export default class IgcRadioComponent extends FormAssociatedRequiredMixin(
+ ${this.renderValidatorContainer()}
`;
}
}
diff --git a/src/components/select/select.spec.ts b/src/components/select/select.spec.ts
index b123b7042..155ca8048 100644
--- a/src/components/select/select.spec.ts
+++ b/src/components/select/select.spec.ts
@@ -4,10 +4,10 @@ import {
expect,
fixture,
html,
- nextFrame,
} from '@open-wc/testing';
import { spy } from 'sinon';
+import type { TemplateResult } from 'lit';
import {
altKey,
arrowDown,
@@ -22,8 +22,10 @@ import {
import { defineComponents } from '../common/definitions/defineComponents.js';
import {
FormAssociatedTestBed,
+ checkValidationSlots,
simulateClick,
simulateKeyboard,
+ simulateScroll,
} from '../common/utils.spec.js';
import IgcInputComponent from '../input/input.js';
import IgcSelectHeaderComponent from './select-header.js';
@@ -311,13 +313,6 @@ describe('Select', () => {
describe('Scroll strategy', () => {
let container: HTMLDivElement;
- const scrollBy = async (amount: number) => {
- container.scrollTo({ top: amount });
- container.dispatchEvent(new Event('scroll'));
- await elementUpdated(select);
- await nextFrame();
- };
-
beforeEach(async () => {
container = await fixture(createScrollableSelectParent());
select = container.querySelector(IgcSelectComponent.tagName)!;
@@ -325,7 +320,7 @@ describe('Select', () => {
it('`scroll` behavior', async () => {
await openSelect();
- await scrollBy(200);
+ await simulateScroll(container, { top: 200 });
expect(select.open).to.be.true;
});
@@ -335,7 +330,7 @@ describe('Select', () => {
select.scrollStrategy = 'close';
await openSelect();
- await scrollBy(200);
+ await simulateScroll(container, { top: 200 });
expect(select.open).to.be.false;
expect(eventSpy.firstCall).calledWith('igcClosing');
@@ -345,7 +340,7 @@ describe('Select', () => {
it('`block behavior`', async () => {
select.scrollStrategy = 'block';
await openSelect();
- await scrollBy(200);
+ await simulateScroll(container, { top: 200 });
expect(select.open).to.be.true;
});
@@ -1211,6 +1206,31 @@ describe('Select', () => {
});
});
+ describe('issue-1123', () => {
+ beforeEach(async () => {
+ select = await fixture(html`
+
+ Orange
+ Apple
+ Banana
+ Mango
+
+ `);
+ });
+
+ it('', async () => {
+ await openSelect();
+
+ const inner = document.getElementById('click-target')!;
+ simulateClick(inner);
+ await elementUpdated(select);
+
+ expect(select.value).to.equal('Orange');
+ });
+ });
+
describe('Form integration', () => {
const spec = new FormAssociatedTestBed(
createBasicSelect()
@@ -1290,28 +1310,42 @@ describe('Select', () => {
});
});
- describe('issue-1123', () => {
- beforeEach(async () => {
- select = await fixture(html`
-
- Orange
- Apple
- Banana
- Mango
+ describe('Validation message slots', () => {
+ let select: IgcSelectComponent;
+
+ async function createFixture(template: TemplateResult) {
+ select = await fixture(template);
+ }
+
+ it('renders value-missing slot', async () => {
+ await createFixture(html`
+
+
`);
+
+ await checkValidationSlots(select, 'valueMissing');
});
- it('', async () => {
- await openSelect();
+ it('renders invalid slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
- const inner = document.getElementById('click-target')!;
- simulateClick(inner);
- await elementUpdated(select);
+ await checkValidationSlots(select, 'invalid');
+ });
- expect(select.value).to.equal('Orange');
+ it('renders custom-error slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ select.setCustomValidity('invalid');
+ await checkValidationSlots(select, 'customError');
});
});
});
diff --git a/src/components/select/select.ts b/src/components/select/select.ts
index 43dad3221..b85639cc8 100644
--- a/src/components/select/select.ts
+++ b/src/components/select/select.ts
@@ -1,4 +1,4 @@
-import { html } from 'lit';
+import { type TemplateResult, html } from 'lit';
import {
property,
query,
@@ -46,6 +46,7 @@ import { type Validator, requiredValidator } from '../common/validators.js';
import IgcIconComponent from '../icon/icon.js';
import IgcInputComponent from '../input/input.js';
import IgcPopoverComponent, { type IgcPlacement } from '../popover/popover.js';
+import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import IgcSelectGroupComponent from './select-group.js';
import IgcSelectHeaderComponent from './select-header.js';
import IgcSelectItemComponent from './select-item.js';
@@ -76,6 +77,9 @@ export interface IgcSelectEventMap {
* @slot helper-text - Renders content below the input.
* @slot toggle-icon - Renders content inside the suffix container.
* @slot toggle-icon-expanded - Renders content for the toggle icon when the component is in open state.
+ * @slot value-missing - Renders content when the required validation fails.
+ * @slot custom-error - Renders content when setCustomValidity(message) is set.
+ * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
*
* @fires igcFocus - Emitted when the select gains focus.
* @fires igcBlur - Emitted when the select loses focus.
@@ -156,9 +160,6 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
@query(IgcInputComponent.tagName, true)
protected input!: IgcInputComponent;
- @queryAssignedElements({ slot: 'helper-text' })
- protected helperText!: Array;
-
@queryAssignedElements({ slot: 'suffix' })
protected inputSuffix!: Array;
@@ -180,10 +181,6 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
return this.inputSuffix.length > 0;
}
- protected get hasHelperText() {
- return this.helperText.length > 0;
- }
-
/**
* The value attribute of the control.
* @attr
@@ -329,6 +326,12 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
this.addEventListener('focusout', this.handleFocusOut);
}
+ protected override createRenderRoot() {
+ const root = super.createRenderRoot();
+ root.addEventListener('slotchange', () => this.requestUpdate());
+ return root;
+ }
+
protected override async firstUpdated() {
await this.updateComplete;
const selected = setInitialSelectionState(this.items);
@@ -464,11 +467,6 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
this.open ? this._navigateToActiveItem(item) : this._selectItem(item);
}
- /** Monitor input slot changes and request update */
- protected inputSlotChanged() {
- this.requestUpdate();
- }
-
private activateItem(item: IgcSelectItemComponent) {
if (this._activeItem) {
this._activeItem.active = false;
@@ -607,11 +605,11 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
return html`
-
+
-
+
`;
}
@@ -630,36 +628,23 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
return html`
-
+
-
+
`;
}
- protected renderHelperText() {
- return html`
-
-
-
- `;
+ protected renderHelperText(): TemplateResult {
+ return IgcValidationContainerComponent.create(this, {
+ id: 'select-helper-text',
+ slot: 'anchor',
+ hasHelperText: true,
+ });
}
protected renderInputAnchor() {
@@ -672,7 +657,7 @@ export default class IgcSelectComponent extends FormAssociatedRequiredMixin(
role="combobox"
readonly
aria-controls="dropdown"
- aria-describedby="helper-text"
+ aria-describedby="select-helper-text"
aria-expanded=${this.open ? 'true' : 'false'}
exportparts="container: input, input: native-input, label, prefix, suffix"
tabIndex=${this.disabled ? -1 : 0}
diff --git a/src/components/textarea/textarea.spec.ts b/src/components/textarea/textarea.spec.ts
index 171d552d8..ae626bb0b 100644
--- a/src/components/textarea/textarea.spec.ts
+++ b/src/components/textarea/textarea.spec.ts
@@ -1,35 +1,71 @@
-import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
+import {
+ elementUpdated,
+ expect,
+ fixture,
+ html,
+ nextFrame,
+} from '@open-wc/testing';
import { spy } from 'sinon';
+import type { TemplateResult } from 'lit';
+import { configureTheme } from '../../theming/config.js';
import { defineComponents } from '../common/definitions/defineComponents.js';
-import { FormAssociatedTestBed } from '../common/utils.spec.js';
+import {
+ FormAssociatedTestBed,
+ checkValidationSlots,
+ simulateInput,
+ simulateScroll,
+} from '../common/utils.spec.js';
import IgcTextareaComponent from './textarea.js';
describe('Textarea component', () => {
- before(() => defineComponents(IgcTextareaComponent));
+ before(() => {
+ defineComponents(IgcTextareaComponent);
+ });
let element: IgcTextareaComponent;
let textArea: HTMLTextAreaElement;
+ async function createFixture(template: TemplateResult) {
+ element = await fixture(template);
+ textArea = element.renderRoot.querySelector('textarea')!;
+ }
+
+ describe('Defaults', () => {
+ it('is accessible', async () => {
+ await createFixture(html``);
+
+ await expect(element).to.be.accessible();
+ await expect(element).shadowDom.to.be.accessible();
+ });
+
+ it('material variant layout', async () => {
+ configureTheme('material');
+ await createFixture(html``);
+
+ expect(element.renderRoot.querySelector('[part="notch"]')).to.exist;
+
+ // Reset theme
+ configureTheme('bootstrap');
+ await nextFrame();
+ });
+ });
+
describe('Setting value through attribute and projection', () => {
const value = 'Hello world!';
it('through attribute', async () => {
- element = await fixture(
- html``
- );
+ await createFixture(html``);
expect(element.value).to.equal(value);
});
it('through slot projection', async () => {
- element = await fixture(
- html`${value}`
- );
+ await createFixture(html`${value}`);
expect(element.value).to.equal(value);
});
it('priority of slot over attribute value binding', async () => {
- element = await fixture(
+ await createFixture(
html`${value}`
);
@@ -37,12 +73,9 @@ describe('Textarea component', () => {
});
it('reflects on slot change state', async () => {
+ await createFixture(html`${value}`);
const additional = ['...', 'Goodbye world!'];
- element = await fixture(
- html`${value}`
- );
-
element.append(...additional);
await elementUpdated(element);
@@ -71,18 +104,13 @@ describe('Textarea component', () => {
describe('Events', () => {
beforeEach(async () => {
- element = await fixture(
- html``
- );
- textArea = element.shadowRoot!.querySelector('textarea')!;
+ await createFixture(html``);
});
it('igcInput', async () => {
const eventSpy = spy(element, 'emitEvent');
- textArea.value = '123';
- textArea.dispatchEvent(new Event('input'));
-
+ simulateInput(textArea, { value: '123' });
await elementUpdated(element);
expect(eventSpy).calledOnceWithExactly('igcInput', { detail: '123' });
@@ -106,10 +134,7 @@ describe('Textarea component', () => {
const projected = 'Hello world!';
beforeEach(async () => {
- element = await fixture(
- html`${projected}`
- );
- textArea = element.shadowRoot!.querySelector('textarea')!;
+ await createFixture(html`${projected}`);
});
it('select()', async () => {
@@ -158,23 +183,20 @@ describe('Textarea component', () => {
'\n'
)
);
- const textarea = element.shadowRoot?.querySelector('textarea');
const [xDelta, yDelta] = [250, 250];
element.wrap = 'off';
element.appendChild(text);
await elementUpdated(element);
- element.scrollTo({ top: yDelta, left: xDelta });
- await elementUpdated(element);
- expect([textarea?.scrollLeft, textarea?.scrollTop]).to.eql([
+ await simulateScroll(element, { top: yDelta, left: xDelta });
+ expect([textArea.scrollLeft, textArea.scrollTop]).to.eql([
xDelta,
yDelta,
]);
- element.scrollTo(xDelta * 2, yDelta * 2);
- await elementUpdated(element);
- expect([textarea?.scrollLeft, textarea?.scrollTop]).to.eql([
+ await simulateScroll(element, { top: yDelta * 2, left: xDelta * 2 });
+ expect([textArea.scrollLeft, textArea.scrollTop]).to.eql([
xDelta * 2,
yDelta * 2,
]);
@@ -265,4 +287,62 @@ describe('Textarea component', () => {
spec.submitValidates();
});
});
+
+ describe('Validation message slots', () => {
+ async function createFixture(template: TemplateResult) {
+ element = await fixture(template);
+ }
+
+ it('renders too-long slot', async () => {
+ await createFixture(html`
+
+ 1234
+
+
+ `);
+
+ await checkValidationSlots(element, 'tooLong');
+ });
+
+ it('renders too-short slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'tooShort');
+ });
+
+ it('renders value-missing slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'valueMissing');
+ });
+
+ it('renders invalid slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ await checkValidationSlots(element, 'invalid');
+ });
+
+ it('renders custom-error slot', async () => {
+ await createFixture(html`
+
+
+
+ `);
+
+ element.setCustomValidity('invalid');
+ await checkValidationSlots(element, 'customError');
+ });
+ });
});
diff --git a/src/components/textarea/textarea.ts b/src/components/textarea/textarea.ts
index 31c20d526..b91b86b01 100644
--- a/src/components/textarea/textarea.ts
+++ b/src/components/textarea/textarea.ts
@@ -1,4 +1,4 @@
-import { LitElement, html, nothing } from 'lit';
+import { LitElement, type TemplateResult, html, nothing } from 'lit';
import {
property,
query,
@@ -23,6 +23,7 @@ import {
minLengthValidator,
requiredValidator,
} from '../common/validators.js';
+import IgcValidationContainerComponent from '../validation-container/validation-container.js';
import { styles as shared } from './themes/shared/textarea.common.css.js';
import { styles } from './themes/textarea.base.css.js';
import { all } from './themes/themes.js';
@@ -45,6 +46,11 @@ export interface IgcTextareaEventMap {
* @slot prefix - Renders content before the input.
* @slot suffix - Renders content after input.
* @slot helper-text - Renders content below the input.
+ * @slot value-missing - Renders content when the required validation fails.
+ * @slot too-long - Renders content when the maxlength validation fails.
+ * @slot too-short - Renders content when the minlength validation fails.
+ * @slot custom-error - Renders content when setCustomValidity(message) is set.
+ * @slot invalid - Renders content when the component is in invalid state (validity.valid = false).
*
* @fires igcInput - Emitted when the control receives user input.
* @fires igcChange - Emitted when the a change to the control value is committed by the user.
@@ -67,7 +73,7 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
/* blazorSuppress */
public static register() {
- registerComponent(IgcTextareaComponent);
+ registerComponent(IgcTextareaComponent, IgcValidationContainerComponent);
}
private declare readonly [themeSymbol]: Theme;
@@ -97,9 +103,6 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
@queryAssignedElements({ slot: 'suffix' })
protected suffixes!: Array;
- @queryAssignedElements({ slot: 'helper-text' })
- protected helperText!: Array;
-
@query('textarea', true)
private input!: HTMLTextAreaElement;
@@ -411,12 +414,8 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
>`;
}
- protected renderHelperText() {
- return html`
-
-
-
- `;
+ protected renderValidationContainer(): TemplateResult {
+ return IgcValidationContainerComponent.create(this);
}
protected renderPrefix() {
@@ -445,7 +444,7 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
${this.renderPrefix()} ${this.renderInput()} ${this.renderSuffix()}
- ${this.renderHelperText()}
+ ${this.renderValidationContainer()}
`;
}
@@ -463,7 +462,7 @@ export default class IgcTextareaComponent extends FormAssociatedRequiredMixin(
${this.renderSuffix()}
- ${this.renderHelperText()}
+ ${this.renderValidationContainer()}
`;
}
diff --git a/src/components/validation-container/validation-container.spec.ts b/src/components/validation-container/validation-container.spec.ts
new file mode 100644
index 000000000..a78e9fb28
--- /dev/null
+++ b/src/components/validation-container/validation-container.spec.ts
@@ -0,0 +1,81 @@
+import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
+import type { TemplateResult } from 'lit';
+import { defineComponents } from '../common/definitions/defineComponents.js';
+import {
+ checkValidationSlots,
+ hasSlotContent,
+ hasSlots,
+} from '../common/utils.spec.js';
+import IgcInputComponent from '../input/input.js';
+import IgcValidationContainerComponent from './validation-container.js';
+
+describe('Validation container', () => {
+ let element: IgcInputComponent;
+ let container: IgcValidationContainerComponent;
+ const helperSlot = 'helper-text';
+
+ before(() => {
+ defineComponents(IgcInputComponent);
+ });
+
+ function waitForUpdate() {
+ return Promise.all([elementUpdated(element), elementUpdated(container)]);
+ }
+
+ async function createFixture(template: TemplateResult) {
+ element = await fixture