Skip to content

Commit 953c3a0

Browse files
committed
fix アプローチ変えてみる
1 parent 787cb5a commit 953c3a0

3 files changed

Lines changed: 61 additions & 81 deletions

File tree

frontend/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@
6464
"chartjs-adapter-date-fns": "^3.0.0",
6565
"date-fns": "^4.1.0",
6666
"redis": "^5.12.1",
67-
"svelte-i18n": "^4.0.1",
68-
"web-haptics": "^0.0.6"
67+
"svelte-i18n": "^4.0.1"
6968
}
7069
}

frontend/pnpm-lock.yaml

Lines changed: 0 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/lib/utils/haptic.ts

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,83 @@
11
/**
22
* 触覚フィードバック(バイブレーション)ユーティリティ。
3-
* web-haptics ライブラリを使用して iOS / Android 両対応の haptic を提供する。
43
*
5-
* iOS (Safari 17.4+):
6-
* web-haptics が内部で input[type=checkbox][switch] + label を DOM に作成し、
7-
* label.click() で Taptic Engine を起動する。
4+
* iOS Safari (17.4+):
5+
* input[type=checkbox][switch] + label.click() を使用して Taptic Engine を起動する。
86
*
97
* 重要な制約:
10-
* - web-haptics はデフォルトで要素を display:none にするが、iOS では
11-
* display:none の要素から発火した click() では Taptic Engine が動作しない。
12-
* - そのため attachHapticToButton の初回呼び出し時に DOM 要素を事前生成し、
13-
* display:none → 画面外配置 (position:fixed left:-9999px) に変更する。
14-
* - trigger() の同期部分はユーザージェスチャーコンテキスト内で実行されるため、
15-
* pointerdown ハンドラから void trigger() で呼び出すことで iOS でも動作する。
8+
* - iOS では async 関数内から label.click() を呼び出すと、await より前でも
9+
* ユーザージェスチャーコンテキストが失われる場合がある。
10+
* - そのため label.click() は pointerdown ハンドラから直接・同期的に呼び出す。
11+
* - display:none の要素では Taptic Engine が動作しないため、
12+
* position:fixed で画面外に配置して描画ツリーに残す。
1613
*
1714
* Android 等:
18-
* Vibration API (navigator.vibrate) を使用する。DOM 要素は作成されない。
15+
* Vibration API (navigator.vibrate) を使用する。
1916
*/
2017

21-
import { WebHaptics } from "web-haptics";
22-
23-
// シングルトンインスタンス(DOM 要素はインスタンス内で管理される)
24-
let haptics: WebHaptics | null = null;
25-
// DOM 事前初期化済みフラグ(複数コンポーネントから呼ばれても一度だけ実行)
26-
let preInitDone = false;
27-
28-
function getHaptics(): WebHaptics {
29-
if (!haptics) {
30-
haptics = new WebHaptics();
31-
}
32-
return haptics;
33-
}
18+
// iOS向け label 要素(シングルトン)
19+
let iosLabel: HTMLLabelElement | null = null;
20+
let domInitialized = false;
3421

