From a5407131d4d15edca924e2718902cefd81e49ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:59:15 +0900 Subject: [PATCH 01/28] =?UTF-8?q?fix/refactor(frontend):=20hotkey=E3=81=AE?= =?UTF-8?q?=E6=94=B9=E4=BF=AE=20(#14157)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve(frontend): hotkeyの改修 (#234) (cherry picked from commit 678be147f4db709dadf25d007cc2e679e98a370e) * Change path, add missing script Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * fix * fix * add missing keycodes * fix * update changelog --------- Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> --- CHANGELOG.md | 2 + packages/frontend/src/boot/main-boot.ts | 27 +-- .../frontend/src/components/MkMediaAudio.vue | 47 +++-- .../frontend/src/components/MkMediaVideo.vue | 47 +++-- packages/frontend/src/components/MkMenu.vue | 20 +- packages/frontend/src/components/MkModal.vue | 8 +- packages/frontend/src/components/MkNote.vue | 57 +++++- .../src/components/MkNoteDetailed.vue | 26 ++- packages/frontend/src/directives/hotkey.ts | 2 +- .../frontend/src/pages/antenna-timeline.vue | 5 +- packages/frontend/src/pages/timeline.vue | 5 +- packages/frontend/src/scripts/hotkey.ts | 171 +++++++++++------- packages/frontend/src/scripts/keycode.ts | 24 --- 13 files changed, 272 insertions(+), 169 deletions(-) delete mode 100644 packages/frontend/src/scripts/keycode.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f62f9664519b..1bcf72167633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 - Fix: アンテナの編集画面のボタンに隙間を追加 - Fix: テーマプレビューが見れない問題を修正 +- Fix: ショートカットキーが連打できる問題を修正 + (Cherry-picked from https://github.com/taiyme/misskey/pull/234) ### Server - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index faf230a1a293..d32701631726 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -13,7 +13,6 @@ import * as sound from '@/scripts/sound.js'; import { $i, signout, updateAccount } from '@/account.js'; import { instance } from '@/instance.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js'; -import { makeHotkey } from '@/scripts/hotkey.js'; import { reactionPicker } from '@/scripts/reaction-picker.js'; import { miLocalStorage } from '@/local-storage.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; @@ -21,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; +import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -69,14 +69,6 @@ export async function mainBoot() { }); } - const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': (): void => { - mainRouter.push('/search'); - }, - }; try { if (defaultStore.state.enableSeasonalScreenEffect) { const month = new Date().getMonth() + 1; @@ -105,9 +97,6 @@ export async function mainBoot() { } if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; - defaultStore.loaded.then(() => { if (defaultStore.state.accountSetupWizard !== -1) { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { @@ -334,7 +323,19 @@ export async function mainBoot() { } // shortcut - document.addEventListener('keydown', makeHotkey(hotkeys)); + const keymap = { + 'p|n': () => { + if ($i == null) return; + post(); + }, + 'd': () => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': () => { + mainRouter.push('/search'); + }, + } as const satisfies Keymap; + document.addEventListener('keydown', makeHotkey(keymap), { passive: false }); initializeSw(); } diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index ebd4fc9ca418..e8dfcc776898 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -80,6 +80,7 @@ import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { type Keymap } from '@/scripts/hotkey.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import MkMediaRange from '@/components/MkMediaRange.vue'; @@ -90,32 +91,44 @@ const props = defineProps<{ }>(); const keymap = { - 'up': () => { - if (hasFocus() && audioEl.value) { - volume.value = Math.min(volume.value + 0.1, 1); - } + 'up': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + volume.value = Math.min(volume.value + 0.1, 1); + } + }, }, - 'down': () => { - if (hasFocus() && audioEl.value) { - volume.value = Math.max(volume.value - 0.1, 0); - } + 'down': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + volume.value = Math.max(volume.value - 0.1, 0); + } + }, }, - 'left': () => { - if (hasFocus() && audioEl.value) { - audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0); - } + 'left': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0); + } + }, }, - 'right': () => { - if (hasFocus() && audioEl.value) { - audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration); - } + 'right': { + allowRepeat: true, + callback: () => { + if (hasFocus() && audioEl.value) { + audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration); + } + }, }, 'space': () => { if (hasFocus()) { togglePlayPause(); } }, -}; +} as const satisfies Keymap; // PlayerElもしくはその子要素にフォーカスがあるかどうか function hasFocus() { diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 707d7c150111..7c46084c6301 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -112,6 +112,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import type { MenuItem } from '@/types/menu.js'; +import { type Keymap } from '@/scripts/hotkey.js'; import bytes from '@/filters/bytes.js'; import { hms } from '@/filters/hms.js'; import { defaultStore } from '@/store.js'; @@ -127,32 +128,44 @@ const props = defineProps<{ }>(); const keymap = { - 'up': () => { - if (hasFocus() && videoEl.value) { - volume.value = Math.min(volume.value + 0.1, 1); - } + 'up': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + volume.value = Math.min(volume.value + 0.1, 1); + } + }, }, - 'down': () => { - if (hasFocus() && videoEl.value) { - volume.value = Math.max(volume.value - 0.1, 0); - } + 'down': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + volume.value = Math.max(volume.value - 0.1, 0); + } + }, }, - 'left': () => { - if (hasFocus() && videoEl.value) { - videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0); - } + 'left': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0); + } + }, }, - 'right': () => { - if (hasFocus() && videoEl.value) { - videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration); - } + 'right': { + allowRepeat: true, + callback: () => { + if (hasFocus() && videoEl.value) { + videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration); + } + }, }, 'space': () => { if (hasFocus()) { togglePlayPause(); } }, -}; +} as const satisfies Keymap; // PlayerElもしくはその子要素にフォーカスがあるかどうか function hasFocus() { diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index d91239b9e221..119504f7448f 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -98,6 +98,7 @@ import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { isTouchUsing } from '@/scripts/touch.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const childrenCache = new WeakMap(); @@ -125,11 +126,20 @@ const items2 = ref(); const child = shallowRef>(); -const keymap = computed(() => ({ - 'up|k|shift+tab': focusUp, - 'down|j|tab': focusDown, - 'esc': close, -})); +const keymap = { + 'up|k|shift+tab': { + allowRepeat: true, + callback: () => focusUp(), + }, + 'down|j|tab': { + allowRepeat: true, + callback: () => focusDown(), + }, + 'esc': { + allowRepeat: true, + callback: () => close(false), + }, +} as const satisfies Keymap; const childShowingItem = ref(); diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue index 9e69ab2207f5..264d8b6c9cc8 100644 --- a/packages/frontend/src/components/MkModal.vue +++ b/packages/frontend/src/components/MkModal.vue @@ -47,6 +47,7 @@ import * as os from '@/os.js'; import { isTouchUsing } from '@/scripts/touch.js'; import { defaultStore } from '@/store.js'; import { deviceKind } from '@/scripts/device-kind.js'; +import { type Keymap } from '@/scripts/hotkey.js'; function getFixedContainer(el: Element | null): Element | null { if (el == null || el.tagName === 'BODY') return null; @@ -154,8 +155,11 @@ if (type.value === 'drawer') { } const keymap = { - 'esc': () => emit('esc'), -}; + 'esc': { + allowRepeat: true, + callback: () => emit('esc'), + }, +} as const satisfies Keymap; const MARGIN = 16; const SCROLLBAR_THICKNESS = 16; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 1313e4c58e5e..5f1820a37935 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -198,6 +198,7 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; import { isEnabledUrlPreview } from '@/instance.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -294,15 +295,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array reply(true), - 'e|a|plus': () => react(true), - 'q': () => renote(true), - 'up|k|shift+tab': focusBefore, - 'down|j|tab': focusAfter, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => { + if (renoteCollapsed.value) return; + reply(); + }, + 'e|a|plus': () => { + if (renoteCollapsed.value) return; + react(); + }, + 'q': () => { + if (renoteCollapsed.value) return; + renote(); + }, + 'm': () => { + if (renoteCollapsed.value) return; + showMenu(); + }, + 'c': () => { + if (renoteCollapsed.value) return; + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => { + if (renoteCollapsed.value) return; + galleryEl.value?.openGallery(); + }, + 'v|enter': () => { + if (renoteCollapsed.value) { + renoteCollapsed.value = false; + } else if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } else if (isLong) { + collapsed.value = !collapsed.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, + 'up|k|shift+tab': { + allowRepeat: true, + callback: () => focusBefore(), + }, + 'down|j|tab': { + allowRepeat: true, + callback: () => focusAfter(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index bc1f41637386..8f65e3b60acb 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -233,6 +233,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { isEnabledUrlPreview } from '@/instance.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -294,13 +295,24 @@ const replies = ref([]); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - 'q': () => renote(true), - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => reply(), + 'e|a|plus': () => react(), + 'q': () => renote(), + 'm': () => showMenu(), + 'c': () => { + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'v|enter': () => { + if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts index b082b6edf2d4..0a7d136f182c 100644 --- a/packages/frontend/src/directives/hotkey.ts +++ b/packages/frontend/src/directives/hotkey.ts @@ -4,7 +4,7 @@ */ import { Directive } from 'vue'; -import { makeHotkey } from '../scripts/hotkey.js'; +import { makeHotkey } from '@/scripts/hotkey.js'; export default { mounted(el, binding) { diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue index 273250d1d000..ea64e457e376 100644 --- a/packages/frontend/src/pages/antenna-timeline.vue +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
(null); const queue = ref(0); const rootEl = shallowRef(); const tlEl = shallowRef>(); -const keymap = computed(() => ({ - 't': focus, -})); function queueUpdated(q) { queue.value = q; diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 98744c631855..813cc326d097 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
{{ i18n.ts._timelineDescription[src] }} @@ -58,9 +58,6 @@ provide('shouldOmitHeaderTitle', true); const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); -const keymap = { - 't': focus, -}; const tlComponent = shallowRef>(); const rootEl = shallowRef(); diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts index 0600bff893b3..fd79baa60484 100644 --- a/packages/frontend/src/scripts/hotkey.ts +++ b/packages/frontend/src/scripts/hotkey.ts @@ -3,93 +3,132 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import keyCode from './keycode.js'; +//#region types +export type Keymap = Record; -type Callback = (ev: KeyboardEvent) => void; +type CallbackFunction = (ev: KeyboardEvent) => unknown; -type Keymap = Record; +type CallbackObject = { + callback: CallbackFunction; + allowRepeat?: boolean; +}; type Pattern = { which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; }; type Action = { patterns: Pattern[]; - callback: Callback; - allowRepeat: boolean; + callback: CallbackFunction; + options: Required>; +}; +//#endregion + +//#region consts +const KEY_ALIASES = { + 'esc': 'Escape', + 'enter': ['Enter', 'NumpadEnter'], + 'space': [' ', 'Spacebar'], + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'plus': ['+', ';'], }; -const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { - const result = { - patterns: [], - callback, - allowRepeat: true, - } as Action; +const MODIFIER_KEYS = ['ctrl', 'alt', 'shift']; - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } +const IGNORE_ELEMENTS = ['input', 'textarea']; +//#endregion - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false, - } as Pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); +//#region impl +export const makeHotkey = (keymap: Keymap) => { + const actions = parseKeymap(keymap); + return (ev: KeyboardEvent) => { + if ('pswp' in window && window.pswp != null) return; + if (document.activeElement != null) { + if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return; + if ((document.activeElement as HTMLElement).isContentEditable) return; + } + for (const { patterns, callback, options } of actions) { + if (matchPatterns(ev, patterns, options)) { + ev.preventDefault(); + ev.stopPropagation(); + callback(ev); } } + }; +}; - return pattern; +const parseKeymap = (keymap: Keymap) => { + return Object.entries(keymap).map(([rawPatterns, rawCallback]) => { + const patterns = parsePatterns(rawPatterns); + const callback = parseCallback(rawCallback); + const options = parseOptions(rawCallback); + return { patterns, callback, options } as const satisfies Action; }); +}; - return result; -}); - -const ignoreElements = ['input', 'textarea']; - -function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = ev.key.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === ev.ctrlKey && - pattern.shift === ev.shiftKey && - pattern.alt === ev.altKey && - !ev.metaKey, - ); -} +const parsePatterns = (rawPatterns: keyof Keymap) => { + return rawPatterns.split('|').map(part => { + const keys = part.split('+').map(trimLower); + const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x))); + const ctrl = keys.includes('ctrl'); + const alt = keys.includes('alt'); + const shift = keys.includes('shift'); + return { which, ctrl, alt, shift } as const satisfies Pattern; + }); +}; -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); +const parseCallback = (rawCallback: Keymap[keyof Keymap]) => { + if (typeof rawCallback === 'object') { + return rawCallback.callback; + } + return rawCallback; +}; - return (ev: KeyboardEvent) => { - if (document.activeElement) { - if (ignoreElements.some(el => document.activeElement!.matches(el))) return; - if (document.activeElement.attributes['contenteditable']) return; - } +const parseOptions = (rawCallback: Keymap[keyof Keymap]) => { + const defaultOptions = { + allowRepeat: false, + } as const satisfies Action['options']; + if (typeof rawCallback === 'object') { + const { callback, ...rawOptions } = rawCallback; + const options = { ...defaultOptions, ...rawOptions }; + return { ...options } as const satisfies Action['options']; + } + return { ...defaultOptions } as const satisfies Action['options']; +}; - for (const action of actions) { - const matched = match(ev, action.patterns); +const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options: Action['options']) => { + if (ev.repeat && !options.allowRepeat) return false; + const key = ev.key.toLowerCase(); + return patterns.some(({ which, ctrl, shift, alt }) => { + if (!which.includes(key)) return false; + if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false; + if (alt !== ev.altKey) return false; + if (shift !== ev.shiftKey) return false; + return true; + }); +}; - if (matched) { - if (!action.allowRepeat && ev.repeat) return; +const parseKeyCode = (input?: string | null) => { + if (input == null) return []; + const raw = getValueByKey(KEY_ALIASES, input); + if (raw == null) return [input]; + if (typeof raw === 'string') return [trimLower(raw)]; + return raw.map(trimLower); +}; - ev.preventDefault(); - ev.stopPropagation(); - action.callback(ev); - break; - } - } - }; +const getValueByKey = < + T extends Record, + K extends keyof T | keyof any, + R extends K extends keyof T ? T[K] : T[keyof T] | undefined, +>(obj: T, key: K) => { + return obj[key] as R; }; + +const trimLower = (str: string) => str.trim().toLowerCase(); +//#endregion diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts deleted file mode 100644 index 7ffceafadae1..000000000000 --- a/packages/frontend/src/scripts/keycode.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default (input: string): string[] => { - if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { - const codes = aliases[input]; - return Array.isArray(codes) ? codes : [codes]; - } else { - return [input]; - } -}; - -export const aliases = { - 'esc': 'Escape', - 'enter': ['Enter', 'NumpadEnter'], - 'space': [' ', 'Spacebar'], - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['NumpadAdd', 'Semicolon'], -}; From 600f16d625d8b4c8e7f8a82fb4dd1a77f80bfcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:57:19 +0900 Subject: [PATCH 02/28] =?UTF-8?q?fix(backend):=20api-doc=E3=82=92Scalar?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4=20(#14152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): api-docをScalarに変更 * Update Changelog --- CHANGELOG.md | 1 + packages/backend/assets/api-doc.html | 20 ++++++++++++++++ packages/backend/assets/redoc.html | 24 ------------------- .../api/openapi/OpenApiServerService.ts | 2 +- .../src/server/api/openapi/gen-spec.ts | 1 - 5 files changed, 22 insertions(+), 26 deletions(-) create mode 100644 packages/backend/assets/api-doc.html delete mode 100644 packages/backend/assets/redoc.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcf72167633..b99a2cbc53bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 ### Client +- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: リバーシの対局を正しく共有できないことがある問題を修正 diff --git a/packages/backend/assets/api-doc.html b/packages/backend/assets/api-doc.html new file mode 100644 index 000000000000..19e0349d47d9 --- /dev/null +++ b/packages/backend/assets/api-doc.html @@ -0,0 +1,20 @@ + + + + Misskey API + + + + + + + + + diff --git a/packages/backend/assets/redoc.html b/packages/backend/assets/redoc.html deleted file mode 100644 index 2557b4532ec3..000000000000 --- a/packages/backend/assets/redoc.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Misskey API - - - - - - - - - - - - - diff --git a/packages/backend/src/server/api/openapi/OpenApiServerService.ts b/packages/backend/src/server/api/openapi/OpenApiServerService.ts index 5210e4d2bc3c..f124aa9f39e8 100644 --- a/packages/backend/src/server/api/openapi/OpenApiServerService.ts +++ b/packages/backend/src/server/api/openapi/OpenApiServerService.ts @@ -25,7 +25,7 @@ export class OpenApiServerService { public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { fastify.get('/api-doc', async (_request, reply) => { reply.header('Cache-Control', 'public, max-age=86400'); - return await reply.sendFile('/redoc.html', staticAssets); + return await reply.sendFile('/api-doc.html', staticAssets); }); fastify.get('/api.json', (_request, reply) => { reply.header('Cache-Control', 'public, max-age=600'); diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 2a14270a2497..efa47a698624 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { info: { version: config.version, title: 'Misskey API', - 'x-logo': { url: '/static-assets/api-doc.png' }, }, externalDocs: { From 02e0a86b12473265c2fc0f219349d2c662f89f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 10 Jul 2024 01:00:40 +0900 Subject: [PATCH 03/28] fix(frontend): remove unused statement fix #14162 --- packages/frontend/src/components/MkNote.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 5f1820a37935..fc728132851c 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -318,7 +318,7 @@ const keymap = { }, 'o': () => { if (renoteCollapsed.value) return; - galleryEl.value?.openGallery(); + showMenu(); }, 'v|enter': () => { if (renoteCollapsed.value) { From 52d8a54fc72b886fecb30a736b3ccf5057ea2a0c Mon Sep 17 00:00:00 2001 From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com> Date: Wed, 10 Jul 2024 20:40:04 +0900 Subject: [PATCH 04/28] =?UTF-8?q?feat(misskey-js):=20`POST=20admin/roles/c?= =?UTF-8?q?reate`=E3=81=AE=E5=9E=8B=E3=82=92=E5=85=B7=E8=B1=A1=E5=8C=96=20?= =?UTF-8?q?(#14167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(misskey-js): `POST admin/roles/create`の型を具象化 * fix * docs: CHANGELOG.md * test(misskey-js): admin/roles/createの型が合うことを表明 * test(misskey-js): single quote * test(misskey-js): 無を読もうとして爆発するのを修正 * test(misskey-js): fix comment --- CHANGELOG.md | 1 + packages/misskey-js/etc/misskey-js.api.md | 18 ++++++++++- packages/misskey-js/src/api.types.ts | 7 ++++- packages/misskey-js/src/entities.ts | 15 ++++++++- packages/misskey-js/test/api.ts | 38 +++++++++++++++++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99a2cbc53bb..aec4e1868c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ ### Misskey.js - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) +- Feat: `/admin/role/create` のロールポリシーの型を修正 ## 2024.5.0 diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index be2f510ac279..d11d2a4f0617 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1160,6 +1160,12 @@ export type Endpoints = Overwrite; + res: AdminRolesCreateResponse; + }; }>; // @public (undocumented) @@ -1185,6 +1191,7 @@ declare namespace entities { SignupPendingResponse, SigninRequest, SigninResponse, + PartialRolePolicyOverride, EmptyRequest, EmptyResponse, AdminMetaResponse, @@ -2725,6 +2732,15 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content'] // @public (undocumented) function parse(acct: string): Acct; +// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PartialRolePolicyOverride = Partial<{ + [k in keyof RolePolicies]: Omit, 'value'> & { + value: RolePolicies[k]; + }; +}>; + // @public (undocumented) export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; @@ -3213,7 +3229,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody'][' // Warnings were encountered during analysis: // -// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts +// src/entities.ts:34:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts index af0bade5b3b4..8c403639b788 100644 --- a/packages/misskey-js/src/api.types.ts +++ b/packages/misskey-js/src/api.types.ts @@ -1,7 +1,8 @@ import { Endpoints as Gen } from './autogen/endpoint.js'; import { UserDetailed } from './autogen/models.js'; -import { UsersShowRequest } from './autogen/entities.js'; +import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js'; import { + PartialRolePolicyOverride, SigninRequest, SigninResponse, SignupPendingRequest, @@ -79,5 +80,9 @@ export type Endpoints = Overwrite< req: SigninRequest; res: SigninResponse; }, + 'admin/roles/create': { + req: Overwrite; + res: AdminRolesCreateResponse; + } } > diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 7a84cb6a1a63..7331a55a1cac 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -1,5 +1,14 @@ import { ModerationLogPayloads } from './consts.js'; -import { Announcement, EmojiDetailed, MeDetailed, Page, User, UserDetailedNotMe } from './autogen/models.js'; +import { + Announcement, + EmojiDetailed, + MeDetailed, + Page, + Role, + RolePolicies, + User, + UserDetailedNotMe +} from './autogen/models.js'; export * from './autogen/entities.js'; export * from './autogen/models.js'; @@ -236,3 +245,7 @@ export type SigninResponse = { id: User['id'], i: string, }; + +type Values> = T[keyof T]; + +export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit, 'value'> & { value: RolePolicies[k] }}>; diff --git a/packages/misskey-js/test/api.ts b/packages/misskey-js/test/api.ts index 95f1946fa27b..1a7574de25e7 100644 --- a/packages/misskey-js/test/api.ts +++ b/packages/misskey-js/test/api.ts @@ -259,4 +259,42 @@ describe('API', () => { expect(isAPIError(e)).toEqual(false); } }); + + test('admin/roles/create の型が合う', async() => { + fetchMock.resetMocks(); + fetchMock.mockResponse(async () => { + return { + // 本来返すべき値は`Role`型だが、テストなのでお茶を濁す + status: 200, + body: '{}' + }; + }); + + const cli = new APIClient({ + origin: 'https://misskey.test', + credential: 'TOKEN', + }); + await cli.request('admin/roles/create', { + name: 'aaa', + asBadge: false, + canEditMembersByModerator: false, + color: '#123456', + condFormula: {}, + description: '', + displayOrder: 0, + iconUrl: '', + isAdministrator: false, + isExplorable: false, + isModerator: false, + isPublic: false, + policies: { + ltlAvailable: { + value: true, + priority: 0, + useDefault: false, + }, + }, + target: 'manual', + }); + }) }); From 679318541afb789842db5f2cbf918b8acf284f1d Mon Sep 17 00:00:00 2001 From: woxtu Date: Thu, 11 Jul 2024 16:29:18 +0900 Subject: [PATCH 05/28] Improve background color specification (#14176) --- packages/frontend/src/pages/settings/drive-cleaner.vue | 6 +++--- packages/frontend/src/pages/settings/drive.vue | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index b20774c4ec50..8d2946db63f9 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue index 139b2e0a07d4..db326f9e6cf4 100644 --- a/packages/frontend/src/pages/welcome.timeline.vue +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -4,24 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only From 121af778a0925d5850e9d88261e9a8e8c6fd968b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 11 Jul 2024 18:44:18 +0900 Subject: [PATCH 08/28] =?UTF-8?q?enhance(frontend):=20=E6=9C=AA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E3=81=AE=E3=82=B5=E3=82=A6=E3=83=B3=E3=83=89=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E5=89=8A=E9=99=A4=20(#14116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): 未使用のサウンド設定を削除 * Update Changelog * Update CHANGELOG.md --- CHANGELOG.md | 3 +++ locales/index.d.ts | 8 -------- locales/ja-JP.yml | 2 -- .../frontend/src/pages/settings/preferences-backups.vue | 2 -- packages/frontend/src/pages/settings/sounds.vue | 2 -- packages/frontend/src/scripts/sound.ts | 2 -- packages/frontend/src/store.ts | 8 -------- 7 files changed, 3 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 259420a6c608..cd123c938e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +### Note +- デッキUIの新着ノートをサウンドで通知する機能の追加(v2024.5.0)に伴い、以前から動作しなくなっていたクライアント設定内の「アンテナ受信」「チャンネル通知」サウンドを削除しました。 + ### General - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index ebd980ed8555..5089f7802e82 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -7515,14 +7515,6 @@ export interface Locale extends ILocale { * 通知 */ "notification": string; - /** - * アンテナ受信 - */ - "antenna": string; - /** - * チャンネル通知 - */ - "channel": string; /** * リアクション選択時 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0d89d33abeec..a03d7921401d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1971,8 +1971,6 @@ _sfx: note: "ノート" noteMy: "ノート(自分)" notification: "通知" - antenna: "アンテナ受信" - channel: "チャンネル通知" reaction: "リアクション選択時" _soundSettings: diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index b6f1043154d3..dace2cd84758 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -113,8 +113,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [ 'sound_note', 'sound_noteMy', 'sound_notification', - 'sound_antenna', - 'sound_channel', ]; const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ 'lightTheme', diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue index 090f0cf14c41..0f1b725fae39 100644 --- a/packages/frontend/src/pages/settings/sounds.vue +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -54,8 +54,6 @@ const sounds = ref>>({ note: defaultStore.reactiveState.sound_note, noteMy: defaultStore.reactiveState.sound_noteMy, notification: defaultStore.reactiveState.sound_notification, - antenna: defaultStore.reactiveState.sound_antenna, - channel: defaultStore.reactiveState.sound_channel, reaction: defaultStore.reactiveState.sound_reaction, }); diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index fcd59510df18..bba855cd6437 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -74,8 +74,6 @@ export const soundsTypes = [ export const operationTypes = [ 'noteMy', 'note', - 'antenna', - 'channel', 'notification', 'reaction', ] as const; diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index e8eb5a1ed7db..9cb274206955 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -479,14 +479,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore, }, - sound_antenna: { - where: 'device', - default: { type: 'syuilo/triple', volume: 1 } as SoundStore, - }, - sound_channel: { - where: 'device', - default: { type: 'syuilo/square-pico', volume: 1 } as SoundStore, - }, sound_reaction: { where: 'device', default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, From 385969e9f56a39a1e5547b94901d155e1e811263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:25:44 +0900 Subject: [PATCH 09/28] =?UTF-8?q?fix(frontend):=20=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=BC=E3=82=AB=E3=82=B9=E3=81=AE=E6=8C=99=E5=8B=95=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#14158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): 直前のパターンを記録するように * fix(frontend): フォーカス/タブ移動に関する挙動を調整 (#226) Cherry-pick commit e8c030673326871edf3623cf2b8675d68f9e1b13 Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> * focusのデザイン修正 * move scripts * Modalにfocus trapを追加 * 記録するホットキーはレートリミット式にする * escキーのハンドリングをMkModalに統一 * fix * enterで子メニューを開けるように * lint * fix focus trap * improve switch accessibility * 一部のmodalのフォーカストラップが外れない問題を修正 * fix * fix * Revert "記録するホットキーはレートリミット式にする" This reverts commit 40a7509286a87911ad4cc06d9482e8a2e5d0e7e8. * Revert "fix(frontend): 直前のパターンを記録するように" This reverts commit 5372b2594023952cff34aa62253ed4efef15b5dd. * Revert "Revert "fix(frontend): 直前のパターンを記録するように"" This reverts commit a9bb52e799e110927ad92cd8f26af980819334e1. * Revert "Revert "記録するホットキーはレートリミット式にする"" This reverts commit bdac34273e0bc5f13604c7e2f9fa6b1321a0df3d. * 試験的にCypressでのFocustrapを無効化 * fix * fix focus-trap * Update Changelog * :v: * fix focustrap invocation logic * スクロールがsticky headerを考慮するように * :art: * スタイルの微調整 * :art: * remove deprecated key aliases * focusElementが足りなかったので修正 * preview系にfocus時スタイルが足りなかったので修正 * `returnFocusElement` -> `returnFocusTo` * lint * Update packages/frontend/src/components/MkModalWindow.vue * Apply suggestions from code review Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com> * keydownイベントをまとめる * use correct pesudo-element selector * fix * rename --------- Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 + .../src/components/MkAchievements.vue | 4 +- packages/frontend/src/components/MkButton.vue | 1 - .../src/components/MkChannelFollowButton.vue | 12 +- .../src/components/MkChannelPreview.vue | 19 +- .../frontend/src/components/MkClipPreview.vue | 8 + .../frontend/src/components/MkContextMenu.vue | 2 +- .../frontend/src/components/MkCwButton.vue | 4 +- packages/frontend/src/components/MkDialog.vue | 16 +- .../frontend/src/components/MkDrive.file.vue | 32 +- .../src/components/MkDrive.folder.vue | 2 +- .../frontend/src/components/MkEmojiPicker.vue | 33 +- .../src/components/MkEmojiPickerDialog.vue | 2 + .../src/components/MkFlashPreview.vue | 6 +- packages/frontend/src/components/MkFolder.vue | 16 +- .../src/components/MkFollowButton.vue | 12 +- .../src/components/MkGalleryPostPreview.vue | 4 +- .../src/components/MkImgWithBlurhash.vue | 4 +- .../frontend/src/components/MkLaunchPad.vue | 2 +- .../frontend/src/components/MkMediaAudio.vue | 26 +- .../frontend/src/components/MkMediaList.vue | 54 ++- .../frontend/src/components/MkMediaVideo.vue | 10 +- .../frontend/src/components/MkMenu.child.vue | 5 +- packages/frontend/src/components/MkMenu.vue | 345 +++++++++++------- packages/frontend/src/components/MkModal.vue | 28 +- .../frontend/src/components/MkModalWindow.vue | 13 +- packages/frontend/src/components/MkNote.vue | 48 ++- .../src/components/MkNoteDetailed.vue | 62 ++-- .../frontend/src/components/MkNotePreview.vue | 2 +- .../src/components/MkNotification.vue | 2 +- .../frontend/src/components/MkPagePreview.vue | 19 +- .../frontend/src/components/MkPopupMenu.vue | 6 +- .../frontend/src/components/MkPostForm.vue | 10 + .../src/components/MkPostFormDialog.vue | 2 +- packages/frontend/src/components/MkRadio.vue | 10 +- packages/frontend/src/components/MkSelect.vue | 27 +- .../frontend/src/components/MkSuperMenu.vue | 10 +- packages/frontend/src/components/MkSwitch.vue | 10 +- .../components/MkTutorialDialog.PostNote.vue | 2 +- .../components/MkTutorialDialog.Sensitive.vue | 2 +- .../components/MkTutorialDialog.Timeline.vue | 2 +- .../src/components/MkVisibilityPicker.vue | 2 +- .../components/global/MkStickyContainer.vue | 6 +- packages/frontend/src/directives/hotkey.ts | 4 +- packages/frontend/src/os.ts | 27 +- .../frontend/src/pages/drive.file.info.vue | 1 + packages/frontend/src/pages/games.vue | 11 +- packages/frontend/src/pages/page.vue | 1 + .../frontend/src/pages/settings/profile.vue | 1 + .../frontend/src/pages/settings/theme.vue | 6 + packages/frontend/src/scripts/focus-trap.ts | 65 ++++ packages/frontend/src/scripts/focus.ts | 96 +++-- .../src/scripts/get-dom-node-or-null.ts | 19 + packages/frontend/src/scripts/hotkey.ts | 51 ++- packages/frontend/src/scripts/scroll.ts | 8 + packages/frontend/src/style.scss | 25 +- packages/frontend/src/ui/_common_/common.vue | 2 +- .../src/ui/_common_/navbar-for-mobile.vue | 6 +- packages/frontend/src/ui/_common_/navbar.vue | 86 ++++- packages/frontend/src/ui/deck/column.vue | 4 +- .../frontend/src/widgets/WidgetCalendar.vue | 2 +- 61 files changed, 920 insertions(+), 379 deletions(-) create mode 100644 packages/frontend/src/scripts/focus-trap.ts create mode 100644 packages/frontend/src/scripts/get-dom-node-or-null.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cd123c938e18..c6f48684b904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ ### Client - Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 - Enhance: 非ログイン時のハイライトTLのデザインを改善 +- Enhance: フロントエンドのアクセシビリティ改善 + (Based on https://github.com/taiyme/misskey/pull/226) - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: リバーシの対局を正しく共有できないことがある問題を修正 diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue index 5d103fa789af..c8134416b506 100644 --- a/packages/frontend/src/components/MkAchievements.vue +++ b/packages/frontend/src/components/MkAchievements.vue @@ -153,7 +153,7 @@ onMounted(() => { background: linear-gradient(0deg, #ffee20, #eb7018); } - &:before { + &::before { content: ""; display: block; position: absolute; @@ -173,7 +173,7 @@ onMounted(() => { background: linear-gradient(0deg, #e1e1e1, #7c7c7c); } - &:before { + &::before { content: ""; display: block; position: absolute; diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 25b003ba5a59..9560efb7d95f 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -250,7 +250,6 @@ function onMousedown(evt: MouseEvent): void { } &:focus-visible { - outline: solid 2px var(--focus); outline-offset: 2px; } diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue index 841d37a56847..35dc3ad4bf83 100644 --- a/packages/frontend/src/components/MkChannelFollowButton.vue +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -87,17 +87,7 @@ async function onClick() { } &:focus-visible { - &:after { - content: ""; - pointer-events: none; - position: absolute; - top: -5px; - right: -5px; - bottom: -5px; - left: -5px; - border: 2px solid var(--focus); - border-radius: 32px; - } + outline-offset: 2px; } &:hover { diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue index 4ff64dc4baff..c30cb66c078c 100644 --- a/packages/frontend/src/components/MkChannelPreview.vue +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only