This commit is contained in:
Timothy Jaeryang Baek 2026-11-05 22:25:44 +09:00
parent 4615d30a61
commit 75f8ae5584
9 changed files with 103 additions and 43 deletions

View file

@ -87,7 +87,7 @@
let scrollEnd = true;
let messagesContainerElement = null;
let chatInputElement = null;
let chatInputElement: { focus: () => void } | null = null;
let threadChatInputElement = null;
let top = false;
@ -148,6 +148,17 @@
);
};
let threadTrigger: HTMLButtonElement | null = null;
const closeThread = async () => {
const trigger = threadTrigger;
threadTrigger = null;
threadId = null;
await tick();
if (document.activeElement === document.body) {
(trigger?.isConnected ? trigger : chatInputElement)?.focus();
}
};
const pinHandler = (
messageId: string,
pinned: boolean,
@ -485,7 +496,8 @@
await tick();
chatInputElement?.focus();
}}
onThread={async (id) => {
onThread={async (id: string, trigger: HTMLButtonElement) => {
threadTrigger = trigger;
threadId = id;
await tick();
threadChatInputElement?.focus();
@ -541,22 +553,14 @@
{#if !largeScreen}
{#if threadId !== null}
<Drawer
show={threadId !== null}
ariaLabel={$i18n.t('Thread')}
onClose={() => {
threadId = null;
}}
>
<Drawer show={threadId !== null} ariaLabel={$i18n.t('Thread')} onClose={closeThread}>
<div class=" {threadId !== null ? ' h-screen w-full' : 'px-6 py-4'} h-full">
<Thread
{threadId}
{channel}
bind:chatInputElement={threadChatInputElement}
onPin={pinHandler}
onClose={() => {
threadId = null;
}}
onClose={closeThread}
/>
</div>
</Drawer>
@ -576,9 +580,7 @@
{channel}
bind:chatInputElement={threadChatInputElement}
onPin={pinHandler}
onClose={() => {
threadId = null;
}}
onClose={closeThread}
/>
</div>
</ResizableSidePanel>

View file

@ -218,9 +218,7 @@
}
);
}}
onThread={(id) => {
onThread(id);
}}
{onThread}
onReaction={(name) => {
if (
(message?.reactions ?? [])

View file

@ -122,6 +122,7 @@
};
let edit = false;
let editButtonElement: HTMLButtonElement;
let editedContent = null;
let showDeleteConfirmDialog = false;
$: renderedMessageId = message ? (id ? `${id}-${message.id}` : message.id) : null;
@ -335,8 +336,8 @@
<button
aria-label={$i18n.t('Reply in Thread')}
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
on:click={() => {
onThread(message.id);
on:click={(event) => {
onThread(message.id, event.currentTarget);
}}
>
<ChatBubbleOvalEllipsis />
@ -348,6 +349,7 @@
{#if onEdit}
<Tooltip content={$i18n.t('Edit')}>
<button
bind:this={editButtonElement}
aria-label={$i18n.t('Edit')}
id="edit-message-{renderedMessageId}"
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
@ -640,9 +642,11 @@
<button
id="close-edit-message-button"
class="flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-lg px-2.5 text-xs font-normal text-gray-600 transition hover:bg-gray-100 hover:text-gray-900 disabled:opacity-60 dark:text-gray-400 dark:hover:bg-gray-850 dark:hover:text-white"
on:click={() => {
on:click={async () => {
edit = false;
editedContent = null;
await tick();
editButtonElement?.focus();
}}
>
{$i18n.t('Cancel')}
@ -796,8 +800,8 @@
<div class="flex items-center gap-1.5 -mt-0.5 mb-1.5">
<button
class="flex min-w-0 items-center text-[0.6875rem] py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
on:click={() => {
onThread(message.id);
on:click={(event) => {
onThread(message.id, event.currentTarget);
}}
>
<span class="shrink-0 font-medium mr-1">

View file

@ -157,7 +157,10 @@
class=" w-full group rounded-xl relative flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-900 transition {buttonClassName}"
>
<h3 class="w-full">
<button class="w-full py-1.5 pl-2 flex items-center gap-1.5 text-xs font-normal">
<button
id={id ? `${id}-button` : undefined}
class="w-full py-1.5 pl-2 flex items-center gap-1.5 text-xs font-normal"
>
{#if chevron}
<div class=" p-[0.0625rem]">
{#if open}

View file

@ -38,6 +38,8 @@
} from '$lib/apis/chats';
import {
chatId,
folders,
pinnedChats,
chatTitle as _chatTitle,
mobile,
showSidebar,
@ -245,6 +247,7 @@
let showShareChatModal = false;
let confirmEdit = false;
let menuButtonElement: HTMLButtonElement;
let chatTitle = '';
@ -295,6 +298,38 @@
}
};
const refreshAfterChange = async (folderId: string | null = null, removed = false) => {
const restoreFocus =
document.activeElement === document.body || itemElement?.contains(document.activeElement);
const menus = Array.from(document.getElementsByName('sidebar-chat-menu'));
const index = menus.indexOf(menuButtonElement);
const neighbors = [...menus.slice(index + 1), ...menus.slice(0, index).reverse()].map(
(button) => button.id
);
await Promise.all([
refreshSidebar(localStorage.token),
getAllTags(localStorage.token).then((value) => tags.set(value))
]);
await tick();
if (!restoreFocus || document.activeElement !== document.body) return;
const fallback = folderId
? 'sidebar-folders-button'
: $pinnedChats.some((chat) => chat.id === id)
? 'sidebar-pinned-chats-button'
: 'sidebar-chats-button';
let target = removed
? neighbors.map((id) => document.getElementById(id)).find(Boolean)
: document.getElementById(`sidebar-chat-menu-${id}`);
const folderList: { id: string; parent_id: string | null }[] = $folders;
while (!target && folderId) {
target = document.getElementById(`folder-${folderId}-expand-button`);
folderId = folderList.find((folder) => folder.id === folderId)?.parent_id ?? null;
}
(target ?? document.getElementById(fallback))?.focus();
};
let deleting = false;
const deleteChatHandler = async (id) => {
@ -307,7 +342,6 @@
});
if (res) {
tags.set(await getAllTags(localStorage.token));
if ($chatId === id) {
await goto('/');
@ -315,7 +349,7 @@
await tick();
}
dispatch('change');
await refreshAfterChange(null, true);
}
deleting = false;
@ -335,7 +369,7 @@
chatId.set('');
}
dispatch('change');
await refreshAfterChange(null, true);
toast.success($i18n.t('Chat archived.'));
} catch (error) {
console.error('Error archiving chat:', error);
@ -355,9 +389,7 @@
);
if (res) {
await refreshChatList(localStorage.token, { refreshPinned: true });
dispatch('change');
await refreshAfterChange(folderId);
toast.success($i18n.t('Chat moved successfully'));
}
@ -366,7 +398,7 @@
}
};
let itemElement;
let itemElement: HTMLDivElement;
let generating = false;
@ -450,7 +482,7 @@
let showDeleteConfirm = false;
const chatTitleInputKeydownHandler = (e) => {
const chatTitleInputKeydownHandler = async (e: KeyboardEvent) => {
// Let Enter and Escape finish IME composition without saving or cancelling the rename.
if (e.isComposing || e.keyCode === 229) {
return;
@ -462,13 +494,14 @@
if (chatTitle !== title) {
editChatTitle(id, chatTitle);
}
}
confirmEdit = false;
chatTitle = '';
} else if (e.key === 'Escape') {
if (e.key === 'Enter' || e.key === 'Escape') {
e.preventDefault();
confirmEdit = false;
chatTitle = '';
await tick();
menuButtonElement?.focus();
}
};
@ -873,12 +906,13 @@
menuOpen = false;
dispatch('unselect');
}}
onPinChange={async () => {
dispatch('change');
}}
onPinChange={(chat) => refreshAfterChange(chat.pinned ? null : chat.folder_id)}
>
<button
type="button"
bind:this={menuButtonElement}
id="sidebar-chat-menu-{id}"
name="sidebar-chat-menu"
aria-label={$i18n.t('Chat Menu')}
class="flex {($settings?.highContrastMode ?? false)
? 'size-6'

View file

@ -52,11 +52,14 @@
let chat = null;
let showFullMessages = false;
export let onPinChange: () => void = () => {};
export let onPinChange: (chat: {
pinned: boolean;
folder_id: string | null;
}) => void | Promise<void> = () => {};
const pinHandler = async () => {
await toggleChatPinnedStatusById(localStorage.token, chatId);
onPinChange();
const chat = await toggleChatPinnedStatusById(localStorage.token, chatId);
await onPinChange(chat);
};
const checkPinned = async () => {

View file

@ -785,6 +785,7 @@
aria-label={open
? $i18n.t('Collapse {{name}}', { name: folders[folderId]?.name })
: $i18n.t('Expand {{name}}', { name: folders[folderId]?.name })}
id="folder-{folderId}-expand-button"
aria-expanded={open}
on:click={(e) => {
e.stopPropagation();

View file

@ -130,6 +130,7 @@
<button
type="button"
class="group flex flex-1 h-full items-center gap-1 text-left text-xs text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400 transition-colors duration-100 {buttonClassName}"
id="{id}-button"
aria-expanded={open}
aria-controls="{id}-content"
>

View file

@ -362,9 +362,12 @@
});
let noteCommentCount = 0;
$: if (currentNoteComments) showNoteChat = true;
const closeNotePanel = () => {
const closeNotePanel = async () => {
currentNoteComments = null;
showNoteChat = false;
await tick();
if (document.activeElement === document.body) noteChatButtonElement?.focus();
};
let noteChatId = null;
let noteChatLoading = false;
@ -390,6 +393,9 @@
let ignoreBlur = false;
let titleInputFocused = false;
let titleGenerating = false;
let titleInputElement: HTMLInputElement;
let noteChatButtonElement: HTMLButtonElement;
let pagesToggleElement: HTMLButtonElement;
let dragged = false;
let loading = false;
@ -579,6 +585,7 @@ ${content}
titleGenerating = false;
await tick();
if (document.activeElement === document.body) titleInputElement?.focus();
changeDebounceHandler();
};
@ -1150,6 +1157,7 @@ ${content}
? 'ml-1'
: ''}"
type="text"
bind:this={titleInputElement}
bind:value={note.title}
placeholder={titleGenerating ? $i18n.t('Generating...') : $i18n.t('Title')}
aria-label={$i18n.t('Title')}
@ -1249,6 +1257,7 @@ ${content}
type="button"
class="p-1 bg-transparent hover:bg-white/5 transition rounded-lg"
aria-label={$i18n.t('Chat')}
bind:this={noteChatButtonElement}
on:click={openNoteChat}
>
<ChatBubbleOval className="size-4" strokeWidth="1.8" />
@ -1423,6 +1432,7 @@ ${content}
class="flex max-w-full items-center gap-1 bg-transparent text-xs text-gray-500 hover:text-gray-900 dark:hover:text-gray-100 transition"
aria-expanded={showNoteTabs}
aria-controls="note-tabs-panel"
bind:this={pagesToggleElement}
on:click={() => {
showNoteTabs = !showNoteTabs;
}}
@ -1521,7 +1531,11 @@ ${content}
floating
>
<NoteTabs
onClose={() => (showNoteTabs = false)}
onClose={async () => {
showNoteTabs = false;
await tick();
pagesToggleElement?.focus();
}}
pages={note.data.pages.map((page) =>
page.id === 'main' && page.inherit_note_title !== false
? { ...page, title: note.title || $i18n.t('Untitled') }