From 653ef109be67277e88f9d09e240cdb5a2c69c9ee Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sun, 12 Apr 2026 16:58:39 +0000 Subject: [PATCH] feat(input-text): add validated success state and text editing context menu to text inputs --- changelog.md | 8 ++ ts_web/00_commitinfo_data.ts | 2 +- .../dees-input-iban/dees-input-iban.ts | 23 ++-- .../dees-input-text/dees-input-text.demo.ts | 3 +- .../dees-input-text/dees-input-text.ts | 114 +++++++++++++++++- 5 files changed, 133 insertions(+), 17 deletions(-) diff --git a/changelog.md b/changelog.md index 5669c8c..4f7601f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-12 - 3.74.0 - feat(input-text) +add validated success state and text editing context menu to text inputs + +- show a delayed checkmark confirmation for successful validation and hide inline validation text afterward +- move IBAN validation handling into the shared text input validation function +- improve the email validation demo to use a stricter regex-based check +- add cut, copy, paste, and select-all context menu actions for text inputs + ## 2026-04-12 - 3.73.2 - fix(input,label) correct validation state attribute handling in text inputs and refine label description icon styling diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 8891bb5..1baec68 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.73.2', + version: '3.74.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-input/dees-input-iban/dees-input-iban.ts b/ts_web/elements/00group-input/dees-input-iban/dees-input-iban.ts index dd1394e..75f6138 100644 --- a/ts_web/elements/00group-input/dees-input-iban/dees-input-iban.ts +++ b/ts_web/elements/00group-input/dees-input-iban/dees-input-iban.ts @@ -50,6 +50,16 @@ export class DeesInputIban extends DeesInputBase { .disabled=${this.disabled} .required=${this.required} .placeholder=${'DE89 3704 0044 0532 0130 00'} + .validationFunction=${(value: string) => { + const normalized = value.replace(/ /g, ''); + if (normalized.length === 0) { + return { valid: true, message: '' }; + } + const isValid = ibantools.isValidIBAN(normalized); + return isValid + ? { valid: true, message: 'IBAN is valid' } + : { valid: false, message: 'Please enter a valid IBAN' }; + }} @input=${(eventArg: InputEvent) => { this.validateIban(eventArg); }} @@ -81,19 +91,6 @@ export class DeesInputIban extends DeesInputBase { } } this.enteredIbanIsValid = ibantools.isValidIBAN(this.enteredString.replace(/ /g, '')); - const deesInputText = this.shadowRoot!.querySelector('dees-input-text') as any; - if (deesInputText) { - if (this.enteredIbanIsValid) { - deesInputText.validationState = 'valid'; - deesInputText.validationText = 'IBAN is valid'; - } else if (this.enteredString.length > 0) { - deesInputText.validationState = 'invalid'; - deesInputText.validationText = 'Please enter a valid IBAN'; - } else { - deesInputText.validationState = undefined; - deesInputText.validationText = ''; - } - } } public getValue(): string { diff --git a/ts_web/elements/00group-input/dees-input-text/dees-input-text.demo.ts b/ts_web/elements/00group-input/dees-input-text/dees-input-text.demo.ts index cc3848a..282ffc5 100644 --- a/ts_web/elements/00group-input/dees-input-text/dees-input-text.demo.ts +++ b/ts_web/elements/00group-input/dees-input-text/dees-input-text.demo.ts @@ -229,7 +229,8 @@ export const demoFunc = () => html` .label=${'Email with Validation'} .value=${'invalid@'} .validationFunction=${(value: string) => { - if (value.includes('@') && value.includes('.')) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (emailRegex.test(value)) { return { valid: true, message: 'Email address is valid' }; } return { valid: false, message: 'Please enter a valid email address' }; diff --git a/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts b/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts index 5b7b039..01aaa85 100644 --- a/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts +++ b/ts_web/elements/00group-input/dees-input-text/dees-input-text.ts @@ -7,11 +7,13 @@ import { customElement, type TemplateResult, property, + state, html, cssManager, css, } from '@design.estate/dees-element'; import { themeDefaultStyles } from '../../00theme.js'; +import '../../00group-overlay/dees-contextmenu/dees-contextmenu.js'; declare global { interface HTMLElementTagNameMap { @@ -63,6 +65,11 @@ export class DeesInputText extends DeesInputBase { }) accessor vintegrated: boolean = false; + @property({ attribute: false }) + accessor validConfirmed: boolean = false; + + private validTimeout: ReturnType | undefined; + public static styles = [ themeDefaultStyles, ...DeesInputBase.baseStyles, @@ -145,6 +152,33 @@ export class DeesInputText extends DeesInputBase { color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')}; } + /* Valid checkmark icon */ + .validIcon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')}; + opacity: 0; + transition: opacity 0.2s ease; + pointer-events: none; + } + + .validIcon.show { + opacity: 1; + } + + .validIcon dees-icon { + font-size: 14px; + color: white; + } + /* Validation styles */ .validationContainer { margin-top: 4px; @@ -239,9 +273,9 @@ export class DeesInputText extends DeesInputBase { input { font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'}; letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'}; - padding-right: ${this.isPasswordBool ? '48px' : '12px'}; + padding-right: ${this.isPasswordBool ? '48px' : this.validConfirmed ? '40px' : '12px'}; } - ${this.validationText + ${this.validationText && !this.validConfirmed ? css` .validationContainer { height: auto; @@ -275,6 +309,9 @@ export class DeesInputText extends DeesInputBase { ` : html``} +
+ +
${this.validationText ? html`
@@ -292,6 +329,13 @@ export class DeesInputText extends DeesInputBase { const result = this.validationFunction(this.value); this.validationState = result.valid ? 'valid' : 'invalid'; this.validationText = result.message || ''; + if (result.valid) { + this.validConfirmed = false; + this.validTimeout = setTimeout(() => { + this.validConfirmed = true; + this.validationState = undefined as any; + }, 500); + } } } @@ -302,10 +346,76 @@ export class DeesInputText extends DeesInputBase { const result = this.validationFunction(this.value); this.validationState = result.valid ? 'valid' : 'invalid'; this.validationText = result.message || ''; + if (result.valid) { + this.validConfirmed = false; + clearTimeout(this.validTimeout); + this.validTimeout = setTimeout(() => { + this.validConfirmed = true; + this.validationState = undefined as any; + }, 500); + } else { + clearTimeout(this.validTimeout); + this.validConfirmed = false; + } } this.changeSubject.next(this); } + public getContextMenuItems() { + const input = this.shadowRoot!.querySelector('input')!; + const hasSelection = input.selectionStart !== input.selectionEnd; + return [ + { + name: 'Cut', + iconName: 'lucide:Scissors', + shortcut: 'Cmd+X', + disabled: !hasSelection, + action: async () => { + const selected = this.value.substring(input.selectionStart!, input.selectionEnd!); + await navigator.clipboard.writeText(selected); + const start = input.selectionStart!; + this.value = this.value.substring(0, start) + this.value.substring(input.selectionEnd!); + input.value = this.value; + input.setSelectionRange(start, start); + this.changeSubject.next(this); + }, + }, + { + name: 'Copy', + iconName: 'lucide:Copy', + shortcut: 'Cmd+C', + disabled: !hasSelection, + action: async () => { + const selected = this.value.substring(input.selectionStart!, input.selectionEnd!); + await navigator.clipboard.writeText(selected); + }, + }, + { + name: 'Paste', + iconName: 'lucide:ClipboardPaste', + shortcut: 'Cmd+V', + action: async () => { + const text = await navigator.clipboard.readText(); + const start = input.selectionStart!; + const end = input.selectionEnd!; + this.value = this.value.substring(0, start) + text + this.value.substring(end); + input.value = this.value; + input.setSelectionRange(start + text.length, start + text.length); + this.changeSubject.next(this); + }, + }, + { divider: true }, + { + name: 'Select All', + iconName: 'lucide:TextCursorInput', + shortcut: 'Cmd+A', + action: async () => { + input.select(); + }, + }, + ]; + } + public getValue(): string { return this.value; }