From 385969e9f56a39a1e5547b94901d155e1e811263 Mon Sep 17 00:00:00 2001
From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 12 Jul 2024 16:25:44 +0900
Subject: fix(frontend): フォーカスの挙動を修正 (#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>
---
 packages/frontend/src/components/MkMenu.vue | 345 ++++++++++++++++++----------
 1 file changed, 218 insertions(+), 127 deletions(-)

(limited to 'packages/frontend/src/components/MkMenu.vue')

diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 119504f744..68479989b2 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -4,23 +4,42 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div role="menu">
+<div role="menu" @focusin.passive.stop="() => {}">
 	<div
-		ref="itemsEl" v-hotkey="keymap"
+		ref="itemsEl"
+		v-hotkey="keymap"
+		tabindex="0"
 		class="_popup _shadow"
-		:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: asDrawer }]"
-		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
-		@contextmenu.self="e => e.preventDefault()"
+		:class="{
+			[$style.root]: true,
+			[$style.center]: align === 'center',
+			[$style.asDrawer]: asDrawer,
+		}"
+		:style="{
+			width: (width && !asDrawer) ? `${width}px` : '',
+			maxHeight: maxHeight ? `${maxHeight}px` : '',
+		}"
+		@keydown.stop="() => {}"
+		@contextmenu.self.prevent="() => {}"
 	>
-		<template v-for="(item, i) in (items2 ?? [])">
-			<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
-			<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
+		<template v-for="item in (items2 ?? [])">
+			<div v-if="item.type === 'divider'" role="separator" tabindex="-1" :class="$style.divider"></div>
+			<span v-else-if="item.type === 'label'" role="menuitem" tabindex="-1" :class="[$style.label, $style.item]">
 				<span style="opacity: 0.7;">{{ item.text }}</span>
 			</span>
-			<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
+			<span v-else-if="item.type === 'pending'" role="menuitem" tabindex="0" :class="[$style.pending, $style.item]">
 				<span><MkEllipsis/></span>
 			</span>
-			<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<MkA
+				v-else-if="item.type === 'link'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:to="item.to"
+				@click.passive="close(true)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
@@ -28,20 +47,48 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 				</div>
 			</MkA>
-			<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<a
+				v-else-if="item.type === 'a'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:href="item.href"
+				:target="item.target"
+				:download="item.download"
+				@click.passive="close(true)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
 					<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 				</div>
 			</a>
-			<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'user'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, { [$style.active]: item.active }]"
+				@click.prevent="item.active ? close(false) : clicked(item.action, $event)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
 				<div v-if="item.indicate" :class="$style.item_content">
 					<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'switch'"
+				role="menuitemcheckbox"
+				tabindex="0"
+				:class="['_button', $style.item]"
+				:disabled="unref(item.disabled)"
+				@click.prevent="switchItem(item)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkSwitchButton v-else :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 				<div :class="$style.item_content">
@@ -49,29 +96,61 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitchButton v-if="item.icon" :class="[$style.switchButton, $style.caret]" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'radio'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showRadioOptions(item, $event)" @click="!preferClick ? null : showRadioOptions(item, $event)">
+			<button
+				v-else-if="item.type === 'radio'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
+				:disabled="unref(item.disabled)"
+				@mouseenter.prevent="preferClick ? null : showRadioOptions(item, $event)"
+				@keydown.enter.prevent="preferClick ? null : showRadioOptions(item, $event)"
+				@click.prevent="!preferClick ? null : showRadioOptions(item, $event)"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
 					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'radioOption'" :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.radioActive]: item.active }]" @click="clicked(item.action, $event, false)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else-if="item.type === 'radioOption'"
+				role="menuitemradio"
+				tabindex="0"
+				:class="['_button', $style.item, $style.radio, { [$style.active]: unref(item.active) }]"
+				@click.prevent="unref(item.active) ? null : clicked(item.action, $event, false)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<div :class="$style.icon">
-					<span :class="[$style.radio, { [$style.radioChecked]: item.active }]"></span>
+					<span :class="[$style.radioIcon, { [$style.radioChecked]: unref(item.active) }]"></span>
 				</div>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text">{{ item.text }}</span>
 				</div>
 			</button>
