Skip to content

Commit

Permalink
fix(OnyxInput, OnyxTextarea): maxlength doesn't restrict the user f…
Browse files Browse the repository at this point in the history
…rom typing more than the allowed characters anymore (#2498)

Relates to #2308 

- `maxlength` doesn't restrict the user anymore from typing more than
the allowed characters.
- This is preferable, as the user can now paste or write content without
it being cut off.
- The previous behavior, which doesn't allow for typing/copying more
characters, can be achieved by setting `strictMaxlength`.
  • Loading branch information
JoCa96 authored Jan 15, 2025
1 parent a0ef1a4 commit 808e638
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 135 deletions.
6 changes: 6 additions & 0 deletions .changeset/nervous-paws-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"sit-onyx": patch
---

fix(OnyxInput, OnyxTextarea): `maxlength` doesn't restrict the user from typing more than the allowed characters.
The previous behavior, which restricts the user from typing more than the allowed characters, can be achieved by setting `strictMaxlength`.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts" setup>
<script lang="ts" setup generic="T">
import { computed, useId } from "vue";
import { useRequired } from "../../composables/required";
import OnyxInfoTooltip from "../OnyxInfoTooltip/OnyxInfoTooltip.vue";
Expand All @@ -12,9 +12,14 @@ const props = withDefaults(defineProps<OnyxFormElementProps>(), {
const { requiredMarkerClass, requiredTypeClass } = useRequired(props);
/**
* Current value of the input.
*/
const modelValue = defineModel<T>();
const counterText = computed(() => {
if (props.withCounter && props.maxlength) {
const text = props.modelValue?.toString() || "";
const text = modelValue.value?.toString() ?? "";
return `${text.length}/${props.maxlength}`;
}
return undefined;
Expand Down
27 changes: 7 additions & 20 deletions packages/sit-onyx/src/components/OnyxFormElement/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { RequiredMarkerProp } from "../../composables/required";
import type { CustomMessageType, FormMessages } from "../../composables/useCustomValidity";
import type { SharedTextInputProps } from "../../composables/useLenientMaxLengthValidation";

export type OnyxFormElementProps = Omit<SharedFormElementProps, "error" | "message" | "success"> & {
errorMessages?: FormMessages;
message?: FormMessages;
successMessages?: FormMessages;
};
export type OnyxFormElementProps = Omit<SharedFormElementProps, "error" | "message" | "success"> &
SharedTextInputProps & {
errorMessages?: FormMessages;
message?: FormMessages;
successMessages?: FormMessages;
};

export type SharedFormElementProps = RequiredMarkerProp & {
/**
Expand All @@ -19,10 +21,6 @@ export type SharedFormElementProps = RequiredMarkerProp & {
* Used to reference the input in JavaScript or in submitted form data.
*/
name?: string;
/**
* Current value of the form element.
*/
modelValue?: unknown;
/**
* Label to show above the form element. Required due to accessibility / screen readers.
* If you want to visually hide the label, use the `hideLabel` property.
Expand Down Expand Up @@ -53,15 +51,4 @@ export type SharedFormElementProps = RequiredMarkerProp & {
* Success messages that inform about the state of form components
*/
success?: CustomMessageType;
/**
* Maximum number of characters that are allowed to be entered.
* Warning: when the value is (pre)set programmatically,
* the input invalidity will not be detected by the browser, it will only turn invalid
* as soon as a user interacts with the input (types something).
*/
maxlength?: number;
/**
* If `true`, a character counter will be displayed if `maxLength` is set.
*/
withCounter?: boolean;
};
3 changes: 3 additions & 0 deletions packages/sit-onyx/src/components/OnyxInput/OnyxInput.ct.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { DENSITIES } from "../../composables/density";
import type { FormMessages } from "../../composables/useCustomValidity";
import { testMaxLengthBehavior } from "../../composables/useLenientMaxLengthValidation.ct-utils";
import { expect, test } from "../../playwright/a11y";
import { executeMatrixScreenshotTest } from "../../playwright/screenshots";
import { createFormElementUtils } from "../OnyxFormElement/OnyxFormElement.ct-utils";
Expand Down Expand Up @@ -430,3 +431,5 @@ test("should show correct message", async ({ mount }) => {
await expect(successMessageElement).toBeHidden();
await expect(errorMessageElement).toBeVisible();
});

testMaxLengthBehavior(OnyxInput);
27 changes: 13 additions & 14 deletions packages/sit-onyx/src/components/OnyxInput/OnyxInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { computed, useTemplateRef } from "vue";
import { useDensity } from "../../composables/density";
import { getFormMessages, useCustomValidity } from "../../composables/useCustomValidity";
import { useErrorClass } from "../../composables/useErrorClass";
import { useLenientMaxLengthValidation } from "../../composables/useLenientMaxLengthValidation";
import { SKELETON_INJECTED_SYMBOL, useSkeletonContext } from "../../composables/useSkeletonState";
import { injectI18n } from "../../i18n";
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core";
Expand All @@ -28,10 +29,6 @@ const props = withDefaults(defineProps<OnyxInputProps>(), {
});
const emit = defineEmits<{
/**
* Emitted when the current value changes.
*/
"update:modelValue": [value: string];
/**
* Emitted when the validity state of the input changes.
*/
Expand All @@ -51,18 +48,20 @@ const slots = defineSlots<{
trailing?(): unknown;
}>();
/**
* Current value of the input.
*/
const modelValue = defineModel<string>({ default: "" });
const { t } = injectI18n();
const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit });
const { maxLength, maxLengthError } = useLenientMaxLengthValidation({ modelValue, props });
const customError = computed(() => props.customError ?? maxLengthError.value);
const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit, customError });
const successMessages = computed(() => getFormMessages(props.success));
const messages = computed(() => getFormMessages(props.message));
const { densityClass } = useDensity(props);
/**
* Current value (with getter and setter) that can be used as "v-model" for the native input.
*/
const value = defineModel<string>({ default: "" });
const patternSource = computed(() => {
if (props.pattern instanceof RegExp) return props.pattern.source;
return props.pattern;
Expand Down Expand Up @@ -97,7 +96,7 @@ const errorClass = useErrorClass(showError);
<input
:id="inputId"
ref="input"
v-model="value"
v-model="modelValue"
v-custom-validity
:placeholder="props.placeholder"
class="onyx-input__native"
Expand All @@ -110,20 +109,20 @@ const errorClass = useErrorClass(showError);
:pattern="patternSource"
:readonly="props.readonly"
:disabled="disabled || props.loading"
:maxlength="maxLength"
:minlength="props.minlength"
:maxlength="props.maxlength"
:aria-label="props.hideLabel ? props.label : undefined"
:title="props.hideLabel ? props.label : undefined"
/>

<button
v-if="!props.hideClearIcon && value !== ''"
v-if="!props.hideClearIcon && modelValue !== ''"
type="button"
class="onyx-input__clear"
:aria-label="t('input.clear')"
:title="t('input.clear')"
tabindex="-1"
@click="() => (value = '')"
@click="() => (modelValue = '')"
>
<OnyxIcon :icon="xSmall" />
</button>
Expand Down
47 changes: 3 additions & 44 deletions packages/sit-onyx/src/components/OnyxInput/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DensityProp } from "../../composables/density";
import type { RequiredMarkerProp } from "../../composables/required";
import type { CustomValidityProp } from "../../composables/useCustomValidity";
import type { SharedTextInputProps } from "../../composables/useLenientMaxLengthValidation";
import type { SkeletonInjected } from "../../composables/useSkeletonState";
import type { AutofocusProp } from "../../types";
import type { FormInjectedProps } from "../OnyxForm/OnyxForm.core";
Expand All @@ -10,12 +11,9 @@ export type OnyxInputProps = FormInjectedProps &
DensityProp &
RequiredMarkerProp &
CustomValidityProp &
Omit<SharedFormElementProps, "modelValue" | "errorMessages"> &
SharedFormElementProps &
SharedTextInputProps &
AutofocusProp & {
/**
* Current value of the input.
*/
modelValue?: string;
/**
* Input type.
*/
Expand All @@ -24,23 +22,6 @@ export type OnyxInputProps = FormInjectedProps &
* Placeholder to show when the value is empty.
*/
placeholder?: string;
/**
* If and how text should be automatically be capitalized when using non-physical keyboards
* (such as virtual keyboard on mobile devices or voice input).
*
* Has no effect when `type` is set to "url", "email" or "password".
*
* @see [MDN autocapitalize](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize)
*/
autocapitalize?: Autocapitalize;
/**
* Specify how to provide automated assistance in filling out the input.
* Some autocomplete values might required specific browser permissions to be allowed by the user.
* Also browsers might require a `name` property.
*
* @see [MDN autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)
*/
autocomplete?: Autocomplete;
/**
* Pattern the value must match to be valid.
*/
Expand All @@ -61,14 +42,6 @@ export type OnyxInputProps = FormInjectedProps &
* Whether to hide the clear icon when the input is filled and focused.
*/
hideClearIcon?: boolean;

/**
* Minimum number of characters that have to to be entered.
* Warning: when the value is (pre)set programmatically,
* the input invalidity will not be detected by the browser, it will only turn invalid
* as soon as a user interacts with the input (types something).
*/
minlength?: number;
/**
* Whether to show a skeleton input.
*/
Expand All @@ -77,17 +50,3 @@ export type OnyxInputProps = FormInjectedProps &

export const INPUT_TYPES = ["email", "password", "search", "tel", "text", "url"] as const;
export type InputType = (typeof INPUT_TYPES)[number];

/**
* @see [MDN autocapitalize](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize)
*/
export const AUTOCAPITALIZE = ["none", "sentences", "words", "characters"] as const;
export type Autocapitalize = (typeof AUTOCAPITALIZE)[number];

/**
* Same as TypeScript native "Autofill" type but without "AutoFillSection" because
* the Vue compiler currently can not handle it (too complex union type).
*
* @since TypeScript version 5.2
*/
export type Autocomplete = Exclude<AutoFill, AutoFillSection | "">;
2 changes: 1 addition & 1 deletion packages/sit-onyx/src/components/OnyxSelectInput/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type OnyxSelectInputProps = FormInjectedProps &
RequiredMarkerProp &
CustomValidityProp &
AutofocusProp &
Omit<SharedFormElementProps, "modelValue" | "maxlength" | "withCounter"> & {
Omit<SharedFormElementProps, "maxlength" | "withCounter"> & {
/**
* Current label(s) of the select.
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/sit-onyx/src/components/OnyxStepper/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { DensityProp } from "../../composables/density";
import type { CustomValidityProp } from "../../composables/useCustomValidity";
import type { Autocomplete } from "../../composables/useLenientMaxLengthValidation";
import type { SkeletonInjected } from "../../composables/useSkeletonState";
import type { AutofocusProp } from "../../types";
import type { FormInjectedProps } from "../OnyxForm/OnyxForm.core";
import type { SharedFormElementProps } from "../OnyxFormElement/types";
import type { Autocomplete } from "../OnyxInput/types";

export type OnyxStepperProps = FormInjectedProps &
DensityProp &
CustomValidityProp &
Omit<SharedFormElementProps, "modelValue" | "errorMessages" | "withCounter" | "maxlength"> &
Omit<SharedFormElementProps, "errorMessages" | "withCounter" | "maxlength"> &
AutofocusProp & {
/**
* Placeholder to show when the value is empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DENSITIES } from "../../composables/density";
import { testMaxLengthBehavior } from "../../composables/useLenientMaxLengthValidation.ct-utils";
import { expect, test } from "../../playwright/a11y";
import { executeMatrixScreenshotTest } from "../../playwright/screenshots";
import { createFormElementUtils } from "../OnyxFormElement/OnyxFormElement.ct-utils";
Expand Down Expand Up @@ -508,3 +509,5 @@ test("should show correct message", async ({ mount }) => {
await expect(successMessageElement).toBeHidden();
await expect(errorMessageElement).toBeVisible();
});

testMaxLengthBehavior(OnyxTextarea);
33 changes: 12 additions & 21 deletions packages/sit-onyx/src/components/OnyxTextarea/OnyxTextarea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { computed, useTemplateRef } from "vue";
import { useDensity } from "../../composables/density";
import { getFormMessages, useCustomValidity } from "../../composables/useCustomValidity";
import { useErrorClass } from "../../composables/useErrorClass";
import { useLenientMaxLengthValidation } from "../../composables/useLenientMaxLengthValidation";
import { SKELETON_INJECTED_SYMBOL, useSkeletonContext } from "../../composables/useSkeletonState";
import { FORM_INJECTED_SYMBOL, useFormContext } from "../OnyxForm/OnyxForm.core";
import OnyxFormElement from "../OnyxFormElement/OnyxFormElement.vue";
import OnyxSkeleton from "../OnyxSkeleton/OnyxSkeleton.vue";
import type { OnyxTextareaProps } from "./types";
const props = withDefaults(defineProps<OnyxTextareaProps>(), {
modelValue: "",
required: false,
autocapitalize: "sentences",
readonly: false,
Expand All @@ -21,28 +21,24 @@ const props = withDefaults(defineProps<OnyxTextareaProps>(), {
});
const emit = defineEmits<{
/**
* Emitted when the current value changes.
*/
"update:modelValue": [value: string];
/**
* Emitted when the validity state of the input changes.
*/
validityChange: [validity: ValidityState];
}>();
const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit });
/**
* Current value of the textarea.
*/
const modelValue = defineModel<string>({ default: "" });
const { maxLength, maxLengthError } = useLenientMaxLengthValidation({ props, modelValue });
const customError = computed(() => props.customError ?? maxLengthError.value);
const { vCustomValidity, errorMessages } = useCustomValidity({ props, emit, customError });
const { densityClass } = useDensity(props);
const successMessages = computed(() => getFormMessages(props.success));
const messages = computed(() => getFormMessages(props.message));
/**
* Current value (with getter and setter) that can be used as "v-model" for the native input.
*/
const value = computed({
get: () => props.modelValue,
set: (value) => emit("update:modelValue", value),
});
/**
* Current CSS variables for the autosize min/max height.
Expand Down Expand Up @@ -93,15 +89,11 @@ defineExpose({ input });
:error-messages="errorMessages"
>
<template #default="{ id }">
<div class="onyx-textarea__wrapper" :data-autosize-value="value">
<!-- eslint-disable vuejs-accessibility/no-autofocus -
We want to provide the flexibility to have the autofocus property.
The JSDoc description includes a warning that it should be used carefully.
-->
<div class="onyx-textarea__wrapper" :data-autosize-value="modelValue">
<textarea
:id="id"
ref="input"
v-model="value"
v-model="modelValue"
v-custom-validity
class="onyx-textarea__native"
:class="{ 'onyx-textarea__native--no-resize': props.disableManualResize }"
Expand All @@ -113,12 +105,11 @@ defineExpose({ input });
:readonly="props.readonly"
:disabled="disabled"
:minlength="props.minlength"
:maxlength="props.maxlength"
:maxlength="maxLength"
:aria-label="props.hideLabel ? props.label : undefined"
:title="props.hideLabel ? props.label : undefined"
@input="handleInput"
></textarea>
<!-- eslint-enable vuejs-accessibility/no-autofocus -->
</div>
</template>
</OnyxFormElement>
Expand Down
1 change: 0 additions & 1 deletion packages/sit-onyx/src/components/OnyxTextarea/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type OnyxTextareaProps = DensityProp &
| "label"
| "labelTooltip"
| "hideLabel"
| "modelValue"
| "placeholder"
| "autocapitalize"
| "autofocus"
Expand Down
Loading

0 comments on commit 808e638

Please sign in to comment.