Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
9efd8af
Fix sample file name
sdvg May 8, 2025
04d949f
Introduce new prop _character-limit for kol-textarea
sdvg May 8, 2025
f2b4d03
Replace _has-counter with `_character-limit` for textarea and adjust …
sdvg May 9, 2025
263ded1
Refactor and add classname when limit exceeded
sdvg May 9, 2025
262a7fb
Add styling for limit exceeded in themes
sdvg May 9, 2025
16199c8
Textarea: Add debounced character counter for screen readers
sdvg May 14, 2025
a7ed804
Add character limit hint
sdvg May 14, 2025
826d56a
Make screen reader hint visually hidden
sdvg May 14, 2025
6f75415
Remove has-counter property from textarea
sdvg May 14, 2025
0db0e4b
Move method to controller
sdvg May 14, 2025
64ebfb7
Implement character-limit for kol-input-email
sdvg May 14, 2025
799913b
Implement character-limit for kol-input-password and kol-input-text
sdvg May 14, 2025
57093a9
Remove unused type files
sdvg May 14, 2025
5e50f2e
Update sample
sdvg May 14, 2025
197dcd0
Fix ariaDescribedBy attribute for inputs
sdvg May 14, 2025
21a9c35
Update all snapshots$
sdvg May 15, 2025
85761dd
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg May 15, 2025
48738b1
Update all snapshots$
sdvg May 15, 2025
1bf882d
Fix initial aria-live value not set and remove redundant state updates
sdvg May 15, 2025
c62f03d
Add character limit E2E tests
sdvg May 15, 2025
0f6ddde
Merge branch 'develop' into 7162-rework-counter-v3
sdvg May 15, 2025
b213151
Add Breaking Changes
sdvg May 15, 2025
9c4fa99
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jun 13, 2025
5e90d49
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jun 23, 2025
7aaaa30
Change API for character counter to MaxLengthBehaviorPropType
sdvg Jun 24, 2025
46bd326
Set default value
sdvg Jun 24, 2025
ac1ee89
Update all snapshots$
sdvg Jun 24, 2025
f7bb502
Fix prop in sample
sdvg Jun 24, 2025
af279c1
Update breaking changes
sdvg Jun 24, 2025
d13af79
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jun 24, 2025
a8d02a6
Merge branch '7162-rework-counter-v3' of github.com:public-ui/kolibri…
sdvg Jun 24, 2025
a60cf3f
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jul 15, 2025
a91592b
Merge branch 'develop' of https://github.com/public-ui/kolibri into 7…
deleonio Jul 16, 2025
4d3dd57
Update all snapshots$
deleonio Jul 16, 2025
79c46f9
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jul 19, 2025
e9d6804
Merge branch 'develop' of github.com:public-ui/kolibri into 7162-rewo…
sdvg Jul 22, 2025
3609c00
Add test cases
sdvg Jul 22, 2025
e15cf5d
Fix test definition
sdvg Jul 22, 2025
48258e5
Bring back prop _has-counter
sdvg Jul 22, 2025
fd6aaca
Implement alternative _has-counter behavior
sdvg Jul 22, 2025
d0d2a53
Update all snapshots$
sdvg Jul 22, 2025
4a7621a
Add tests for FormFieldCharacterLimitHint
sdvg Jul 22, 2025
4a76255
Merge remote-tracking branch 'origin/7162-rework-counter-v3' into 716…
sdvg Jul 22, 2025
427edd0
Fix counter not updating for empty strings
sdvg Jul 22, 2025
3a21425
Merge branch 'develop' into 7162-rework-counter-v3
sdvg Jul 22, 2025
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
13 changes: 13 additions & 0 deletions packages/components/src/components/@deprecated/input/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { ControlledInputController } from '../../input-adapter-leanup/controller
import type { Props as AdapterProps } from '../../input-adapter-leanup/types';
import type { Props, Watches } from './types';
import { validateAccessAndShortKey } from '../../../schema/validators/access-and-short-key';
import { debounce } from 'lodash-es';

