Skip to content

Commit ddb8825

Browse files
committed
メニューをタブと矢印キーで操作できるようにした
1 parent b6872da commit ddb8825

File tree

5 files changed

+127
-15
lines changed

5 files changed

+127
-15
lines changed

routes/stage-select/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ onMount(() => {
138138
<button
139139
type="button"
140140
class={`appearance-none focus:outline-none bg-white border-6 pt-8 pb-6 pl-8 pr-6 transition-colors duration-200 text-7xl cursor-pointer ${
141-
selected === i ? 'border-red-500 ring ring-red-500' : 'border-base'
141+
selected === i ? 'border-red-500 ring ring-red-500 bg-amber-100!' : 'border-base'
142142
}`}
143143
on:click={() => select(i)}
144144
>

src/ui-components/menu/GameOverMenu.svelte

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import Key from "../Key.svelte";
33
import "./menu.css";
4+
import { onMount } from "svelte";
45
56
type Props = {
67
gameover: boolean;
@@ -9,15 +10,47 @@ type Props = {
910
};
1011
const { gameover, reset, stageNum }: Props = $props();
1112
let el: HTMLDialogElement;
13+
let btn1: HTMLElement;
14+
let btn2: HTMLElement;
1215
$effect(() => {
1316
if (gameover) {
1417
el.showModal();
18+
setTimeout(() => {
19+
btn1.focus();
20+
}, 100);
1521
} else {
1622
if (el.open) el.close();
1723
}
1824
});
19-
document.addEventListener("keydown", (ev) => {
20-
if (ev.key === "Escape") ev.preventDefault();
25+
function handleKeyDown(e: KeyboardEvent) {
26+
const buttons = [btn1, btn2];
27+
const currentIndex = buttons.indexOf(document.activeElement as HTMLElement);
28+
if (gameover) {
29+
if (e.key === "ArrowUp") {
30+
if (currentIndex > 0) {
31+
buttons[currentIndex - 1].focus();
32+
} else {
33+
buttons[buttons.length - 1].focus();
34+
}
35+
e.preventDefault();
36+
} else if (e.key === "ArrowDown") {
37+
if (currentIndex < buttons.length - 1) {
38+
buttons[currentIndex + 1].focus();
39+
} else {
40+
buttons[0].focus();
41+
}
42+
e.preventDefault();
43+
}
44+
}
45+
if (e.key === "Escape") {
46+
e.preventDefault();
47+
}
48+
}
49+
$effect(() => {
50+
document.addEventListener("keydown", handleKeyDown);
51+
return () => {
52+
document.removeEventListener("keydown", handleKeyDown);
53+
};
2154
});
2255
</script>
2356

@@ -34,9 +67,11 @@ document.addEventListener("keydown", (ev) => {
3467
el.close();
3568
reset();
3669
}}
70+
bind:this={btn1}
71+
tabindex="1"
3772
>
3873
Restart
3974
</button>
40-
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}"> Back to Stage Select </a>
75+
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}" bind:this={btn2} tabindex="2"> Back to Stage Select </a>
4176
</div>
4277
</dialog>

src/ui-components/menu/GoalMenu.svelte

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { onDestroy } from "svelte";
33
import "./menu.css";
44
import { invalidate } from "$app/navigation";
5+
import { onMount } from "svelte";
56
67
type Props = {
78
goaled: boolean;
@@ -11,15 +12,49 @@ type Props = {
1112
};
1213
const { goaled, nextStage, reset, stageNum }: Props = $props();
1314
let el: HTMLDialogElement;
15+
let btn1: HTMLElement;
16+
let btn2: HTMLElement;
17+
let btn3: HTMLElement;
1418
$effect(() => {
1519
if (goaled) {
1620
el.showModal();
21+
setTimeout(() => {
22+
btn1.focus();
23+
}, 100);
1724
} else {
1825
if (el.open) el.close();
1926
}
2027
});
21-
document.addEventListener("keydown", (ev) => {
22-
if (ev.key === "Escape") ev.preventDefault();
28+
29+
function handleKeyDown(e: KeyboardEvent) {
30+
const buttons = [btn1, btn2, btn3];
31+
const currentIndex = buttons.indexOf(document.activeElement as HTMLElement);
32+
if (goaled) {
33+
if (e.key === "ArrowUp") {
34+
if (currentIndex > 0) {
35+
buttons[currentIndex - 1].focus();
36+
} else {
37+
buttons[buttons.length - 1].focus();
38+
}
39+
e.preventDefault();
40+
} else if (e.key === "ArrowDown") {
41+
if (currentIndex < buttons.length - 1) {
42+
buttons[currentIndex + 1].focus();
43+
} else {
44+
buttons[0].focus();
45+
}
46+
e.preventDefault();
47+
}
48+
}
49+
if (e.key === "Escape") {
50+
e.preventDefault();
51+
}
52+
}
53+
$effect(() => {
54+
document.addEventListener("keydown", handleKeyDown);
55+
return () => {
56+
document.removeEventListener("keydown", handleKeyDown);
57+
};
2358
});
2459
2560
const nextStageUrl = $derived(`/game?stage=${nextStage}`);
@@ -32,16 +67,18 @@ onDestroy(() => {
3267
<dialog bind:this={el} class="modal">
3368
<div class="modal-box flex flex-col gap-1">
3469
<h1 class="text-center my-6">Goal!</h1>
35-
<a class="btn modal-btn" href={nextStageUrl}> Next Stage </a>
70+
<a class="btn modal-btn" href={nextStageUrl} bind:this={btn1} tabindex="1"> Next Stage </a>
3671
<button
3772
class="btn modal-btn"
3873
onclick={() => {
3974
el.close();
4075
reset();
4176
}}
77+
bind:this={btn2}
78+
tabindex="2"
4279
>
4380
Restart
4481
</button>
45-
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}"> Back to Stage Select </a>
82+
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}" bind:this={btn3} tabindex="3" > Back to Stage Select </a>
4683
</div>
4784
</dialog>
Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script lang="ts">
22
import "./menu.css";
3+
import { onMount } from "svelte";
34
type Props = {
45
paused: boolean;
56
alreadyStopped: boolean;
@@ -10,30 +11,63 @@ type Props = {
1011
};
1112
let { paused, alreadyStopped, onresume, onpause, onreset, stageNum }: Props = $props();
1213
let el: HTMLDialogElement;
14+
let btn1: HTMLElement;
15+
let btn2: HTMLElement;
16+
let btn3: HTMLElement;
17+
1318
$effect(() => {
1419
if (paused && !alreadyStopped) {
1520
el.showModal();
21+
setTimeout(() => {
22+
btn1.focus();
23+
}, 100);
1624
} else {
1725
el.close();
1826
}
1927
});
2028
21-
document.addEventListener("keydown", (ev) => {
22-
if (ev.key === "Escape")
29+
function handleKeyDown(e: KeyboardEvent) {
30+
const buttons = [btn1, btn2, btn3];
31+
const currentIndex = buttons.indexOf(document.activeElement as HTMLElement);
32+
if (paused) {
33+
if (e.key === "ArrowUp") {
34+
if (currentIndex > 0) {
35+
buttons[currentIndex - 1].focus();
36+
} else {
37+
buttons[buttons.length - 1].focus();
38+
}
39+
e.preventDefault();
40+
} else if (e.key === "ArrowDown") {
41+
if (currentIndex < buttons.length - 1) {
42+
buttons[currentIndex + 1].focus();
43+
} else {
44+
buttons[0].focus();
45+
}
46+
e.preventDefault();
47+
}
48+
}
49+
if (e.key === "Escape") {
2350
if (!paused && !alreadyStopped) {
2451
onpause();
25-
ev.preventDefault();
52+
e.preventDefault();
2653
}
54+
}
55+
}
56+
$effect(() => {
57+
document.addEventListener("keydown", handleKeyDown);
58+
return () => {
59+
document.removeEventListener("keydown", handleKeyDown);
60+
};
2761
});
2862
</script>
2963

3064
<dialog bind:this={el} onclose={onresume} class="modal">
3165
<div class="modal-box flex flex-col gap-1">
3266
<h1 class="text-center my-6">Paused</h1>
3367
<form method="dialog" class="w-full">
34-
<button class="btn modal-btn" type="submit">Resume</button>
68+
<button class="btn modal-btn" type="submit" bind:this={btn1} tabindex="1">Resume</button>
3569
</form>
36-
<button class="btn modal-btn" onclick={() => onreset()}>Restart</button>
37-
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}"> Back to Stage Select </a>
70+
<button class="btn modal-btn" onclick={() => onreset()} bind:this={btn2} tabindex="2">Restart</button>
71+
<a class="btn modal-btn" href="/stage-select?w={stageNum.split("-")[0]}&s={stageNum.split("-")[1]}" bind:this={btn3} tabindex="3"> Back to Stage Select </a>
3872
</div>
3973
</dialog>

src/ui-components/menu/menu.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
font-size: inherit;
77
height: max-content;
88
}
9+
.modal-btn:focus-visible {
10+
outline-width: 6px;
11+
outline-style: solid;
12+
outline-color: oklch(63.7% 0.237 25.331); /* red-500 */
13+
background: oklch(96.2% 0.059 95.617); /* amber-100 */
14+
}
915
.modal-box {
10-
background: oklch(82.8% 0.189 84.429 / 70%);
16+
background: oklch(82.8% 0.189 84.429 / 70%); /* amber-400 */
1117
backdrop-filter: blur(2px);
1218
padding: 0.75rem 1rem;
1319
border-radius: 0.5rem;

0 commit comments

Comments
 (0)