diff --git a/apps/files_sharing/src/views/SharingDetailsTab.vue b/apps/files_sharing/src/views/SharingDetailsTab.vue
index c1cc67ca72289..aa858f0a70c33 100644
--- a/apps/files_sharing/src/views/SharingDetailsTab.vue
+++ b/apps/files_sharing/src/views/SharingDetailsTab.vue
@@ -167,6 +167,7 @@
+ @update:model-value="onExpirationChange"
+ @change="checkExpirationDateValidity" />
diff --git a/apps/settings/src/components/Users/UserFormFields.vue b/apps/settings/src/components/Users/UserFormFields.vue
index 2e408e8329b6e..5e41946e968a9 100644
--- a/apps/settings/src/components/Users/UserFormFields.vue
+++ b/apps/settings/src/components/Users/UserFormFields.vue
@@ -30,7 +30,9 @@
autocomplete="off"
spellcheck="false"
pattern="[a-zA-Z0-9 _\.@\-']+"
- :required="fieldConfig.username?.required" />
+ :required="fieldConfig.username?.required"
+ @input="updateUsernameValidity"
+ @blur="updateUsernameValidity" />
void } | null>(null)
const minPasswordLength = computed(() => store.getters.getPasswordPolicyMinLength)
+/**
+ * Customize the browser-native constraint validation message.
+ *
+ * @param event Input/blur event from the underlying input element
+ */
+function updateUsernameValidity(event: Event) {
+ const input = event.target as HTMLInputElement | null
+ if (!input) {
+ return
+ }
+
+ // Clear first so native constraint flags are evaluated without a stale customError.
+ input.setCustomValidity('')
+ if (input.validity.patternMismatch) {
+ input.setCustomValidity(t('settings', 'Only letters, numbers, spaces, and _.@-\' are allowed'))
+ }
+}
+
// Errors not bound to a dedicated input, shown in the catch-all live region.
const unhandledErrors = computed(() => {
const handled = new Set(['displayName', 'password', 'email'])
diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue
index 4a350907db74c..c71d74e93865c 100644
--- a/core/src/components/login/LoginForm.vue
+++ b/core/src/components/login/LoginForm.vue
@@ -63,7 +63,7 @@
:spellchecking="false"
:autocomplete="autoCompleteAllowed ? 'username' : 'off'"
required
- :error="userNameInputLengthIs255"
+ :error="userNameError"
:helper-text="userInputHelperText"
data-login-form-input-user
@change="updateUsername" />
@@ -241,6 +241,10 @@ export default {
|| this.throttleDelay > 5000
},
+ userNameError() {
+ return this.isError || this.userNameInputLengthIs255
+ },
+
errorLabel() {
if (this.invalidPassword) {
return t('core', 'Wrong login or password.')
diff --git a/core/src/tests/components/Login/LoginForm.spec.ts b/core/src/tests/components/Login/LoginForm.spec.ts
index b471c9bdf81df..9c6299bd2d40f 100644
--- a/core/src/tests/components/Login/LoginForm.spec.ts
+++ b/core/src/tests/components/Login/LoginForm.spec.ts
@@ -66,6 +66,18 @@ describe('core: LoginForm', () => {
expect(input.value).toBe('test-user')
})
+ it('marks username field as error when login credentials are invalid', () => {
+ const page = render(LoginForm, {
+ props: {
+ errors: ['invalidpassword'],
+ username: 'wrong-user',
+ },
+ })
+
+ const input = page.getByRole('textbox', { name: /Account name or email/ })
+ expect(input.closest('.input-field--error')).not.toBeNull()
+ })
+
describe('', () => {
beforeAll(() => {
vi.useFakeTimers()
diff --git a/tests/playwright/e2e/login/login.spec.ts b/tests/playwright/e2e/login/login.spec.ts
index 6101e80f2e009..be003950a907f 100644
--- a/tests/playwright/e2e/login/login.spec.ts
+++ b/tests/playwright/e2e/login/login.spec.ts
@@ -45,24 +45,26 @@ test.describe('Login', () => {
await expect(page).toHaveURL(/apps\/dashboard(\/|$)/)
})
- test('wrong password shows error and marks password field invalid', async ({ page, user }) => {
+ test('wrong password shows error and marks both fields invalid', async ({ page, user }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login(user.userId, `${user.password}--wrong`)
await expect(page).toHaveURL(/\/login/)
await expect(page.getByText(/Wrong login or password/i)).toBeVisible()
- await expect(loginPage.passwordInput().and(page.locator(':invalid'))).toHaveCount(1)
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.usernameInput() })).toBeVisible()
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.passwordInput() })).toBeVisible()
})
- test('wrong account name shows error and marks password field invalid', async ({ page, user }) => {
+ test('wrong account name shows error and marks both fields invalid', async ({ page, user }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login(`${user.userId}--wrong`, user.password)
await expect(page).toHaveURL(/\/login/)
await expect(page.getByText(/Wrong login or password/i)).toBeVisible()
- await expect(loginPage.passwordInput().and(page.locator(':invalid'))).toHaveCount(1)
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.usernameInput() })).toBeVisible()
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.passwordInput() })).toBeVisible()
})
test('disabled account shows disabled error', async ({ page, disabledUser }) => {
@@ -72,7 +74,8 @@ test.describe('Login', () => {
await expect(page).toHaveURL(/\/login/)
await expect(page.getByText(/Account.*disabled/i)).toBeVisible()
- await expect(loginPage.passwordInput().and(page.locator(':invalid'))).toHaveCount(1)
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.usernameInput() })).toBeVisible()
+ await expect(page.locator('.input-field--error').filter({ has: loginPage.passwordInput() })).toBeVisible()
})
test('logout redirects to the login page', async ({ page, context, user }) => {