-
Notifications
You must be signed in to change notification settings - Fork 267
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13019 from rak-phillip/chore/12771-dropdown-compo…
…nent Create accessible dropdown component
- Loading branch information
Showing
24 changed files
with
722 additions
and
289 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
pkg/rancher-components/src/components/RcButton/RcButton.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
<script setup lang="ts"> | ||
/** | ||
* A button element used for performing actions, such as submitting forms or | ||
* opening dialogs. | ||
* | ||
* Example: | ||
* | ||
* <rc-button primary @click="doAction">Perform an Action</rc-button> | ||
*/ | ||
import { computed, ref, defineExpose } from 'vue'; | ||
import { ButtonRoleProps, ButtonSizeProps } from './types'; | ||
const buttonRoles: { role: keyof ButtonRoleProps, className: string }[] = [ | ||
{ role: 'primary', className: 'role-primary' }, | ||
{ role: 'secondary', className: 'role-secondary' }, | ||
{ role: 'tertiary', className: 'role-tertiary' }, | ||
{ role: 'link', className: 'role-link' }, | ||
{ role: 'ghost', className: 'role-ghost' }, | ||
]; | ||
const buttonSizes: { size: keyof ButtonSizeProps, className: string }[] = [ | ||
{ size: 'small', className: 'btn-sm' }, | ||
]; | ||
const props = defineProps<ButtonRoleProps & ButtonSizeProps>(); | ||
const buttonClass = computed(() => { | ||
const activeRole = buttonRoles.find(({ role }) => props[role]); | ||
const isButtonSmall = buttonSizes.some(({ size }) => props[size]); | ||
return { | ||
btn: true, | ||
[activeRole?.className || 'role-primary']: true, | ||
'btn-sm': isButtonSmall, | ||
}; | ||
}); | ||
const RcFocusTarget = ref<HTMLElement | null>(null); | ||
const focus = () => { | ||
RcFocusTarget?.value?.focus(); | ||
}; | ||
defineExpose({ focus }); | ||
</script> | ||
|
||
<template> | ||
<button | ||
ref="RcFocusTarget" | ||
role="button" | ||
:class="{ ...buttonClass, ...($attrs.class || { }) }" | ||
> | ||
<slot name="before"> | ||
<!-- Empty Content --> | ||
</slot> | ||
<slot> | ||
<!-- Empty Content --> | ||
</slot> | ||
<slot name="after"> | ||
<!-- Empty Content --> | ||
</slot> | ||
</button> | ||
</template> | ||
|
||
<style lang="scss" scoped> | ||
.role-link { | ||
&:focus, &.focused { | ||
outline: var(--outline-width) solid var(--border); | ||
box-shadow: 0 0 0 var(--outline-width) var(--outline); | ||
} | ||
} | ||
button { | ||
&.role-ghost { | ||
padding: 0; | ||
background-color: transparent; | ||
&:focus, &.focused { | ||
outline: 2px solid var(--primary-keyboard-focus); | ||
outline-offset: 0; | ||
} | ||
&:focus-visible { | ||
outline: 2px solid var(--primary-keyboard-focus); | ||
outline-offset: 0; | ||
} | ||
} | ||
}</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default as RcButton } from './RcButton.vue'; | ||
export type { RcButtonType } from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// TODO: 13211 Investigate why `InstanceType<typeof RcButton>` fails prod builds | ||
// export type RcButtonType = InstanceType<typeof RcButton> | ||
export type RcButtonType = { | ||
focus: () => void; | ||
} | ||
|
||
export type ButtonRoleProps = { | ||
primary?: boolean; | ||
secondary?: boolean; | ||
tertiary?: boolean; | ||
link?: boolean; | ||
ghost?: boolean; | ||
} | ||
|
||
export type ButtonSizeProps = { | ||
small?: boolean; | ||
} |
111 changes: 111 additions & 0 deletions
111
pkg/rancher-components/src/components/RcDropdown/RcDropdown.vue
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<script setup lang="ts"> | ||
/** | ||
* Offers a list of choices to the user, such as a set of actions or functions. | ||
* Opened by activating RcDropdownTrigger. | ||
* | ||
* Example: | ||
* | ||
* <rc-dropdown :aria-label="t('nav.actionMenu.label')"> | ||
* <rc-dropdown-trigger tertiary> | ||
* <i class="icon icon-actions" /> | ||
* </rc-dropdown-trigger> | ||
* <template #dropdownCollection> | ||
* <rc-dropdown-item @click="performAction()"> | ||
* Action 1 | ||
* </rc-dropdown-item> | ||
* <rc-dropdown-separator /> | ||
* <rc-dropdown-item @click="performAction()"> | ||
* Action 2 | ||
* </rc-dropdown-item> | ||
* </template> | ||
* </rc-dropdown> | ||
*/ | ||
import { useTemplateRef } from 'vue'; | ||
import { useClickOutside } from '@shell/composables/useClickOutside'; | ||
import { useDropdownContext } from '@components/RcDropdown/useDropdownContext'; | ||
defineProps<{ | ||
ariaLabel?: string | ||
}>(); | ||
const { | ||
isMenuOpen, | ||
showMenu, | ||
returnFocus, | ||
setFocus, | ||
provideDropdownContext, | ||
registerDropdownCollection, | ||
} = useDropdownContext(); | ||
provideDropdownContext(); | ||
const popperContainer = useTemplateRef<HTMLElement>('popperContainer'); | ||
const dropdownTarget = useTemplateRef<HTMLElement>('dropdownTarget'); | ||
useClickOutside(dropdownTarget, () => showMenu(false)); | ||
const applyShow = () => { | ||
registerDropdownCollection(dropdownTarget.value); | ||
setFocus(); | ||
}; | ||
</script> | ||
|
||
<template> | ||
<v-dropdown | ||
no-auto-focus | ||
:triggers="[]" | ||
:shown="isMenuOpen" | ||
:auto-hide="false" | ||
:container="popperContainer" | ||
:placement="'bottom-end'" | ||
@apply-show="applyShow" | ||
> | ||
<slot name="default"> | ||
<!--Empty slot content Trigger--> | ||
</slot> | ||
|
||
<template #popper> | ||
<div | ||
ref="dropdownTarget" | ||
role="menu" | ||
aria-orientation="vertical" | ||
dropdown-menu-collection | ||
:aria-label="ariaLabel || 'Dropdown Menu'" | ||
> | ||
<slot name="dropdownCollection"> | ||
<!--Empty slot content--> | ||
</slot> | ||
</div> | ||
</template> | ||
</v-dropdown> | ||
<div | ||
ref="popperContainer" | ||
class="popperContainer" | ||
@keydown.tab="showMenu(false)" | ||
@keydown.escape="returnFocus" | ||
> | ||
<!--Empty container for mounting popper content--> | ||
</div> | ||
</template> | ||
|
||
<style lang="scss" scoped> | ||
.popperContainer { | ||
display: contents; | ||
&:deep(.v-popper__popper) { | ||
.v-popper__wrapper { | ||
box-shadow: 0px 6px 18px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.15); | ||
border-radius: var(--border-radius-lg); | ||
.v-popper__arrow-container { | ||
display: none; | ||
} | ||
.v-popper__inner { | ||
padding: 10px 0 10px 0; | ||
} | ||
} | ||
} | ||
} | ||
</style> |
Oops, something went wrong.