From 93e21e7a8726bc9f58a6e193c528adeecad1c6b5 Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Thu, 12 Sep 2024 14:05:45 +0200 Subject: [PATCH] fix(a11y): focus trap for contextual helpers Adds a focus trap to contextual helpers to prevent the user from focusing elements in the background. --- .../unreleased/enhancement-a11y-improvements | 8 ++ .../OcContextualHelper/OcContextualHelper.vue | 9 +-- .../components/OcInfoDrop/OcInfoDrop.spec.ts | 21 ++++- .../src/components/OcInfoDrop/OcInfoDrop.vue | 80 ++++++++++--------- .../src/components/OcModal/OcModal.vue | 3 +- .../_OcTextInputPassword.vue | 6 +- packages/design-system/src/helpers/types.ts | 2 +- .../components/Users/SideBar/DetailsPanel.vue | 7 +- .../src/components/Shares/CopyPrivateLink.vue | 2 +- .../SideBar/Shares/Collaborators/ListItem.vue | 1 + .../Shares/Collaborators/RoleDropdown.vue | 1 + .../src/views/spaces/Projects.vue | 6 +- .../CopyPrivateLink.spec.ts.snap | 7 +- .../src/views/ConnectionsPanel.vue | 3 +- .../src/views/IncomingInvitations.vue | 3 +- .../src/views/OutgoingInvitations.vue | 3 +- .../src/components/CreateShortcutModal.vue | 2 + .../src/components/Spaces/QuotaModal.vue | 2 +- 18 files changed, 112 insertions(+), 54 deletions(-) create mode 100644 changelog/unreleased/enhancement-a11y-improvements diff --git a/changelog/unreleased/enhancement-a11y-improvements b/changelog/unreleased/enhancement-a11y-improvements new file mode 100644 index 00000000000..aa5c9ccf7a1 --- /dev/null +++ b/changelog/unreleased/enhancement-a11y-improvements @@ -0,0 +1,8 @@ +Enhancement: Accessibility improvements + +The following accessibility improvements have been made: + +- Contextual helpers now have a focus trap, meaning the user can't focus elements in the background while the contextual helper is showing. + +https://github.com/owncloud/web/pull/11574 +https://github.com/owncloud/web/issues/10725 diff --git a/packages/design-system/src/components/OcContextualHelper/OcContextualHelper.vue b/packages/design-system/src/components/OcContextualHelper/OcContextualHelper.vue index 0298cfb4550..0b0818ffd56 100644 --- a/packages/design-system/src/components/OcContextualHelper/OcContextualHelper.vue +++ b/packages/design-system/src/components/OcContextualHelper/OcContextualHelper.vue @@ -13,7 +13,7 @@ import uniqueId from '../../utils/uniqueId' import OcButton from '../OcButton/OcButton.vue' import OcIcon from '../OcIcon/OcIcon.vue' import OcInfoDrop from '../OcInfoDrop/OcInfoDrop.vue' -import { ContextualHelperData } from '../../helpers' +import { ContextualHelperDataListItem } from '../../helpers' export default defineComponent({ name: 'OcContextualHelper', @@ -25,8 +25,7 @@ export default defineComponent({ */ title: { type: String, - required: false, - default: '' + required: true }, /** * Text at the beginning @@ -40,9 +39,9 @@ export default defineComponent({ * List element */ list: { - type: Array as PropType, + type: Array as PropType, required: false, - default: (): ContextualHelperData[] => [] + default: (): ContextualHelperDataListItem[] => [] }, /** * Text at the end diff --git a/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.spec.ts b/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.spec.ts index 55ee0da7eb2..41be326625f 100644 --- a/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.spec.ts +++ b/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.spec.ts @@ -1,10 +1,15 @@ +import { FocusTrap } from 'focus-trap-vue' +import OcDrop from '../OcDrop/OcDrop.vue' import OcInfoDrop from './OcInfoDrop.vue' import { PartialComponentProps, defaultPlugins, shallowMount } from 'web-test-helpers' describe('OcInfoDrop', () => { function getWrapperWithProps(props: PartialComponentProps) { return shallowMount(OcInfoDrop, { - props, + props: { + ...props, + title: props.title || 'test-title' + }, global: { plugins: [...defaultPlugins()], renderStubDefaultSlot: true, @@ -45,5 +50,19 @@ describe('OcInfoDrop', () => { const wrapper = getWrapperWithProps({ endText: 'test-my-text' }) expect(wrapper.find('.info-text-end').text()).toBe('test-my-text') }) + describe('focus trap', () => { + it('is active if the drop is open', async () => { + const wrapper = getWrapperWithProps({ title: 'title' }) + wrapper.findComponent('oc-drop-stub').vm.$emit('show-drop') + await wrapper.vm.$nextTick() + const focusTrap = wrapper.findComponent('focus-trap-stub') + expect(focusTrap.props('active')).toBeTruthy() + }) + it('is not active if the drop is closed', () => { + const wrapper = getWrapperWithProps({ title: 'title' }) + const focusTrap = wrapper.findComponent('focus-trap-stub') + expect(focusTrap.props('active')).toBeFalsy() + }) + }) }) }) diff --git a/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.vue b/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.vue index dec894d3e0b..d7098051692 100644 --- a/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.vue +++ b/packages/design-system/src/components/OcInfoDrop/OcInfoDrop.vue @@ -6,57 +6,57 @@ :toggle="toggle" :mode="mode" close-on-click + @hide-drop="() => (dropOpen = false)" + @show-drop="() => (dropOpen = true)" > -
-
-