Comment on lines +42 to 43
Copy link

Copilot AI Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a lightweight debounce implementation instead of importing the entire lodash-es library to reduce bundle size. A simple setTimeout-based debounce function would be sufficient for this use case.

Suggested change
import { debounce } from 'lodash-es';
function debounce(func: (...args: any[]) => void, wait: number): (...args: any[]) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function (...args: any[]) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), wait);
};
}

Copilot uses AI. Check for mistakes.
type ValueChangeListener = (value: StencilUnknown) => void;

Expand Down Expand Up @@ -272,4 +273,16 @@ export class InputController extends ControlledInputController implements Watche
onFocus: this.onFocus.bind(this),
onInput: this.onInput.bind(this),
};

public readonly updateCurrentLengthDebounced = debounce((length: number) => {
setState(this.component, '_currentLengthDebounced', length);
Comment thread
sdvg marked this conversation as resolved.
}, 500);

public hasSoftCharacterLimit() {
return typeof this.component.state._maxLength === 'number' && this.component.state._maxLengthBehavior === 'soft';
}

public hasCounter() {
return this.component.state._hasCounter === true;
}
}
9 changes: 7 additions & 2 deletions packages/components/src/components/input-email/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Generic } from 'adopted-style-sheets';

import type { InputEmailProps, InputEmailWatches, MultiplePropType } from '../../schema';
import { validateMultiple } from '../../schema';
import type { InputEmailProps, InputEmailWatches, MaxLengthBehaviorPropType, MultiplePropType } from '../../schema';
import { validateMultiple, validateMaxLengthBehavior } from '../../schema';

import { InputTextEmailController } from '../input-text/controller';

Expand All @@ -17,8 +17,13 @@ export class InputEmailController extends InputTextEmailController implements In
validateMultiple(this.component, value);
}

public validateMaxLengthBehavior(value?: MaxLengthBehaviorPropType): void {
validateMaxLengthBehavior(this.component, value);
}

public componentWillLoad(): void {
super.componentWillLoad();
this.validateMaxLengthBehavior(this.component._maxLengthBehavior);
this.validateMultiple(this.component._multiple);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test } from '@stencil/playwright';
import { testInputCallbacksAndEvents, testInputValueReflection } from '../../e2e';
import { testInputCallbacksAndEvents, testInputCharacterLimit, testInputValueReflection } from '../../e2e';
import { testInputMessage } from '../../e2e/input-msg';

const COMPONENT_NAME = 'kol-input-email';
Expand All @@ -13,5 +13,6 @@ test.describe(COMPONENT_NAME, () => {
testInputCallbacksAndEvents<HTMLKolInputEmailElement>({
componentName: COMPONENT_NAME,
});
testInputCharacterLimit(COMPONENT_NAME);
testInputMessage<HTMLKolInputEmailElement>(COMPONENT_NAME);
});
42 changes: 27 additions & 15 deletions packages/components/src/components/input-email/shadow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
InputTypeOnOff,
LabelWithExpertSlotPropType,
MsgPropType,
MaxLengthBehaviorPropType,
MultiplePropType,
NamePropType,
ShortKeyPropType,
Expand All @@ -22,7 +23,6 @@ import type {
SyncValueBySelectorPropType,
TooltipAlignPropType,
} from '../../schema';
import { setState } from '../../schema';

import { nonce } from '../../utils/dev.utils';
import { propagateSubmitEventToForm } from '../form/controller';
Expand Down Expand Up @@ -73,9 +73,7 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
};

private readonly onInput = (event: InputEvent) => {
const value = (event.target as HTMLInputElement).value;
setState(this, '_currentLength', value.length);
this._value = value;
this._value = (event.target as HTMLInputElement).value;
this.controller.onFacade.onInput(event);
};