3522
/**
36-
* web-haptics が作成した DOM 要素の display:none を画面外配置に変更する。
37-
* iOS では display:none の要素への click() では Taptic Engine が動作しないため。
23+
* iOS向け DOM 要素を初期化する。
24+
* display:none を使わず position:fixed で画面外に配置して描画ツリーに残す。
25+
* ユーザージェスチャーの前に呼び出してよい(DOM 生成のみ、haptic は発火しない)。
3826
*/
39-
function fixHapticsElementStyles(): void {
40-
if (typeof document === "undefined") return;
41-
const label = document.querySelector('label[for^="web-haptics"]');
42-
if (!label) return; // Android は Vibration API を使用するため DOM 要素なし
43-
// display:none → 画面外に配置(描画ツリーに残したまま非表示にする)
44-
(label as HTMLElement).style.cssText =
45-
"position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;";
46-
const input = label.querySelector("input");
47-
if (input) {
48-
// all:initial でリセット後に switch 要素として描画させる
49-
(input as HTMLInputElement).style.cssText =
50-
"all:initial;appearance:auto;display:block;";
27+
function initDOM(): void {
28+
if (domInitialized || typeof document === "undefined") return;
29+
domInitialized = true;
30+
31+
// Android / Chrome 系は Vibration API を使用するため DOM 要素不要
32+
if (
33+
typeof navigator !== "undefined" &&
34+
typeof navigator.vibrate === "function"
35+
) {
36+
return;
5137
}
38+
39+
const label = document.createElement("label");
40+
const input = document.createElement("input");
41+
input.type = "checkbox";
42+
input.setAttribute("switch", "");
43+
input.id = "haptic-switch";
44+
label.setAttribute("for", "haptic-switch");
45+
46+
// 画面外に配置(display:none は iOS で haptic が動作しないため使用しない)
47+
label.style.cssText =
48+
"position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;pointer-events:none;";
49+
50+
label.appendChild(input);
51+
document.body.appendChild(label);
52+
iosLabel = label;
5253
}
5354

5455
/**
55-
* DOM 要素を事前初期化する。
56-
* trigger() を非ジェスチャーコンテキストで呼び出すことで iOS 向け DOM 要素を生成し、
57-
* その後 display:none を画面外配置に変更する。
58-
* (非ジェスチャーなので haptic は発火しないが DOM だけ作られる)
56+
* ユーザージェスチャーコンテキスト内で同期的に haptic を発火する。
57+
* 必ず同期的なイベントハンドラ(pointerdown 等)から直接呼び出すこと。
58+
* async 関数内から呼び出すと iOS で gesture context が失われる。
5959
*/
60-
function preInitHaptics(): void {
61-
if (preInitDone || typeof document === "undefined") return;
62-
preInitDone = true;
63-
// trigger() の同期部分が実行されることで iOS 向け DOM 要素が生成される
64-
void getHaptics().trigger("light");
65-
// trigger() の同期部分完了後(await 前)に DOM 要素が存在するのでスタイルを修正
66-
fixHapticsElementStyles();
60+
function triggerHaptic(): void {
61+
if (
62+
typeof navigator !== "undefined" &&
63+
typeof navigator.vibrate === "function"
64+
) {
65+
// Android / Chrome: Vibration API
66+
navigator.vibrate(25);
67+
return;
68+
}
69+
// iOS Safari: label.click() でTaptic Engineを起動
70+
iosLabel?.click();
6771
}
6872

6973
/**
70-
* ボタン要素に pointerdown ハンドラを付与し、ユーザージェスチャーの中で振動させる
74+
* ボタン要素に pointerdown ハンドラを付与し、haptic を発火する
7175
* iOS Safari ではユーザージェスチャーコンテキストが必要なため、
72-
* 非同期の成功コールバックではなくボタン押下時点で発火する
76+
* pointerdown で同期的に発火する
7377
*/
7478
export function attachHapticToButton(button: HTMLElement): () => void {
75-
// 初回呼び出し時に iOS 向け DOM 要素を事前初期化してスタイルを修正する
76-
preInitHaptics();
79+
// iOS向け DOM 要素を事前生成(ユーザージェスチャーの前に実行してOK)
80+
initDOM();
7781

7882
function handler() {
7983
// div ラッパーに attach した場合も含め、内包ボタンが disabled なら無視する
@@ -82,9 +86,10 @@ export function attachHapticToButton(button: HTMLElement): () => void {
8286
? (button as HTMLButtonElement)
8387
: button.querySelector("button");
8488
if (btn?.disabled) return;
85-
// void で Promise を明示的に破棄(同期部分のみジェスチャーコンテキストで実行される
86-
void getHaptics().trigger("medium");
89+
// 同期的に haptic を発火(async 経由にしないことで iOS の gesture context を保持
90+
triggerHaptic();
8791
}
92+
8893
button.addEventListener("pointerdown", handler);
8994
// クリーンアップ関数を返す
9095
return () => button.removeEventListener("pointerdown", handler);

0 commit comments

Comments
 (0)