Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/api-generator/src/locale/en/VDialog.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"noClickAnimation": "Disables the bounce effect when clicking outside of a `v-dialog`'s content when using the **persistent** prop.",
"openOnHover": "Designates whether component should activate when its activator is hovered.",
"persistent": "Clicking outside of the element or pressing **esc** key will not deactivate it.",
"retainFocus": "Tab focus will return to the first child of the dialog by default. Disable this when using external tools that require focus such as TinyMCE or vue-clipboard.",
"scrollable": "When set to true, expects a `v-card` and a `v-card-text` component with a designated height. For more information, check out the [scrollable example](/components/dialogs#scrollable)."
},
"events": {
Expand Down
1 change: 1 addition & 0 deletions packages/api-generator/src/locale/en/generic.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"falseValue": "Sets value for falsy state.",
"firstDayOfWeek": "Sets the first day of the week, starting with 0 for Sunday. (Note: not guaranteed to work when using custom date adapters.)",
"firstDayOfYear": "Sets the day that determines the first week of the year, starting with 0 for Sunday. For ISO 8601 this should be 4. (Note: not guaranteed to work when using custom date adapters.)",
"retainFocus": "Captures and keeps focus within the content element when using **Tab** and **Shift**+**Tab**. Recommended to be `false` when using external tools that require focus such as TinyMCE or vue-clipboard.",
"fullWidth": "Sets the component width to 100%.",
"height": "Sets the height for the component.",
"hideNoData": "Hides the menu when there are no options to show. Useful for preventing the menu from opening before results are fetched asynchronously. Also has the effect of opening the menu when the `items` array changes if not already open.",
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/data/new-in.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
},
"VDialog": {
"props": {
"captureFocus": "3.11.0",
"retainFocus": "3.11.0",
"stickToTarget": "3.10.0"
}
},
Expand Down Expand Up @@ -167,12 +169,16 @@
},
"VMenu": {
"props": {
"captureFocus": "3.11.0",
"retainFocus": "3.11.0",
"stickToTarget": "3.10.0",
"submenu": "3.7.0"
}
},
"VNavigationDrawer": {
"props": {
"captureFocus": "3.11.0",
"retainFocus": "3.11.0",
"persistent": "3.6.0"
}
},
Expand All @@ -183,6 +189,8 @@
},
"VOverlay": {
"props": {
"captureFocus": "3.11.0",
"retainFocus": "3.11.0",
"stickToTarget": "3.10.0"
}
},
Expand Down
54 changes: 6 additions & 48 deletions packages/vuetify/src/components/VDialog/VDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,24 @@ import { useProxiedModel } from '@/composables/proxiedModel'
import { useScopeId } from '@/composables/scopeId'

