mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
fix(web-core): keep an open select's Escape from closing its overlay (#10582)
This commit is contained in:
committed by
GitHub
parent
025f6eb533
commit
6115c74bbe
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="className" class="vts-select">
|
||||
<div ref="rootRef" :class="className" class="vts-select">
|
||||
<UiInput
|
||||
ref="triggerRef"
|
||||
:accent
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
useFormSelectController,
|
||||
} from '@core/packages/form-select'
|
||||
import { toVariants } from '@core/utils/to-variants.util.ts'
|
||||
import { useCurrentElement, useElementSize } from '@vueuse/core'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { computed, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -86,11 +86,13 @@ const {
|
||||
isLoading,
|
||||
} = select as FormSelect
|
||||
|
||||
const { triggerRef, dropdownRef, searchRef, isOpen, floatingStyles } = useFormSelectController(select as FormSelect)
|
||||
const { rootRef, triggerRef, dropdownRef, searchRef, isOpen, floatingStyles } = useFormSelectController(
|
||||
select as FormSelect
|
||||
)
|
||||
|
||||
const className = computed(() => toVariants({ accent }))
|
||||
|
||||
const { width } = useElementSize(useCurrentElement())
|
||||
const { width } = useElementSize(rootRef)
|
||||
|
||||
const minWidth = computed(() => `${width.value}px`)
|
||||
|
||||
@@ -106,7 +108,6 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
/* Teleported */
|
||||
.dropdown-list {
|
||||
min-width: v-bind(minWidth);
|
||||
max-height: 36.2rem; /* 8 Dropdown items */
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ifElse } from '@core/utils/if-else.utils.ts'
|
||||
import { autoUpdate, flip, type MaybeElement, shift, size, useFloating } from '@floating-ui/vue'
|
||||
import { clamp, onClickOutside, useEventListener, useFocusWithin, whenever } from '@vueuse/core'
|
||||
import { logicOr } from '@vueuse/math'
|
||||
import { clamp, onClickOutside, useEventListener, whenever } from '@vueuse/core'
|
||||
import { computed, provide, reactive, ref, watch } from 'vue'
|
||||
import { type FormOptionIndex, type FormSelect, IK_FORM_SELECT_CONTROLLER } from './types.ts'
|
||||
import { useFormSelectKeyboardNavigation } from './use-form-select-keyboard-navigation.ts'
|
||||
|
||||
export function useFormSelectController(select: FormSelect) {
|
||||
const rootRef = ref<HTMLElement>()
|
||||
|
||||
const activeOption = computed(() => select.options.value.find(option => option.flags.active))
|
||||
|
||||
const isOpen = ref(false)
|
||||
@@ -15,10 +16,6 @@ export function useFormSelectController(select: FormSelect) {
|
||||
|
||||
const triggerRef = ref<HTMLElement>()
|
||||
|
||||
const { focused: isTriggerFocused } = useFocusWithin(triggerRef)
|
||||
|
||||
const isActive = logicOr(isTriggerFocused, isOpen)
|
||||
|
||||
useEventListener(triggerRef, 'click', () => openDropdown())
|
||||
|
||||
function focusTrigger() {
|
||||
@@ -28,7 +25,7 @@ export function useFormSelectController(select: FormSelect) {
|
||||
/* KEYBOARD NAVIGATION */
|
||||
|
||||
const { isNavigatingWithKeyboard, stopKeyboardNavigation } = useFormSelectKeyboardNavigation({
|
||||
isActive,
|
||||
target: rootRef,
|
||||
isMultiple: select.isMultiple,
|
||||
isOpen,
|
||||
onSelect: () => activeOption.value?.toggleFlag('selected', true),
|
||||
@@ -46,6 +43,14 @@ export function useFormSelectController(select: FormSelect) {
|
||||
|
||||
useEventListener(dropdownRef, 'mousemove', () => stopKeyboardNavigation())
|
||||
|
||||
useEventListener(dropdownRef, 'mousedown', event => {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
/* DROPDOWN PLACEMENT */
|
||||
|
||||
const { floatingStyles } = useFloating(triggerRef, dropdownRef, {
|
||||
@@ -80,7 +85,7 @@ export function useFormSelectController(select: FormSelect) {
|
||||
return true
|
||||
}
|
||||
|
||||
whenever(isOpen, () => focusSearch(), { flush: 'post' })
|
||||
whenever(isOpen, () => focusSearchOrTrigger(), { flush: 'post' })
|
||||
|
||||
const currentIndex = computed(() => select.options.value.findIndex(option => option.flags.active))
|
||||
|
||||
@@ -215,6 +220,7 @@ export function useFormSelectController(select: FormSelect) {
|
||||
)
|
||||
|
||||
return {
|
||||
rootRef,
|
||||
triggerRef,
|
||||
dropdownRef,
|
||||
searchRef,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ifElse } from '@core/utils/if-else.utils.ts'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
import { type EffectScope, effectScope, ref, type Ref } from 'vue'
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { FORM_SELECT_HANDLED_KEY, type FormOptionIndex } from './types.ts'
|
||||
|
||||
export function useFormSelectKeyboardNavigation({
|
||||
isActive,
|
||||
target,
|
||||
isOpen,
|
||||
isMultiple,
|
||||
onOpen,
|
||||
@@ -13,7 +12,7 @@ export function useFormSelectKeyboardNavigation({
|
||||
onToggle,
|
||||
onSelect,
|
||||
}: {
|
||||
isActive: Ref<boolean>
|
||||
target: Ref<HTMLElement | undefined>
|
||||
isOpen: Ref<boolean>
|
||||
isMultiple: Ref<boolean>
|
||||
onOpen: () => void
|
||||
@@ -99,7 +98,7 @@ export function useFormSelectKeyboardNavigation({
|
||||
event: KeyboardEvent,
|
||||
key: FORM_SELECT_HANDLED_KEY.TAB | FORM_SELECT_HANDLED_KEY.ESCAPE
|
||||
) {
|
||||
if (key === FORM_SELECT_HANDLED_KEY.ESCAPE) {
|
||||
if (key === FORM_SELECT_HANDLED_KEY.ESCAPE && isOpen.value) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
@@ -108,47 +107,36 @@ export function useFormSelectKeyboardNavigation({
|
||||
|
||||
const handledKeys = Object.values(FORM_SELECT_HANDLED_KEY)
|
||||
|
||||
let scope: EffectScope | undefined
|
||||
onKeyStroke(
|
||||
handledKeys,
|
||||
(event): void => {
|
||||
const key = event.key as FORM_SELECT_HANDLED_KEY
|
||||
|
||||
function registerEvents() {
|
||||
scope = effectScope()
|
||||
|
||||
scope.run(() =>
|
||||
onKeyStroke(handledKeys, (event): void => {
|
||||
const key = event.key as FORM_SELECT_HANDLED_KEY
|
||||
|
||||
switch (key) {
|
||||
case FORM_SELECT_HANDLED_KEY.DOWN:
|
||||
case FORM_SELECT_HANDLED_KEY.UP:
|
||||
return handleArrowUpDownKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.PAGE_DOWN:
|
||||
case FORM_SELECT_HANDLED_KEY.PAGE_UP:
|
||||
return handlePageUpDownKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.HOME:
|
||||
case FORM_SELECT_HANDLED_KEY.END:
|
||||
return handleHomeEndKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.ENTER:
|
||||
case FORM_SELECT_HANDLED_KEY.SPACE:
|
||||
return handleEnterSpaceKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.LEFT:
|
||||
case FORM_SELECT_HANDLED_KEY.RIGHT:
|
||||
return handleLeftRightKeys()
|
||||
case FORM_SELECT_HANDLED_KEY.TAB:
|
||||
case FORM_SELECT_HANDLED_KEY.ESCAPE:
|
||||
return handleTabEscapeKeys(event, key)
|
||||
default:
|
||||
return key
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function unregisterEvents() {
|
||||
scope?.stop()
|
||||
scope = undefined
|
||||
}
|
||||
|
||||
ifElse(isActive, registerEvents, unregisterEvents)
|
||||
switch (key) {
|
||||
case FORM_SELECT_HANDLED_KEY.DOWN:
|
||||
case FORM_SELECT_HANDLED_KEY.UP:
|
||||
return handleArrowUpDownKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.PAGE_DOWN:
|
||||
case FORM_SELECT_HANDLED_KEY.PAGE_UP:
|
||||
return handlePageUpDownKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.HOME:
|
||||
case FORM_SELECT_HANDLED_KEY.END:
|
||||
return handleHomeEndKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.ENTER:
|
||||
case FORM_SELECT_HANDLED_KEY.SPACE:
|
||||
return handleEnterSpaceKeys(event, key)
|
||||
case FORM_SELECT_HANDLED_KEY.LEFT:
|
||||
case FORM_SELECT_HANDLED_KEY.RIGHT:
|
||||
return handleLeftRightKeys()
|
||||
case FORM_SELECT_HANDLED_KEY.TAB:
|
||||
case FORM_SELECT_HANDLED_KEY.ESCAPE:
|
||||
return handleTabEscapeKeys(event, key)
|
||||
default:
|
||||
return key
|
||||
}
|
||||
},
|
||||
{ target }
|
||||
)
|
||||
|
||||
return {
|
||||
isNavigatingWithKeyboard,
|
||||
|
||||
@@ -378,7 +378,14 @@ export function useFormSelect<
|
||||
selectedValues,
|
||||
newValues => {
|
||||
if (isMultiple.value) {
|
||||
model.value = newValues as TMultiple extends true ? $TValue[] : $TValue
|
||||
const modelValues = model.value as $TValue[]
|
||||
|
||||
if (
|
||||
newValues.length !== modelValues.length ||
|
||||
newValues.some((value, index) => toRaw(value) !== toRaw(modelValues[index]))
|
||||
) {
|
||||
model.value = newValues as TMultiple extends true ? $TValue[] : $TValue
|
||||
}
|
||||
} else if (newValues.length > 0) {
|
||||
model.value = newValues[0] as TMultiple extends true ? $TValue[] : $TValue
|
||||
}
|
||||
|
||||
@@ -241,6 +241,8 @@ useOverlayEscape(() => emit('dismiss'))
|
||||
|
||||
Like `useOverlayTrigger()`, it works from any component rendered inside an overlay, and does nothing when the component is not rendered in an overlay.
|
||||
|
||||
An Escape whose default has been prevented is ignored: a nested component that handles Escape itself calls `event.preventDefault()` to keep the overlay open.
|
||||
|
||||
## Composition: building your own composable
|
||||
|
||||
`useOverlay` is designed to be wrapped in small, domain-specific composables:
|
||||
|
||||
@@ -5,9 +5,10 @@ import { inject } from 'vue'
|
||||
|
||||
/**
|
||||
* Calls the handler when Escape is pressed while the calling component is the
|
||||
* current (topmost) overlay and is not busy. The handler is expected to emit
|
||||
* the component's own close event, so that Escape goes through the exact same
|
||||
* pipeline as a pointer interaction.
|
||||
* current (topmost) overlay, is not busy and the Escape has not been
|
||||
* default-prevented. The handler is expected to emit the component's own close
|
||||
* event, so that Escape goes through the exact same pipeline as a pointer
|
||||
* interaction.
|
||||
*/
|
||||
export function useOverlayEscape(handler: () => void) {
|
||||
const overlayKey = inject(IK_OVERLAY_KEY, undefined)
|
||||
@@ -18,8 +19,8 @@ export function useOverlayEscape(handler: () => void) {
|
||||
|
||||
const overlayStore = useOverlayStore()
|
||||
|
||||
onKeyStroke('Escape', () => {
|
||||
if (!overlayStore.isCurrent(overlayKey.value)) {
|
||||
onKeyStroke('Escape', event => {
|
||||
if (event.defaultPrevented || !overlayStore.isCurrent(overlayKey.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
- [xo-cli] fix output failing when outputing a rest api answer (PR [#10541](https://github.com/vatesfr/xen-orchestra/pull/10541))
|
||||
- [XO5/Self Service] Fix removing a tag from a resource set removing all the other tags instead (PR [#10550](https://github.com/vatesfr/xen-orchestra/pull/10550))
|
||||
- [Backups/DR] Report the actual XAPI error instead of `(intermediate value) is not iterable` when a VM import fails (PR [#10551](https://github.com/vatesfr/xen-orchestra/pull/10551/))
|
||||
- [VDI/Export] Fix pressing Escape in the format select closing the whole export drawer (PR [#10582](https://github.com/vatesfr/xen-orchestra/pull/10582))
|
||||
|
||||
### Packages to release
|
||||
|
||||
|
||||
Reference in New Issue
Block a user