+ +
+
+

+ + + +

+

+

+ + {{ $gettext(item.text) }} + +
+

- + {{ $gettext('Read more') }}

-

-

- - {{ $gettext(item.text) }} - -
-

- - {{ $gettext('Read more') }} - -

+ diff --git a/packages/design-system/src/components/OcModal/OcModal.vue b/packages/design-system/src/components/OcModal/OcModal.vue index 19e666f89fc..779d1c89299 100644 --- a/packages/design-system/src/components/OcModal/OcModal.vue +++ b/packages/design-system/src/components/OcModal/OcModal.vue @@ -86,6 +86,7 @@ import OcIcon from '../OcIcon/OcIcon.vue' import OcTextInput from '../OcTextInput/OcTextInput.vue' import { FocusTrap } from 'focus-trap-vue' import { FocusTargetOrFalse, FocusTrapTabbableOptions } from 'focus-trap' +import { ContextualHelperData } from '../../helpers' /** * Modals are generally used to force the user to focus on confirming or completing a single action. @@ -179,7 +180,7 @@ export default defineComponent({ * Contextual helper data */ contextualHelperData: { - type: Object, + type: Object as PropType, required: false, default: null }, diff --git a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue index 9f91571f6d7..97d238572ac 100644 --- a/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue +++ b/packages/design-system/src/components/_OcTextInputPassword/_OcTextInputPassword.vue @@ -67,7 +67,11 @@ ]" v-text="getPasswordPolicyRuleMessage(testedRule)" > - +
diff --git a/packages/design-system/src/helpers/types.ts b/packages/design-system/src/helpers/types.ts index 7d9fac0f170..73db2734437 100644 --- a/packages/design-system/src/helpers/types.ts +++ b/packages/design-system/src/helpers/types.ts @@ -4,7 +4,7 @@ export interface ContextualHelperDataListItem { } export interface ContextualHelperData { - title?: string + title: string text?: string list?: ContextualHelperDataListItem[] readMoreLink?: string diff --git a/packages/web-app-admin-settings/src/components/Users/SideBar/DetailsPanel.vue b/packages/web-app-admin-settings/src/components/Users/SideBar/DetailsPanel.vue index 6133e6e8d72..4e378b0e3ca 100644 --- a/packages/web-app-admin-settings/src/components/Users/SideBar/DetailsPanel.vue +++ b/packages/web-app-admin-settings/src/components/Users/SideBar/DetailsPanel.vue @@ -40,6 +40,7 @@ 'User roles become available once the user has logged in for the first time.' ) " + :title="$gettext('User role')" /> @@ -62,6 +63,7 @@ 'User quota becomes available once the user has logged in for the first time.' ) " + :title="$gettext('Quota')" /> @@ -72,7 +74,10 @@ - - + diff --git a/packages/web-app-files/src/components/Shares/CopyPrivateLink.vue b/packages/web-app-files/src/components/Shares/CopyPrivateLink.vue index 71dae7b0962..eeb320c672c 100644 --- a/packages/web-app-files/src/components/Shares/CopyPrivateLink.vue +++ b/packages/web-app-files/src/components/Shares/CopyPrivateLink.vue @@ -4,7 +4,7 @@ - + diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue index c1260941f22..8ef55238587 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/ListItem.vue @@ -50,6 +50,7 @@ 'External user, registered with another organization’s account but granted access to your resources. External users can only have “view” or “edit” permission.' ) " + :title="$gettext('External user')" />
diff --git a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue index 6bba9d9b9c2..ed699ecb158 100644 --- a/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue +++ b/packages/web-app-files/src/components/SideBar/Shares/Collaborators/RoleDropdown.vue @@ -23,6 +23,7 @@ v-if="isDisabledRole" class="oc-ml-xs files-permission-actions-list" :list="existingSharePermissions.map((permission) => ({ text: permission }))" + :title="$gettext('Custom permissions')" />
- +
diff --git a/packages/web-app-files/tests/unit/components/Shares/__snapshots__/CopyPrivateLink.spec.ts.snap b/packages/web-app-files/tests/unit/components/Shares/__snapshots__/CopyPrivateLink.spec.ts.snap index 22fb14ceee1..43a1e1e75e7 100644 --- a/packages/web-app-files/tests/unit/components/Shares/__snapshots__/CopyPrivateLink.spec.ts.snap +++ b/packages/web-app-files/tests/unit/components/Shares/__snapshots__/CopyPrivateLink.spec.ts.snap @@ -12,7 +12,12 @@ exports[`CopyPrivateLink > should render a button 1`] = `
- +
+

Permanent link

+

Copy the link to point your team to this item. Works only for people with existing access.

diff --git a/packages/web-app-ocm/src/views/ConnectionsPanel.vue b/packages/web-app-ocm/src/views/ConnectionsPanel.vue index 50ffaa82a3c..01d76f3fc4c 100644 --- a/packages/web-app-ocm/src/views/ConnectionsPanel.vue +++ b/packages/web-app-ocm/src/views/ConnectionsPanel.vue @@ -138,7 +138,8 @@ export default defineComponent({ return { text: $gettext( 'Federated conections for mutual sharing. To share, go to "Files" app, select the resource click "Share" in the context menu and select account type "federated".' - ) + ), + title: $gettext('Federated connections') } }) diff --git a/packages/web-app-ocm/src/views/IncomingInvitations.vue b/packages/web-app-ocm/src/views/IncomingInvitations.vue index 16fe980f4b7..c907f900998 100644 --- a/packages/web-app-ocm/src/views/IncomingInvitations.vue +++ b/packages/web-app-ocm/src/views/IncomingInvitations.vue @@ -97,7 +97,8 @@ export default defineComponent({ return { text: $gettext( 'Once you accept the invitation, the inviter will be added to your connections.' - ) + ), + title: $gettext('Accepting invitations') } }) diff --git a/packages/web-app-ocm/src/views/OutgoingInvitations.vue b/packages/web-app-ocm/src/views/OutgoingInvitations.vue index 9cb1a5865df..1a63d9c6cbd 100644 --- a/packages/web-app-ocm/src/views/OutgoingInvitations.vue +++ b/packages/web-app-ocm/src/views/OutgoingInvitations.vue @@ -187,7 +187,8 @@ export default defineComponent({ return { text: $gettext( 'Create an invitation link and send it to the person you want to share with.' - ) + ), + title: $gettext('Invitation link') } }) diff --git a/packages/web-pkg/src/components/CreateShortcutModal.vue b/packages/web-pkg/src/components/CreateShortcutModal.vue index 48ddd15dc47..4f55fc59152 100644 --- a/packages/web-pkg/src/components/CreateShortcutModal.vue +++ b/packages/web-pkg/src/components/CreateShortcutModal.vue @@ -20,6 +20,7 @@ 'Enter the target URL of a webpage or the name of a file. Users will be directed to this webpage or file.' ) " + :title="$gettext('Webpage or file')" class="oc-ml-xs" />
@@ -96,6 +97,7 @@ >
diff --git a/packages/web-pkg/src/components/Spaces/QuotaModal.vue b/packages/web-pkg/src/components/Spaces/QuotaModal.vue index 833b8391144..9f4290c6c49 100644 --- a/packages/web-pkg/src/components/Spaces/QuotaModal.vue +++ b/packages/web-pkg/src/components/Spaces/QuotaModal.vue @@ -52,7 +52,7 @@ export default defineComponent({ }, warningMessageContextualHelperData: { type: Object as PropType, - default: (): ContextualHelperData => ({}) + default: (): ContextualHelperData => undefined }, resourceType: { type: String,