// Utilities
import { mergeProps, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { focusableChildren, genericComponent, IN_BROWSER, propsFactory, useRender } from '@/util'
import { mergeProps, nextTick, ref, watch } from 'vue'
import { genericComponent, omit, propsFactory, useRender } from '@/util'

// Types
import type { OverlaySlots } from '@/components/VOverlay/VOverlay'

export const makeVDialogProps = propsFactory({
fullscreen: Boolean,
retainFocus: {
type: Boolean,
default: true,
},
scrollable: Boolean,

...makeVOverlayProps({
...omit(makeVOverlayProps({
captureFocus: true,
origin: 'center center' as const,
scrollStrategy: 'block' as const,
transition: { component: VDialogTransition },
zIndex: 2400,
}),
retainFocus: true,
}), ['disableInitialFocus']),
}, 'VDialog')

export const VDialog = genericComponent<OverlaySlots>()({
Expand All @@ -51,46 +49,6 @@ export const VDialog = genericComponent<OverlaySlots>()({
const { scopeId } = useScopeId()

const overlay = ref<VOverlay>()
function onFocusin (e: FocusEvent) {
const before = e.relatedTarget as HTMLElement | null
const after = e.target as HTMLElement | null

if (
before !== after &&
overlay.value?.contentEl &&
// We're the topmost dialog
overlay.value?.globalTop &&
// It isn't the document or the dialog body
![document, overlay.value.contentEl].includes(after!) &&
// It isn't inside the dialog body
!overlay.value.contentEl.contains(after)
) {
const focusable = focusableChildren(overlay.value.contentEl)

if (!focusable.length) return

const firstElement = focusable[0]
const lastElement = focusable[focusable.length - 1]

if (before === firstElement) {
lastElement.focus()
} else {
firstElement.focus()
}
}
}

onBeforeUnmount(() => {
document.removeEventListener('focusin', onFocusin)
})

if (IN_BROWSER) {
watch(() => isActive.value && props.retainFocus, val => {
val
? document.addEventListener('focusin', onFocusin)
: document.removeEventListener('focusin', onFocusin)
}, { immediate: true })
}

function onAfterEnter () {
emit('afterEnter')
Expand Down
47 changes: 6 additions & 41 deletions packages/vuetify/src/components/VMenu/VMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
computed,
inject,
mergeProps,
nextTick,
onBeforeUnmount,
onDeactivated,
provide,
Expand All @@ -33,7 +32,6 @@ import {
focusChild,
genericComponent,
getNextElement,
IN_BROWSER,
isClickInsideElement,
omit,
propsFactory,
Expand All @@ -48,9 +46,9 @@ export const makeVMenuProps = propsFactory({
// disableKeys: Boolean,
id: String,
submenu: Boolean,
disableInitialFocus: Boolean,

...omit(makeVOverlayProps({
captureFocus: true,
closeDelay: 250,
closeOnContentClick: true,
locationStrategy: 'connected' as const,
Expand Down Expand Up @@ -103,46 +101,13 @@ export const VMenu = genericComponent<OverlaySlots>()({
},
})

onBeforeUnmount(() => {
parent?.unregister()
document.removeEventListener('focusin', onFocusIn)
})
onBeforeUnmount(() => parent?.unregister())
onDeactivated(() => isActive.value = false)

async function onFocusIn (e: FocusEvent) {
const before = e.relatedTarget as HTMLElement | null
const after = e.target as HTMLElement | null

await nextTick()

if (
isActive.value &&
before !== after &&
overlay.value?.contentEl &&
// We're the topmost menu
overlay.value?.globalTop &&
// It isn't the document or the menu body
![document, overlay.value.contentEl].includes(after!) &&
// It isn't inside the menu body
!overlay.value.contentEl.contains(after)
) {
const focusable = focusableChildren(overlay.value.contentEl)
focusable[0]?.focus()
}
}

watch(isActive, val => {
if (val) {
parent?.register()
if (IN_BROWSER && !props.disableInitialFocus) {
document.addEventListener('focusin', onFocusIn, { once: true })
}
} else {
parent?.unregister()
if (IN_BROWSER) {
document.removeEventListener('focusin', onFocusIn)
}
}
val
? parent?.register()
: parent?.unregister()
}, { immediate: true })

function onClickOutside (e: MouseEvent) {
Expand All @@ -165,7 +130,7 @@ export const VMenu = genericComponent<OverlaySlots>()({
e.shiftKey ? 'prev' : 'next',
(el: HTMLElement) => el.tabIndex >= 0
)
if (!nextElement) {
if (!nextElement && !props.retainFocus) {
isActive.value = false
overlay.value?.activatorEl?.focus()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { provideDefaults } from '@/composables/defaults'
import { makeDelayProps, useDelay } from '@/composables/delay'
import { makeDisplayProps, useDisplay } from '@/composables/display'
import { makeElevationProps, useElevation } from '@/composables/elevation'
import { makeFocusTrapProps, useFocusTrap } from '@/composables/focusTrap'
import { makeLayoutItemProps, useLayoutItem } from '@/composables/layout'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeRoundedProps, useRounded } from '@/composables/rounded'
Expand All @@ -28,7 +29,7 @@ import { useToggleScope } from '@/composables/toggleScope'

// Utilities
import { computed, nextTick, readonly, ref, shallowRef, toRef, Transition, watch } from 'vue'
import { genericComponent, propsFactory, toPhysical, useRender } from '@/util'
import { genericComponent, omit, propsFactory, toPhysical, useRender } from '@/util'

// Types
import type { PropType } from 'vue'
Expand Down Expand Up @@ -91,6 +92,7 @@ export const makeVNavigationDrawerProps = propsFactory({
...makeElevationProps(),
...makeLayoutItemProps(),
...makeRoundedProps(),
...omit(makeFocusTrapProps({ captureFocus: false }), ['disableInitialFocus']),
...makeTagProps({ tag: 'nav' }),
...makeThemeProps(),
}, 'VNavigationDrawer')
Expand Down Expand Up @@ -141,6 +143,8 @@ export const VNavigationDrawer = genericComponent<VNavigationDrawerSlots>()({
location.value !== 'bottom'
)

useFocusTrap(props, { isActive, globalTop: isTemporary, contentEl: rootEl })

useToggleScope(() => props.expandOnHover && props.rail != null, () => {
watch(isHovering, val => emit('update:rail', !val))
})
Expand Down
7 changes: 6 additions & 1 deletion packages/vuetify/src/components/VOverlay/VOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { makeActivatorProps, useActivator } from './useActivator'
import { useBackgroundColor } from '@/composables/color'
import { makeComponentProps } from '@/composables/component'
import { makeDimensionProps, useDimension } from '@/composables/dimensions'
import { makeFocusTrapProps, useFocusTrap } from '@/composables/focusTrap'
import { useHydration } from '@/composables/hydration'
import { makeLazyProps, useLazy } from '@/composables/lazy'
import { useRtl } from '@/composables/locale'
Expand Down Expand Up @@ -40,6 +41,7 @@ import {
getCurrentInstance,
getScrollParent,
IN_BROWSER,
omit,
propsFactory,
standardEasing,
useRender,
Expand Down Expand Up @@ -108,6 +110,7 @@ export const makeVOverlayProps = propsFactory({
...makeLazyProps(),
...makeLocationStrategyProps(),
...makeScrollStrategyProps(),
...makeFocusTrapProps(),
...makeThemeProps(),
...makeTransitionProps(),
}, 'VOverlay')
Expand All @@ -122,7 +125,7 @@ export const VOverlay = genericComponent<OverlaySlots>()({
props: {
_disableGlobalStack: Boolean,

...makeVOverlayProps(),
...omit(makeVOverlayProps(), ['disableInitialFocus']),
},

emits: {
Expand Down Expand Up @@ -203,6 +206,8 @@ export const VOverlay = genericComponent<OverlaySlots>()({
)
}

useFocusTrap(props, { isActive, globalTop, contentEl })

IN_BROWSER && watch(isActive, val => {
if (val) {
window.addEventListener('keydown', onKeydown)
Expand Down
11 changes: 10 additions & 1 deletion packages/vuetify/src/components/VSnackbar/VSnackbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,16 @@ export const makeVSnackbarProps = propsFactory({
...makeThemeProps(),
...omit(makeVOverlayProps({
transition: 'v-snackbar-transition',
}), ['persistent', 'noClickAnimation', 'scrim', 'scrollStrategy', 'stickToTarget']),
}), [
'persistent',
'noClickAnimation',
'retainFocus',
'captureFocus',
'disableInitialFocus',
'scrim',
'scrollStrategy',
'stickToTarget',
]),
}, 'VSnackbar')

export const VSnackbar = genericComponent<VSnackbarSlots>()({
Expand Down
3 changes: 3 additions & 0 deletions packages/vuetify/src/components/VTooltip/VTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const makeVTooltipProps = propsFactory({
transition: null,
}), [
'absolute',
'retainFocus',
'captureFocus',
'disableInitialFocus',
'persistent',
]),
}, 'VTooltip')
Expand Down
Loading