-			<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
+			<button
+				v-else-if="item.type === 'parent'"
+				role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, $style.parent, { [$style.active]: childShowingItem === item }]"
+				@mouseenter.prevent="preferClick ? null : showChildren(item, $event)"
+				@keydown.enter.prevent="preferClick ? null : showChildren(item, $event)"
+				@click.prevent="!preferClick ? null : showChildren(item, $event)"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
 				<div :class="$style.item_content">
 					<span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
 					<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
 				</div>
 			</button>
-			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button
+				v-else role="menuitem"
+				tabindex="0"
+				:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
+				@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
+				@mouseenter.passive="onItemMouseEnter"
+				@mouseleave.passive="onItemMouseLeave"
+			>
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
@@ -80,25 +159,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</button>
 		</template>
-		<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
+		<span v-if="items2 == null || items2.length === 0" tabindex="-1" :class="[$style.none, $style.item]">
 			<span>{{ i18n.ts.none }}</span>
 		</span>
 	</div>
 	<div v-if="childMenu">
-		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
+		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" @actioned="childActioned" @closed="closeChild"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
-import { focusPrev, focusNext } from '@/scripts/focus.js';
+import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, unref, watch } from 'vue';
 import MkSwitchButton from '@/components/MkSwitch.button.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { isTouchUsing } from '@/scripts/touch.js';
 import { type Keymap } from '@/scripts/hotkey.js';
+import { isFocusable } from '@/scripts/focus.js';
+import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
 
 const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
 </script>