Expand All @@ -84,6 +82,7 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
state: this.state,
class: clsx('kol-input-email', 'email', {
'has-value': this.state._hasValue,
'kol-form-field--has-counter': this.controller.hasSoftCharacterLimit() || this.controller.hasCounter(),
}),
tooltipAlign: this._tooltipAlign,
onClick: () => this.inputRef?.focus(),
Expand All @@ -92,10 +91,13 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
}

private getInputProps(): InputStateWrapperProps {
const ariaDescribedBy = typeof this.state._maxLength === 'number' ? [`${this.state._id}-character-limit-hint`] : undefined; // When a character limit is defined, we provide an additional hint referenced by aria-describedby.

return {
ref: this.catchRef,
state: this.state,
type: 'email',
ariaDescribedBy,
...this.controller.onFacade,
onInput: this.onInput,
onKeyDown: this.onKeyDown,
Expand Down Expand Up @@ -133,16 +135,20 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
@Prop() public _autoComplete?: InputTypeOnOff;

/**
* Makes the element not focusable and ignore all events.
* @TODO: Change type back to `DisabledPropType` after Stencil#4663 has been resolved.
* Shows a character counter for the input element.
*/
@Prop() public _disabled?: boolean = false;
@Prop() public _hasCounter?: boolean = false;

/**
* Shows the character count on the lower border of the input.
* @TODO: Change type back to `HasCounterPropType` after Stencil#4663 has been resolved.
* Defines the behavior when maxLength is set. 'hard' sets the maxlength attribute, 'soft' shows a character counter without preventing input.
*/
@Prop() public _hasCounter?: boolean = false;
@Prop() public _maxLengthBehavior?: MaxLengthBehaviorPropType = 'hard';

/**
* Makes the element not focusable and ignore all events.
* @TODO: Change type back to `DisabledPropType` after Stencil#4663 has been resolved.
*/
@Prop() public _disabled?: boolean = false;

/**
* Hides the error message but leaves it in the DOM for the input's aria-describedby.
Expand Down Expand Up @@ -264,6 +270,7 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
@State() public state: InputEmailStates = {
_autoComplete: 'off',
_currentLength: 0,
_currentLengthDebounced: 0,
_hasValue: false,
_hideMsg: false,
_id: `id-${nonce()}`,
Expand Down Expand Up @@ -296,11 +303,6 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
this.controller.validateDisabled(value);
}

@Watch('_hasCounter')
public validateHasCounter(value?: boolean): void {
this.controller.validateHasCounter(value);
}

@Watch('_hideMsg')
public validateHideMsg(value?: HideMsgPropType): void {
this.controller.validateHideMsg(value);
Expand All @@ -311,6 +313,11 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
this.controller.validateHideLabel(value);
}

@Watch('_hasCounter')
public validateHasCounter(value?: boolean): void {
this.controller.validateHasCounter(value);
}

@Watch('_hint')
public validateHint(value?: string): void {
this.controller.validateHint(value);
Expand Down Expand Up @@ -406,6 +413,11 @@ export class KolInputEmail implements InputEmailAPI, FocusableElement {
this.controller.validateValue(value);
}

@Watch('_maxLengthBehavior')
public validateMaxLengthBehavior(value?: MaxLengthBehaviorPropType): void {
this.controller.validateMaxLengthBehavior(value);
}

public componentWillLoad(): void {
this._touched = this._touched === true;
this.controller.componentWillLoad();
Expand Down
18 changes: 9 additions & 9 deletions packages/components/src/components/input-password/controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { InputPasswordProps, InputPasswordWatches, InputTypeOnOff } from '../../schema';
import type { InputPasswordProps, InputPasswordWatches, InputTypeOnOff, MaxLengthBehaviorPropType } from '../../schema';
import { validateHasCounter, watchBoolean, watchNumber, watchString, watchValidator } from '../../schema';
import { validateMaxLengthBehavior } from '../../schema/props/max-length-behavior';
import type { PasswordVariantPropType } from '../../schema/props/variant/password-variant';
import { validatePasswordVariant } from '../../schema/props/variant/password-variant';

Expand All @@ -18,6 +19,7 @@ export class InputPasswordController extends InputIconController implements Inpu
protected afterSyncCharCounter = () => {
if (typeof this.component._value === 'string' && this.component._value.length > 0) {
this.component.state._currentLength = this.component._value.length;
this.updateCurrentLengthDebounced(this.component._value.length);
}
};

Expand All @@ -32,11 +34,11 @@ export class InputPasswordController extends InputIconController implements Inpu
}

public validateHasCounter(value?: boolean): void {
validateHasCounter(this.component, value, {
hooks: {
afterPatch: this.afterSyncCharCounter,
},
});
validateHasCounter(this.component, value);
}

public validateMaxLengthBehavior(value?: MaxLengthBehaviorPropType): void {
validateMaxLengthBehavior(this.component, value);
}

public validateVariant(value?: PasswordVariantPropType): void {
Expand All @@ -45,9 +47,6 @@ export class InputPasswordController extends InputIconController implements Inpu

public validateMaxLength(value?: number): void {
watchNumber(this.component, '_maxLength', value, {
hooks: {
afterPatch: this.afterSyncCharCounter,
},
min: 0,
});
}
Expand Down Expand Up @@ -82,6 +81,7 @@ export class InputPasswordController extends InputIconController implements Inpu
super.componentWillLoad();
this.validateAutoComplete(this.component._autoComplete);
this.validateHasCounter(this.component._hasCounter);
this.validateMaxLengthBehavior(this.component._maxLengthBehavior);
this.validateMaxLength(this.component._maxLength);
this.validatePattern(this.component._pattern);
this.validatePlaceholder(this.component._placeholder);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { test } from '@stencil/playwright';
import { testInputCallbacksAndEvents, testInputValueReflection } from '../../e2e';
import { testInputCallbacksAndEvents, testInputCharacterLimit, testInputValueReflection } from '../../e2e';
import { testInputMessage } from '../../e2e/input-msg';

const COMPONENT_NAME = 'kol-input-password';
Expand All @@ -14,6 +14,7 @@ test.describe('kol-input-password', () => {
testInputCallbacksAndEvents<HTMLKolInputPasswordElement>({
componentName: COMPONENT_NAME,
});
testInputCharacterLimit(COMPONENT_NAME);
testInputMessage<HTMLKolInputPasswordElement>(COMPONENT_NAME);

test.describe('Password Visibility Toggle', () => {
Expand Down
43 changes: 28 additions & 15 deletions packages/components/src/components/input-password/shadow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import type {
InputTypeOnDefault,
InputTypeOnOff,
LabelWithExpertSlotPropType,
MaxLengthBehaviorPropType,
MsgPropType,
NamePropType,
ShortKeyPropType,
Stringified,
SyncValueBySelectorPropType,
TooltipAlignPropType,
} from '../../schema';
import { devHint, setState } from '../../schema';
import { devHint } from '../../schema';

import { nonce } from '../../utils/dev.utils';
import { propagateSubmitEventToForm } from '../form/controller';
Expand Down Expand Up @@ -77,9 +78,7 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
};

private readonly onInput = (event: InputEvent) => {
const value = (event.target as HTMLInputElement).value;
setState(this, '_currentLength', value.length);
this._value = value;
this._value = (event.target as HTMLInputElement).value;
this.controller.onFacade.onInput(event);
};

Expand All @@ -88,6 +87,7 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
state: this.state,
class: clsx('kol-input-password', 'password', {
'has-value': this.state._hasValue,
'kol-form-field--has-counter': this.controller.hasSoftCharacterLimit() || this.controller.hasCounter(),
}),
tooltipAlign: this._tooltipAlign,
onClick: () => this.inputRef?.focus(),
Expand All @@ -96,10 +96,13 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
}

private getInputProps(): InputStateWrapperProps {
const ariaDescribedBy = typeof this.state._maxLength === 'number' ? [`${this.state._id}-character-limit-hint`] : undefined; // When a character limit is defined, we provide an additional hint referenced by aria-describedby.

return {
ref: this.catchRef,
type: this._passwordVisible ? 'text' : 'password',
state: this.state,
ariaDescribedBy,
...this.controller.onFacade,
onInput: this.onInput,
onKeyDown: this.onKeyDown,
Expand Down Expand Up @@ -159,16 +162,20 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
@Prop() public _autoComplete?: InputTypeOnOff;

/**
* Makes the element not focusable and ignore all events.
* @TODO: Change type back to `DisabledPropType` after Stencil#4663 has been resolved.
* Shows a character counter for the input element.
*/
@Prop() public _disabled?: boolean = false;
@Prop() public _hasCounter?: boolean = false;

/**
* Shows the character count on the lower border of the input.
* @TODO: Change type back to `HasCounterPropType` after Stencil#4663 has been resolved.
* Defines the behavior when maxLength is set. 'hard' sets the maxlength attribute, 'soft' shows a character counter without preventing input.
*/
@Prop() public _hasCounter?: boolean = false;
@Prop() public _maxLengthBehavior?: MaxLengthBehaviorPropType = 'hard';

/**
* Makes the element not focusable and ignore all events.
* @TODO: Change type back to `DisabledPropType` after Stencil#4663 has been resolved.
*/
@Prop() public _disabled?: boolean = false;

/**
* Hides the error message but leaves it in the DOM for the input's aria-describedby.
Expand Down Expand Up @@ -285,6 +292,7 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
@State() public state: InputPasswordStates = {
_autoComplete: 'off',
_currentLength: 0,
_currentLengthDebounced: 0,
_hasValue: false,
_hideMsg: false,
_id: `id-${nonce()}`,
Expand Down Expand Up @@ -315,6 +323,11 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
}
}

@Watch('_maxLengthBehavior')
public validateMaxLengthBehavior(value?: MaxLengthBehaviorPropType): void {
this.controller.validateMaxLengthBehavior(value);
}

@Watch('_disabled')
public validateDisabled(value?: boolean): void {
this.controller.validateDisabled(value);
Expand All @@ -324,11 +337,6 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
this.controller.validateVariant(value);
}

@Watch('_hasCounter')
public validateHasCounter(value?: boolean): void {
this.controller.validateHasCounter(value);
}

@Watch('_hideMsg')
public validateHideMsg(value?: HideMsgPropType): void {
this.controller.validateHideMsg(value);
Expand All @@ -339,6 +347,11 @@ export class KolInputPassword implements InputPasswordAPI, FocusableElement {
this.controller.validateHideLabel(value);
}

@Watch('_hasCounter')
public validateHasCounter(value?: boolean): void {
this.controller.validateHasCounter(value);
}

@Watch('_hint')
public validateHint(value?: string): void {
this.controller.validateHint(value);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { test } from '@stencil/playwright';
import { testInputCallbacksAndEvents, testInputValueReflection } from '../../e2e';
import { testInputCallbacksAndEvents, testInputCharacterLimit, testInputValueReflection } from '../../e2e';
import { testInputMessage } from '../../e2e/input-msg';

const COMPONENT_NAME = 'kol-input-text';
Expand Down Expand Up @@ -29,5 +29,6 @@ test.describe('kol-input-text', () => {
testInputCallbacksAndEvents<HTMLKolInputTextElement>({
componentName: COMPONENT_NAME,
});
testInputCharacterLimit(COMPONENT_NAME);
testInputMessage<HTMLKolInputTextElement>(COMPONENT_NAME);
});
Loading
Loading