Skip to content

Commit

Permalink
fix(a11y): focus trap for contextual helpers
Browse files Browse the repository at this point in the history
Adds a focus trap to contextual helpers to prevent the user from focusing elements in the background.
  • Loading branch information
JammingBen committed Sep 13, 2024
1 parent f60fa96 commit 93e21e7
Show file tree
Hide file tree
Showing 18 changed files with 112 additions and 54 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/enhancement-a11y-improvements
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -25,8 +25,7 @@ export default defineComponent({
*/
title: {
type: String,
required: false,
default: ''
required: true
},
/**
* Text at the beginning
Expand All @@ -40,9 +39,9 @@ export default defineComponent({
* List element
*/
list: {
type: Array as PropType<ContextualHelperData[]>,
type: Array as PropType<ContextualHelperDataListItem[]>,
required: false,
default: (): ContextualHelperData[] => []
default: (): ContextualHelperDataListItem[] => []
},
/**
* Text at the end
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof OcInfoDrop>) {
return shallowMount(OcInfoDrop, {
props,
props: {
...props,
title: props.title || 'test-title'
},
global: {
plugins: [...defaultPlugins()],
renderStubDefaultSlot: true,
Expand Down Expand Up @@ -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<typeof OcDrop>('oc-drop-stub').vm.$emit('show-drop')
await wrapper.vm.$nextTick()
const focusTrap = wrapper.findComponent<typeof FocusTrap>('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<typeof FocusTrap>('focus-trap-stub')
expect(focusTrap.props('active')).toBeFalsy()
})
})
})
})
80 changes: 43 additions & 37 deletions packages/design-system/src/components/OcInfoDrop/OcInfoDrop.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,57 @@
:toggle="toggle"
:mode="mode"
close-on-click
@hide-drop="() => (dropOpen = false)"
@show-drop="() => (dropOpen = true)"
>
<div class="info-drop-content">
<div v-if="title" class="oc-flex oc-flex-between info-header oc-border-b oc-pb-s">
<h4 class="oc-m-rm info-title" v-text="$gettext(title)" />
<focus-trap :active="dropOpen">
<div class="info-drop-content">
<div class="oc-flex oc-flex-between info-header oc-border-b oc-pb-s">
<h4 class="oc-m-rm info-title" v-text="$gettext(title)" />
<oc-button
v-oc-tooltip="$gettext('Close')"
appearance="raw"
:aria-label="$gettext('Close')"
>
<oc-icon name="close" fill-type="line" size="medium" variation="inherit" />
</oc-button>
</div>
<p v-if="text" class="info-text" v-text="$gettext(text)" />
<dl v-if="list.length" class="info-list">
<component :is="item.headline ? 'dt' : 'dd'" v-for="(item, index) in list" :key="index">
{{ $gettext(item.text) }}
</component>
</dl>
<p v-if="endText" class="info-text-end" v-text="$gettext(endText)" />
<oc-button
v-oc-tooltip="$gettext('Close')"
v-if="readMoreLink"
type="a"
appearance="raw"
:aria-label="$gettext('Close')"
size="small"
class="info-more-link"
:href="readMoreLink"
target="_blank"
>
<oc-icon name="close" fill-type="line" size="medium" variation="inherit" />
{{ $gettext('Read more') }}
</oc-button>
</div>
<p v-if="text" class="info-text" v-text="$gettext(text)" />
<dl v-if="list.length" class="info-list">
<component :is="item.headline ? 'dt' : 'dd'" v-for="(item, index) in list" :key="index">
{{ $gettext(item.text) }}
</component>
</dl>
<p v-if="endText" class="info-text-end" v-text="$gettext(endText)" />
<oc-button
v-if="readMoreLink"
type="a"
appearance="raw"
size="small"
class="info-more-link"
:href="readMoreLink"
target="_blank"
>
{{ $gettext('Read more') }}
</oc-button>
</div>
</focus-trap>
</oc-drop>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { defineComponent, PropType, ref } from 'vue'
import OcButton from '../OcButton/OcButton.vue'
import OcIcon from '../OcIcon/OcIcon.vue'
import OcDrop from '../OcDrop/OcDrop.vue'
import uniqueId from '../../utils/uniqueId'
export type ListElement = {
text: string
headline?: boolean
}
import { FocusTrap } from 'focus-trap-vue'
import { ContextualHelperDataListItem } from '../../helpers'
export default defineComponent({
name: 'OcInfoDrop',
status: 'unreleased',
components: { OcButton, OcIcon, OcDrop },
components: { OcButton, OcIcon, OcDrop, FocusTrap },
props: {
/**
* Id of the element
Expand Down Expand Up @@ -100,8 +100,7 @@ export default defineComponent({
*/
title: {
type: String,
required: false,
default: ''
required: true
},
/**
* Text at the beginning
Expand All @@ -115,9 +114,9 @@ export default defineComponent({
* List element
*/
list: {
type: Array as PropType<ListElement[]>,
type: Array as PropType<ContextualHelperDataListItem[]>,
required: false,
default: (): ListElement[] => []
default: (): ContextualHelperDataListItem[] => []
},
/**
* Text at the end
Expand All @@ -135,6 +134,13 @@ export default defineComponent({
required: false,
default: ''
}
},
setup() {
const dropOpen = ref(false)
return {
dropOpen
}
}
})
</script>
Expand Down
3 changes: 2 additions & 1 deletion packages/design-system/src/components/OcModal/OcModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -179,7 +180,7 @@ export default defineComponent({
* Contextual helper data
*/
contextualHelperData: {
type: Object,
type: Object as PropType<ContextualHelperData>,
required: false,
default: null
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@
]"
v-text="getPasswordPolicyRuleMessage(testedRule)"
></span>
<oc-contextual-helper v-if="testedRule.helperMessage" :text="testedRule.helperMessage" />
<oc-contextual-helper
v-if="testedRule.helperMessage"
:text="testedRule.helperMessage"
:title="$gettext('Password policy')"
/>
</div>
</div>
</portal>
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface ContextualHelperDataListItem {
}

export interface ContextualHelperData {
title?: string
title: string
text?: string
list?: ContextualHelperDataListItem[]
readMoreLink?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
'User roles become available once the user has logged in for the first time.'
)
"
:title="$gettext('User role')"
/>
</span>
</td>
Expand All @@ -62,6 +63,7 @@
'User quota becomes available once the user has logged in for the first time.'
)
"
:title="$gettext('Quota')"
/>
</span>
</td>
Expand All @@ -72,7 +74,10 @@
<span v-if="_user.memberOf.length" v-text="groupsDisplayValue" />
<span v-else>
<span class="oc-mr-xs">-</span>
<oc-contextual-helper :text="$gettext('No groups assigned.')" />
<oc-contextual-helper
:text="$gettext('No groups assigned.')"
:title="$gettext('Groups')"
/>
</span>
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<oc-icon size="small" :name="copied ? 'checkbox-circle' : 'file-copy'" />
<span class="oc-ml-xs" v-text="$gettext('Permanent link')"
/></oc-button>
<oc-contextual-helper class="oc-ml-xs" :text="helperText" />
<oc-contextual-helper class="oc-ml-xs" :text="helperText" :title="$gettext('Permanent link')" />
</div>
</template>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
/>
</div>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
/>
</div>
<oc-drop
Expand Down
6 changes: 5 additions & 1 deletion packages/web-app-files/src/views/spaces/Projects.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
<create-space v-if="hasCreatePermission" class="oc-mr-s" />
<div v-if="!selectedResourcesIds?.length" class="oc-flex oc-flex-middle oc-pl-s">
<span v-text="$gettext('Learn about spaces')" />
<oc-contextual-helper :text="spacesHelpText" class="oc-ml-xs" />
<oc-contextual-helper
:text="spacesHelpText"
:title="$gettext('Spaces')"
class="oc-ml-xs"
/>
</div>
</template>
</app-bar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ exports[`CopyPrivateLink > should render a button 1`] = `
<div id="oc-contextual-helper-1" class="oc-drop oc-box-shadow-medium oc-rounded oc-width-1-1 oc-info-drop">
<div class="oc-card oc-card-body oc-background-secondary oc-p-m">
<div class="info-drop-content">
<!--v-if-->
<div class="oc-flex oc-flex-between info-header oc-border-b oc-pb-s">
<h4 class="oc-m-rm info-title">Permanent link</h4> <button type="button" aria-label="Close" class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw">
<!--v-if-->
<!-- @slot Content of the button --> <span class="oc-icon oc-icon-m oc-icon-inherit"><!----></span>
</button>
</div>
<p class="info-text">Copy the link to point your team to this item. Works only for people with existing access.</p>
<!--v-if-->
<!--v-if-->
Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-ocm/src/views/ConnectionsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
})
Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-ocm/src/views/IncomingInvitations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
})
Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-ocm/src/views/OutgoingInvitations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
})
Expand Down
2 changes: 2 additions & 0 deletions packages/web-pkg/src/components/CreateShortcutModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
</div>
Expand Down Expand Up @@ -96,6 +97,7 @@
></label>
<oc-contextual-helper
:text="$gettext('Shortcut name as it will appear in the file list.')"
:title="$gettext('Shortcut name')"
class="oc-ml-xs"
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/web-pkg/src/components/Spaces/QuotaModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default defineComponent({
},
warningMessageContextualHelperData: {
type: Object as PropType<ContextualHelperData>,
default: (): ContextualHelperData => ({})
default: (): ContextualHelperData => undefined
},
resourceType: {
type: String,
Expand Down

0 comments on commit 93e21e7

Please sign in to comment.