@@ -108,7 +188,6 @@ const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
 const props = defineProps<{
 	items: MenuItem[];
-	viaKeyboard?: boolean;
 	asDrawer?: boolean;
 	align?: 'center' | string;
 	width?: number;
@@ -120,7 +199,9 @@ const emit = defineEmits<{
 	(ev: 'hide'): void;
 }>();
 
-const itemsEl = shallowRef<HTMLDivElement>();
+const isNestingMenu = inject<boolean>('isNestingMenu', false);
+
+const itemsEl = shallowRef<HTMLElement>();
 
 const items2 = ref<InnerMenuItem[]>();
 
@@ -177,25 +258,19 @@ function childActioned() {
 	close(true);
 }
 
-const onGlobalMousedown = (event: MouseEvent) => {
-	if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
-	if (child.value && child.value.checkHit(event)) return;
-	closeChild();
-};
-
 let childCloseTimer: null | number = null;
 
-function onItemMouseEnter(item) {
+function onItemMouseEnter() {
 	childCloseTimer = window.setTimeout(() => {
 		closeChild();
 	}, 300);
 }
 
-function onItemMouseLeave(item) {
+function onItemMouseLeave() {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
+async function showRadioOptions(item: MenuRadio, ev: Event) {
 	const children: MenuItem[] = Object.keys(item.options).map<MenuRadioOption>(key => {
 		const value = item.options[key];
 		return {
@@ -210,7 +285,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 
 	if (props.asDrawer) {
 		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
-			emit('close');
+			close(false);
 		});
 		emit('hide');
 	} else {
@@ -220,7 +295,7 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent) {
 	}
 }
 
-async function showChildren(item: MenuParent, ev: MouseEvent) {
+async function showChildren(item: MenuParent, ev: Event) {
 	const children: MenuItem[] = await (async () => {
 		if (childrenCache.has(item)) {
 			return childrenCache.get(item)!;
@@ -237,7 +312,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
 
 	if (props.asDrawer) {
 		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
-			emit('close');
+			close(false);
 		});
 		emit('hide');
 	} else {
@@ -256,41 +331,87 @@ function clicked(fn: MenuAction, ev: MouseEvent, doClose = true) {
 }
 
 function close(actioned = false) {
-	emit('close', actioned);
+	disposeHandlers();
+	nextTick(() => {
+		closeChild();
+		emit('close', actioned);
+	});
+}
+
+function switchItem(item: MenuSwitch & { ref: any }) {
+	if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return;
+	item.ref = !item.ref;
 }
 
 function focusUp() {
-	focusPrev(document.activeElement);
+	if (disposed) return;
+	if (!itemsEl.value?.contains(document.activeElement)) return;
+
+	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
+	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
+	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
+
+	targetElement.focus();
 }
 
 function focusDown() {
-	focusNext(document.activeElement);
-}
+	if (disposed) return;
+	if (!itemsEl.value?.contains(document.activeElement)) return;
 
-function switchItem(item: MenuSwitch & { ref: any }) {
-	if (item.disabled !== undefined && (typeof item.disabled === 'boolean' ? item.disabled : item.disabled.value)) return;
-	item.ref = !item.ref;
-}
+	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
+	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
+	const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
+	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
 
-function getValue<T>(item?: ComputedRef<T> | T) {
-	return isRef(item) ? item.value : item;
+	targetElement.focus();
 }
 
-onMounted(() => {
-	if (props.viaKeyboard) {
-		nextTick(() => {
-			if (itemsEl.value) focusNext(itemsEl.value.children[0], true, false);
-		});
-	}
+const onGlobalFocusin = (ev: FocusEvent) => {
+	if (disposed) return;
+	if (itemsEl.value?.parentElement?.contains(getNodeOrNull(ev.target))) return;
+	nextTick(() => {
+		if (itemsEl.value != null && isFocusable(itemsEl.value)) {
+			itemsEl.value.focus({ preventScroll: true });
+			nextTick(() => focusDown());
+		}
+	});
+};
 
-	// TODO: アクティブな要素までスクロール
-	//itemsEl.scrollTo();
+const onGlobalMousedown = (ev: MouseEvent) => {
+	if (disposed) return;
+	if (childTarget.value?.contains(getNodeOrNull(ev.target))) return;
+	if (child.value?.checkHit(ev)) return;
+	closeChild();
+};
 
+const setupHandlers = () => {
+	if (!isNestingMenu) {
+		document.addEventListener('focusin', onGlobalFocusin, { passive: true });
+	}
 	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
+};
+
+let disposed = false;
+
+const disposeHandlers = () => {
+	disposed = true;
+	if (!isNestingMenu) {
+		document.removeEventListener('focusin', onGlobalFocusin);
+	}
+	document.removeEventListener('mousedown', onGlobalMousedown);
+};
+
+onMounted(() => {
+	setupHandlers();
+
+	if (!isNestingMenu) {
+		nextTick(() => itemsEl.value?.focus({ preventScroll: true }));
+	}
 });
 
 onBeforeUnmount(() => {
-	document.removeEventListener('mousedown', onGlobalMousedown);
+	disposeHandlers();
 });
 </script>
 
@@ -303,6 +424,10 @@ onBeforeUnmount(() => {
 	overflow: auto;
 	overscroll-behavior: contain;
 
+	&:focus-visible {
+		outline: none;
+	}
+
 	&.center {
 		> .item {
 			text-align: center;
@@ -320,7 +445,7 @@ onBeforeUnmount(() => {
 			font-size: 1em;
 			padding: 12px 24px;
 
-			&:before {
+			&::before {
 				width: calc(100% - 24px);
 				border-radius: 12px;
 			}
@@ -350,8 +475,10 @@ onBeforeUnmount(() => {
 	text-align: left;
 	overflow: hidden;
 	text-overflow: ellipsis;
+	text-decoration: none !important;
+	color: var(--menuFg, var(--fg));
 
-	&:before {
+	&::before {
 		content: "";
 		display: block;
 		position: absolute;
@@ -365,56 +492,56 @@ onBeforeUnmount(() => {
 		border-radius: 6px;
 	}
 
-	&:not(:disabled):hover {
-		color: var(--accent);
-		text-decoration: none;
+	&:focus-visible {
+		outline: none;
 
-		&:before {
-			background: var(--accentedBg);
+		&:not(:hover):not(:active)::before {
+			outline: var(--focus) solid 2px;
+			outline-offset: -2px;
 		}
 	}
 
-	&.danger {
-		color: #ff2a2a;
-
-		&:hover {
-			color: #fff;
+	&:not(:disabled) {
+		&:hover,
+		&:focus-visible:active,
+		&:focus-visible.active {
+			color: var(--menuHoverFg, var(--accent));
 
-			&:before {
-				background: #ff4242;
+			&::before {
+				background-color: var(--menuHoverBg, var(--accentedBg));
 			}
 		}
 
-		&:active {
-			color: #fff;
+		&:not(:focus-visible):active,
+		&:not(:focus-visible).active {
+			color: var(--menuActiveFg, var(--fgOnAccent));
 
-			&:before {
-				background: #d42e2e !important;
+			&::before {
+				background-color: var(--menuActiveBg, var(--accent));
 			}
 		}
 	}
 
-	&:active,
-	&.active {
-		color: var(--fgOnAccent) !important;
-		opacity: 1;
-
-		&:before {
-			background: var(--accent) !important;
-		}
+	&:disabled {
+		cursor: not-allowed;
 	}
 
-	&.radioActive {
-		color: var(--accent) !important;
-		opacity: 1;
+	&.danger {
+		--menuFg: #ff2a2a;
+		--menuHoverFg: #fff;
+		--menuHoverBg: #ff4242;
+		--menuActiveFg: #fff;
+		--menuActiveBg: #d42e2e;
+	}
 
-		&:before {
-			background-color: var(--accentedBg) !important;
-		}
+	&.radio {
+		--menuActiveFg: var(--accent);
+		--menuActiveBg: var(--accentedBg);
 	}
 
-	&:not(:active):focus-visible {
-		box-shadow: 0 0 0 2px var(--focus) inset;
+	&.parent {
+		--menuActiveFg: var(--accent);
+		--menuActiveBg: var(--accentedBg);
 	}
 
 	&.label {
@@ -432,22 +559,6 @@ onBeforeUnmount(() => {
 		pointer-events: none;
 		opacity: 0.7;
 	}
-
-	&.parent {
-		pointer-events: auto;
-		display: flex;
-		align-items: center;
-		cursor: default;
-
-		&.childShowing {
-			color: var(--accent);
-			text-decoration: none;
-
-			&:before {
-				background: var(--accentedBg);
-			}
-		}
-	}
 }
 
 .item_content {
@@ -466,18 +577,6 @@ onBeforeUnmount(() => {
 	overflow: hidden;
 }
 
-.switch {
-	position: relative;
-	display: flex;
-	transition: all 0.2s ease;
-	user-select: none;
-	cursor: pointer;
-}
-
-.switchDisabled {
-	cursor: not-allowed;
-}
-
 .switchButton {
 	margin-left: -2px;
 	--height: 1.35em;
@@ -489,14 +588,6 @@ onBeforeUnmount(() => {
 	text-overflow: ellipsis;
 }
 
-.switchInput {
-	position: absolute;
-	width: 0;
-	height: 0;
-	opacity: 0;
-	margin: 0;
-}
-
 .icon {
 	margin-right: 8px;
 	line-height: 1;
@@ -525,12 +616,12 @@ onBeforeUnmount(() => {
 	border-top: solid 0.5px var(--divider);
 }
 
-.radio {
+.radioIcon {
 	display: inline-block;
 	position: relative;
 	width: 1em;
 	height: 1em;
-	vertical-align: -.125em;
+	vertical-align: -0.125em;
 	border-radius: 50%;
 	border: solid 2px var(--divider);
 	background-color: var(--panel);
-- 
cgit v1.2.3-freya

