From 6bc56d3d701b93ce4672dfda8eafd2e5070499b8 Mon Sep 17 00:00:00 2001 From: Matteo Date: Wed, 26 Nov 2025 13:33:32 +0100 Subject: [PATCH] chore(design-system, web-app-files, web-pkg, web-runtime): [OCISDEV-496] ui improvements --- .../__snapshots__/OcDatepicker.spec.ts.snap | 2 +- .../src/components/OcIcon/OcIcon.vue | 1 + .../OcTextInput/OcTextInput.spec.ts | 20 +++++---- .../components/OcTextInput/OcTextInput.vue | 16 ++++++- .../__snapshots__/OcTextInput.spec.ts.snap | 8 ++-- .../OcTextInputPassword.vue | 44 +++++++++++++++++-- .../Shares/ExpirationDateIndicator.vue | 8 +++- .../SideBar/Shares/Links/ListItem.vue | 1 + .../src/components/CreateLinkModal.vue | 1 + .../__snapshots__/DateFilter.spec.ts.snap | 8 ++-- 10 files changed, 85 insertions(+), 24 deletions(-) diff --git a/packages/design-system/src/components/OcDatepicker/__snapshots__/OcDatepicker.spec.ts.snap b/packages/design-system/src/components/OcDatepicker/__snapshots__/OcDatepicker.spec.ts.snap index 19bbfd23ee9..c0294f9dacf 100644 --- a/packages/design-system/src/components/OcDatepicker/__snapshots__/OcDatepicker.spec.ts.snap +++ b/packages/design-system/src/components/OcDatepicker/__snapshots__/OcDatepicker.spec.ts.snap @@ -6,7 +6,7 @@ exports[`OcDatePicker > renders 1`] = ` -
+ diff --git a/packages/design-system/src/components/OcIcon/OcIcon.vue b/packages/design-system/src/components/OcIcon/OcIcon.vue index 70dd1acbd1d..c914ccde30e 100644 --- a/packages/design-system/src/components/OcIcon/OcIcon.vue +++ b/packages/design-system/src/components/OcIcon/OcIcon.vue @@ -11,6 +11,7 @@ { passwordPolicy = { active: false, pass: false } ) { const passwordPolicyMock = mock() + const policyRules = [ + { + code: 'minLength', + message: '%{param1}+ letters', + format: ['8'], + verified: passwordPolicy.pass + } + ] + passwordPolicyMock.rules = policyRules passwordPolicyMock.missing.mockReturnValue({ - rules: [ - { - code: 'minLength', - message: '%{param1}+ letters', - format: ['8'], - verified: passwordPolicy.pass - } - ] + rules: policyRules }) - passwordPolicyMock.check.mockReturnValueOnce(passwordPolicy.pass) + passwordPolicyMock.check.mockReturnValue(passwordPolicy.pass) if (passwordPolicy.active) { options.props = { diff --git a/packages/design-system/src/components/OcTextInput/OcTextInput.vue b/packages/design-system/src/components/OcTextInput/OcTextInput.vue index 2943708ca8d..b54b99f5296 100644 --- a/packages/design-system/src/components/OcTextInput/OcTextInput.vue +++ b/packages/design-system/src/components/OcTextInput/OcTextInput.vue @@ -53,6 +53,9 @@ 'oc-text-input-warning': !!warningMessage, 'oc-text-input-danger': !!errorMessage }" + :aria-live="errorMessage || warningMessage ? 'assertive' : 'off'" + aria-atomic="true" + role="alert" > { const additionalAttributes = computed(() => { const additionalAttrs: Record = {} + const describedByIds: string[] = [] + if (!!warningMessage || !!errorMessage || !!descriptionMessage) { - additionalAttrs['aria-describedby'] = messageId + describedByIds.push(messageId.value) + } + + if (type === 'password' && Object.keys(passwordPolicy).length) { + describedByIds.push(`${id}-password-policy`) } + + if (describedByIds.length > 0) { + additionalAttrs['aria-describedby'] = describedByIds.join(' ') + } + // FIXME: placeholder usage is discouraged, we need to find a better UX concept if (defaultValue) { additionalAttrs['placeholder'] = defaultValue diff --git a/packages/design-system/src/components/OcTextInput/__snapshots__/OcTextInput.spec.ts.snap b/packages/design-system/src/components/OcTextInput/__snapshots__/OcTextInput.spec.ts.snap index 2ae6d81f8d4..83f96950719 100644 --- a/packages/design-system/src/components/OcTextInput/__snapshots__/OcTextInput.spec.ts.snap +++ b/packages/design-system/src/components/OcTextInput/__snapshots__/OcTextInput.spec.ts.snap @@ -6,7 +6,7 @@ exports[`OcTextInput > password input field > password policy > displays error s
-
-
+
Password requirements: 8+ letters
8+ letters
@@ -35,7 +35,7 @@ exports[`OcTextInput > password input field > password policy > displays success
-
-
+
Password meets all requirements
8+ letters
diff --git a/packages/design-system/src/components/OcTextInputPassword/OcTextInputPassword.vue b/packages/design-system/src/components/OcTextInputPassword/OcTextInputPassword.vue index 4845e3d3e06..1217b2caba6 100644 --- a/packages/design-system/src/components/OcTextInputPassword/OcTextInputPassword.vue +++ b/packages/design-system/src/components/OcTextInputPassword/OcTextInputPassword.vue @@ -14,6 +14,7 @@ :value="value" :type="showPassword ? 'text' : 'password'" :disabled="disabled" + :aria-invalid="hasPasswordPolicyViolation" @input="$emit('input', $event)" @change="$emit('change', $event)" /> @@ -52,7 +53,14 @@
-
+
+
attrs.id as string) +const passwordPolicyId = computed(() => `${inputId.value}-password-policy`) + const showPasswordPolicyInformation = computed(() => { - return !!Object.keys(passwordPolicy.rules || {}).length + return !!Object.keys(passwordPolicy?.rules || {}).length }) const testedPasswordPolicy = computed(() => { return passwordPolicy.missing(unref(value)) }) +const hasPasswordPolicyViolation = computed(() => { + if (!Object.keys(passwordPolicy?.rules || {})?.length) { + return false + } + // Check if password has any policy violations or if there's an explicit error + return !passwordPolicy.check(unref(value)) || hasError +}) + +const passwordPolicySummary = computed(() => { + const tested = testedPasswordPolicy.value + if (!tested || !tested.rules || tested.rules.length === 0) { + return '' + } + + const unmetRequirements = tested.rules.filter((rule) => !rule.verified) + if (unmetRequirements.length === 0) { + return $gettext('Password meets all requirements') + } + + const messages = unmetRequirements.map((rule) => getPasswordPolicyRuleMessage(rule)) + return $gettext( + 'Password requirements: %{requirements}', + { requirements: messages.join(', ') }, + true + ) +}) + const getPasswordPolicyRuleMessage = (rule: PasswordPolicyRule) => { const paramObj: Record = {} @@ -170,7 +208,7 @@ const generatePassword = () => { watch( () => value, (value) => { - if (!Object.keys(passwordPolicy).length) { + if (!Object.keys(passwordPolicy?.rules || {})?.length) { return } diff --git a/packages/web-app-files/src/components/SideBar/Shares/ExpirationDateIndicator.vue b/packages/web-app-files/src/components/SideBar/Shares/ExpirationDateIndicator.vue index 8a16b2d3c50..89b08c57801 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/ExpirationDateIndicator.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/ExpirationDateIndicator.vue @@ -1,7 +1,11 @@ diff --git a/packages/web-app-files/src/components/SideBar/Shares/Links/ListItem.vue b/packages/web-app-files/src/components/SideBar/Shares/Links/ListItem.vue index a4f2ee5e4bc..17ec278ccc8 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Links/ListItem.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Links/ListItem.vue @@ -33,6 +33,7 @@ name="lock-password" class="oc-files-file-link-has-password oc-mr-xs" fill-type="line" + :accessible-label="$gettext('This link is password-protected')" :aria-label="$gettext('This link is password-protected')" role="img" /> diff --git a/packages/web-pkg/src/components/CreateLinkModal.vue b/packages/web-pkg/src/components/CreateLinkModal.vue index eceb8bbd19d..f9b4022303f 100644 --- a/packages/web-pkg/src/components/CreateLinkModal.vue +++ b/packages/web-pkg/src/components/CreateLinkModal.vue @@ -11,6 +11,7 @@
-
+ @@ -67,7 +67,7 @@ exports[`DateFilter > can use a custom attribute as display name 1`] = `
-
+ @@ -142,7 +142,7 @@ exports[`DateFilter > renders all items 1`] = `
-
+ @@ -152,7 +152,7 @@ exports[`DateFilter > renders all items 1`] = `
-
+