mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
feat(web): support Enter key to confirm archive and other dialogs (#1490)
This commit is contained in:
parent
2206d21327
commit
b30a45efec
3 changed files with 65 additions and 3 deletions
5
.changeset/web-confirm-dialog-enter-key.md
Normal file
5
.changeset/web-confirm-dialog-enter-key.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
web: Press Enter to confirm in archive and other confirmation dialogs.
|
||||
|
|
@ -3,11 +3,19 @@
|
|||
Dialog (height auto, right-aligned footer). The single confirmation surface
|
||||
for user actions — driven app-wide by useConfirmDialog(). -->
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from '../ui/Dialog.vue';
|
||||
import Button from '../ui/Button.vue';
|
||||
|
||||
withDefaults(defineProps<{
|
||||
const confirmButtonRef = ref<InstanceType<typeof Button> | null>(null);
|
||||
|
||||
function confirmButtonElement(): HTMLElement | null {
|
||||
const el = confirmButtonRef.value?.$el;
|
||||
return el instanceof HTMLElement ? el : null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
open: boolean;
|
||||
title: string;
|
||||
message?: string;
|
||||
|
|
@ -32,6 +40,33 @@ function onCancel(): void {
|
|||
emit('update:open', false);
|
||||
emit('cancel');
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key !== 'Enter' || !props.open || props.loading) return;
|
||||
// Preserve native Enter semantics for interactive controls (buttons, links,
|
||||
// form fields) so tabbing to Cancel / Close and pressing Enter does not
|
||||
// accidentally confirm the dialog. Only treat Enter as confirm when focus is
|
||||
// on a non-interactive part of the dialog.
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (
|
||||
target instanceof HTMLButtonElement ||
|
||||
target instanceof HTMLAnchorElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target instanceof HTMLInputElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
emit('confirm');
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('keydown', onKeydown);
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') window.removeEventListener('keydown', onKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -39,6 +74,7 @@ function onCancel(): void {
|
|||
:open="open"
|
||||
:title="title"
|
||||
height="auto"
|
||||
:initial-focus="confirmButtonElement"
|
||||
@update:open="emit('update:open', $event)"
|
||||
@close="onCancel"
|
||||
>
|
||||
|
|
@ -47,7 +83,12 @@ function onCancel(): void {
|
|||
<Button variant="secondary" :disabled="loading" @click="onCancel">
|
||||
{{ cancelLabel ?? t('common.cancel') }}
|
||||
</Button>
|
||||
<Button :variant="variant" :loading="loading" @click="emit('confirm')">
|
||||
<Button
|
||||
ref="confirmButtonRef"
|
||||
:variant="variant"
|
||||
:loading="loading"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
{{ confirmLabel ?? t('common.confirm') }}
|
||||
</Button>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const props = withDefaults(defineProps<{
|
|||
/** When false, the body has no padding so the consumer controls layout
|
||||
* (e.g. a full-bleed side-nav). */
|
||||
padded?: boolean;
|
||||
/** Element (or selector / resolver) to receive focus when the dialog opens.
|
||||
* Falls back to the first focusable element, then the dialog panel. */
|
||||
initialFocus?: HTMLElement | string | (() => HTMLElement | null | undefined);
|
||||
}>(), {
|
||||
closeOnOverlay: true,
|
||||
closeOnEsc: true,
|
||||
|
|
@ -50,6 +53,18 @@ function focusables(): HTMLElement[] {
|
|||
return panel.value ? Array.from(panel.value.querySelectorAll<HTMLElement>(FOCUSABLE)) : [];
|
||||
}
|
||||
|
||||
function resolveInitialFocus(): HTMLElement | null {
|
||||
const { initialFocus } = props;
|
||||
if (!initialFocus) return null;
|
||||
if (typeof initialFocus === 'function') {
|
||||
return initialFocus() ?? null;
|
||||
}
|
||||
if (typeof initialFocus === 'string') {
|
||||
return panel.value?.querySelector<HTMLElement>(initialFocus) ?? null;
|
||||
}
|
||||
return panel.value?.contains(initialFocus) ? initialFocus : null;
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (!props.open) return;
|
||||
if (event.key === 'Escape' && props.closeOnEsc) {
|
||||
|
|
@ -87,8 +102,9 @@ watch(
|
|||
openDialogCount.value += 1;
|
||||
previouslyFocused = document.activeElement;
|
||||
await nextTick();
|
||||
const initial = resolveInitialFocus();
|
||||
const list = focusables();
|
||||
(list[0] ?? panel.value)?.focus();
|
||||
(initial ?? list[0] ?? panel.value)?.focus();
|
||||
} else {
|
||||
openDialogCount.value = Math.max(0, openDialogCount.value - 1);
|
||||
if (previouslyFocused instanceof HTMLElement) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue