diff --git a/src/components/validation-container/validation-container.ts b/src/components/validation-container/validation-container.ts index c5abf33bf..a4e756323 100644 --- a/src/components/validation-container/validation-container.ts +++ b/src/components/validation-container/validation-container.ts @@ -11,6 +11,7 @@ import { styles as shared } from './themes/shared/validator.common.css.js'; import { all } from './themes/themes.js'; import { styles } from './themes/validator.base.css.js'; +/** Configuration for the validation container. */ interface ValidationContainerConfig { /** The id attribute for the validation container. */ id?: string; @@ -22,29 +23,39 @@ interface ValidationContainerConfig { hasHelperText?: boolean; } -function getValidationSlots(element: IgcValidationContainerComponent) { +const VALIDATION_SLOTS_SELECTOR = 'slot:not([name="helper-text"])'; +const ALL_SLOTS_SELECTOR = 'slot'; + +function getValidationSlots( + element: IgcValidationContainerComponent +): NodeListOf { return element.renderRoot.querySelectorAll( - "slot:not([name='helper-text'])" + VALIDATION_SLOTS_SELECTOR ); } -function hasProjection(element: IgcValidationContainerComponent) { - return Array.from( - element.renderRoot.querySelectorAll('slot') - ).every((slot) => isEmpty(slot.assignedElements({ flatten: true }))); +function hasProjection(element: IgcValidationContainerComponent): boolean { + const allSlots = + element.renderRoot.querySelectorAll(ALL_SLOTS_SELECTOR); + return Array.from(allSlots).every((slot) => + isEmpty(slot.assignedElements({ flatten: true })) + ); } function hasProjectedValidation( element: IgcValidationContainerComponent, slotName?: string -) { - const config: AssignedNodesOptions = { flatten: true }; +): boolean { const slots = Array.from(getValidationSlots(element)); - return slotName - ? slots - .filter((slot) => slot.name === slotName) - .some((slot) => slot.assignedElements(config).length > 0) - : slots.some((slot) => slot.assignedElements(config).length > 0); + const config: AssignedNodesOptions = { flatten: true }; + + if (slotName) { + return slots + .filter((slot) => slot.name === slotName) + .some((slot) => !isEmpty(slot.assignedElements(config))); + } + + return slots.some((slot) => !isEmpty(slot.assignedElements(config))); } /* blazorSuppress */ @@ -60,7 +71,7 @@ export default class IgcValidationContainerComponent extends LitElement { public static override styles = [styles, shared]; /* blazorSuppress */ - public static register() { + public static register(): void { registerComponent(IgcValidationContainerComponent, IgcIconComponent); } @@ -71,21 +82,26 @@ export default class IgcValidationContainerComponent extends LitElement { hasHelperText: true, } ): TemplateResult { - const { renderValidationSlots } = IgcValidationContainerComponent.prototype; const helperText = config.hasHelperText ? html`` - : null; + : nothing; + + const validationSlots = + IgcValidationContainerComponent.prototype._renderValidationSlots( + host.validity, + true + ); return html` - ${helperText}${renderValidationSlots(host.validity, true)} + ${helperText}${validationSlots} `; } @@ -109,7 +125,7 @@ export default class IgcValidationContainerComponent extends LitElement { this._target.addEventListener('invalid', this); } - public get target() { + public get target(): IgcFormControl { return this._target; } @@ -118,73 +134,87 @@ export default class IgcValidationContainerComponent extends LitElement { addThemingController(this, all); } - protected override createRenderRoot() { + protected override createRenderRoot(): HTMLElement | DocumentFragment { const root = super.createRenderRoot(); root.addEventListener('slotchange', this); return root; } - public handleEvent({ type }: Event) { - const isInvalid = type === 'invalid'; - const isSlotChange = type === 'slotchange'; - - if (isInvalid || isSlotChange) { - this.invalid = isInvalid ? true : this.invalid; - this._hasSlottedContent = hasProjectedValidation(this); + /** @internal */ + public handleEvent(event: Event): void { + switch (event.type) { + case 'invalid': + if (!this.invalid) { + this.invalid = true; + } + break; + case 'slotchange': { + const newHasSlottedContent = hasProjectedValidation(this); + if (this._hasSlottedContent !== newHasSlottedContent) { + this._hasSlottedContent = newHasSlottedContent; + } + break; + } } this.requestUpdate(); } - protected renderValidationMessage(slotName: string) { - const icon = hasProjectedValidation(this, slotName) + protected _renderValidationMessage(slotName: string): TemplateResult { + const hasProjectedIcon = hasProjectedValidation(this, slotName); + const parts = { 'validation-message': true, empty: !hasProjectedIcon }; + const icon = hasProjectedIcon ? html` ` - : null; + : nothing; return html` -
+
${icon}
`; } - protected *renderValidationSlots(validity: ValidityState, projected = false) { + protected *_renderValidationSlots( + validity: ValidityState, + projected = false + ): Generator { + if (!validity.valid) { + yield projected + ? html`` + : this._renderValidationMessage('invalid'); + } + for (const key in validity) { - if (key === 'valid' && !validity[key]) { - yield projected - ? html`` - : this.renderValidationMessage('invalid'); - } else if (validity[key as keyof ValidityState]) { + if (key !== 'valid' && validity[key as keyof ValidityState]) { const name = toKebabCase(key); - yield projected ? html`` - : this.renderValidationMessage(name); + : this._renderValidationMessage(name); } } } - protected renderHelper() { + protected _renderHelper(): TemplateResult | typeof nothing { return this.invalid && this._hasSlottedContent ? nothing : html``; } - protected override render() { + protected override render(): TemplateResult { + const slots = this.invalid + ? this._renderValidationSlots(this.target.validity) + : nothing; + return html`
- ${this.invalid - ? this.renderValidationSlots(this.target.validity) - : nothing} - ${this.renderHelper()} + ${slots}${this._renderHelper()}
`; }