Show the seating editor as a centered popup
Add Fluid.Popup, a centered dialog with the same open/close API as Sheet: it scales in like the confirm alert, fades out in place, and closes on backdrop tap or Escape with focus trapped inside. The host's seating editor now uses it instead of a bottom sheet; its list scrolls inside the popup on short screens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd39c043c4
commit
ed3d1e437e
4 files changed
+132
-13
No files matched your search
@@ -4,6 +4,7 @@
|
|||||||
// 画面上のいまの位置と速度から続くので、動いている最中につかんだり戻したりできる
|
// 画面上のいまの位置と速度から続くので、動いている最中につかんだり戻したりできる
|
||||||
// - VelocityTracker / project / rubberband: 指の速度・勢いから止まる位置の先読み・端での抵抗
|
// - VelocityTracker / project / rubberband: 指の速度・勢いから止まる位置の先読み・端での抵抗
|
||||||
// - Sheet: 下から出るシート(つまみを下へ払うと閉じる)
|
// - Sheet: 下から出るシート(つまみを下へ払うと閉じる)
|
||||||
|
// - Popup: 画面の中央に出るポップアップ(背景を押す・Esc で閉じる)
|
||||||
// - Segmented: つまみが滑るセグメント(つまみをドラッグしても選べる)
|
// - Segmented: つまみが滑るセグメント(つまみをドラッグしても選べる)
|
||||||
// - presence / countTo / shake / haptic / navBar: 出し入れ・数字の変化・揺れ・振動・ナビゲーションバー
|
// - presence / countTo / shake / haptic / navBar: 出し入れ・数字の変化・揺れ・振動・ナビゲーションバー
|
||||||
// CSP でインラインスタイルを禁止しているので、動きはすべて CSSOM(el.style)で付ける
|
// CSP でインラインスタイルを禁止しているので、動きはすべて CSSOM(el.style)で付ける
|
||||||
@@ -304,6 +305,86 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- ポップアップ ----------
|
||||||
|
// root は背景(.popup-backdrop)、中に .popup。画面の中央に少し大きい所から縮んで現れ、閉じるときはその場で薄くなる
|
||||||
|
// (App.confirm のアラートと同じ動き)。背景を押す・Esc で閉じる。
|
||||||
|
// opts: { focus(開いたときにフォーカスする要素のセレクタ), onClose, onClosed }
|
||||||
|
function Popup(root, opts = {}) {
|
||||||
|
const box = root.querySelector('.popup');
|
||||||
|
const appear = new Spring(0, { precision: 0.002 });
|
||||||
|
let isOpen = false;
|
||||||
|
let closing = false;
|
||||||
|
let prevFocus = null;
|
||||||
|
let downOnBackdrop = false;
|
||||||
|
|
||||||
|
appear.onUpdate = (v) => {
|
||||||
|
root.style.setProperty('--appear', v.toFixed(3));
|
||||||
|
box.style.opacity = v >= 1 ? '' : v.toFixed(3);
|
||||||
|
box.style.transform = closing || v >= 1 || reducedMotion() ? '' : `scale(${(1.08 - 0.08 * v).toFixed(4)})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
close();
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
trapFocus(box, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
if (isOpen) return;
|
||||||
|
isOpen = true;
|
||||||
|
closing = false;
|
||||||
|
if (root.hidden) {
|
||||||
|
root.hidden = false;
|
||||||
|
appear.set(0);
|
||||||
|
}
|
||||||
|
root.style.pointerEvents = '';
|
||||||
|
appear.to(1, { damping: 1, response: 0.32, allowReduced: true });
|
||||||
|
prevFocus = document.activeElement;
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
const target = opts.focus ? box.querySelector(opts.focus) : null;
|
||||||
|
(target || box).focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (!isOpen) return;
|
||||||
|
isOpen = false;
|
||||||
|
closing = true;
|
||||||
|
document.removeEventListener('keydown', onKey);
|
||||||
|
root.style.pointerEvents = 'none'; // 消えている途中の背景で次の操作を止めない
|
||||||
|
appear.to(0, {
|
||||||
|
damping: 1,
|
||||||
|
response: 0.2,
|
||||||
|
allowReduced: true,
|
||||||
|
onRest: () => {
|
||||||
|
if (isOpen) return;
|
||||||
|
root.hidden = true;
|
||||||
|
if (opts.onClosed) opts.onClosed();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (prevFocus && prevFocus.focus && document.contains(prevFocus)) prevFocus.focus({ preventScroll: true });
|
||||||
|
if (opts.onClose) opts.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景を押したら閉じる(ポップアップの中から背景へのドラッグは除く)
|
||||||
|
root.addEventListener('pointerdown', (e) => {
|
||||||
|
downOnBackdrop = e.target === root;
|
||||||
|
});
|
||||||
|
root.addEventListener('click', (e) => {
|
||||||
|
if (e.target === root && downOnBackdrop) close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
get isOpen() {
|
||||||
|
return isOpen;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- セグメント ----------
|
// ---------- セグメント ----------
|
||||||
// container の子(label > input[type=radio] か、.active の付いた button)を選ぶつまみを滑らせる。
|
// container の子(label > input[type=radio] か、.active の付いた button)を選ぶつまみを滑らせる。
|
||||||
// 選ばれている所を押すとつまみが少し縮み、そのまま横へドラッグすると指に付いてくる。
|
// 選ばれている所を押すとつまみが少し縮み、そのまま横へドラッグすると指に付いてくる。
|
||||||
@@ -548,6 +629,7 @@
|
|||||||
clamp,
|
clamp,
|
||||||
reducedMotion,
|
reducedMotion,
|
||||||
Sheet,
|
Sheet,
|
||||||
|
Popup,
|
||||||
Segmented,
|
Segmented,
|
||||||
presence,
|
presence,
|
||||||
countTo,
|
countTo,
|
||||||
|
|||||||
@@ -1026,6 +1026,44 @@ a {
|
|||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.015em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- ポップアップ(fluid.js の Popup): 画面の中央に出る大きめのダイアログ ---------- */
|
||||||
|
.popup-backdrop {
|
||||||
|
--appear: 1;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 30;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: max(16px, env(safe-area-inset-top)) 16px max(16px, env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
/* 暗さは現れ方に合わせて変わる */
|
||||||
|
.popup-backdrop::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--scrim);
|
||||||
|
opacity: var(--appear);
|
||||||
|
}
|
||||||
|
.popup {
|
||||||
|
--bg-2: var(--sheet-cell);
|
||||||
|
position: relative;
|
||||||
|
width: min(440px, 100%);
|
||||||
|
max-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: var(--sheet-bg);
|
||||||
|
box-shadow: var(--shadow-l), inset 0 0 0 0.5px var(--material-edge);
|
||||||
|
outline: none;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
.popup-header {
|
||||||
|
flex: none;
|
||||||
|
padding: 16px 20px 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- アラート(App.confirm): 少し大きい所から現れ、その場で消える ---------- */
|
/* ---------- アラート(App.confirm): 少し大きい所から現れ、その場で消える ---------- */
|
||||||
.alert-backdrop {
|
.alert-backdrop {
|
||||||
--appear: 1;
|
--appear: 1;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
const reviewSheet = Fluid.Sheet($('review-modal'));
|
const reviewSheet = Fluid.Sheet($('review-modal'));
|
||||||
const rankingSheet = Fluid.Sheet($('ranking-modal'));
|
const rankingSheet = Fluid.Sheet($('ranking-modal'));
|
||||||
const seatingSheet = Fluid.Sheet($('seating-modal'));
|
const seatingPopup = Fluid.Popup($('seating-modal'));
|
||||||
const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' });
|
const nameSheet = Fluid.Sheet($('name-modal'), { dismissible: false, focus: '#name-input' });
|
||||||
// 払って閉じた・背景を押した・Esc のときも「戻る」と同じ
|
// 払って閉じた・背景を押した・Esc のときも「戻る」と同じ
|
||||||
const betSheet = Fluid.Sheet($('bet-modal'), {
|
const betSheet = Fluid.Sheet($('bet-modal'), {
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
$('action-bar').hidden = true;
|
$('action-bar').hidden = true;
|
||||||
reviewSheet.close();
|
reviewSheet.close();
|
||||||
rankingSheet.close();
|
rankingSheet.close();
|
||||||
seatingSheet.close();
|
seatingPopup.close();
|
||||||
raising = false;
|
raising = false;
|
||||||
betSheet.close();
|
betSheet.close();
|
||||||
$('not-found').hidden = false;
|
$('not-found').hidden = false;
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
else betSheet.close();
|
else betSheet.close();
|
||||||
renderReview(s);
|
renderReview(s);
|
||||||
if (rankingSheet.isOpen) renderRanking(s);
|
if (rankingSheet.isOpen) renderRanking(s);
|
||||||
if (seatingSheet.isOpen) syncSeating(s);
|
if (seatingPopup.isOpen) syncSeating(s);
|
||||||
renderTimer();
|
renderTimer();
|
||||||
|
|
||||||
// 自分の番になったら知らせる
|
// 自分の番になったら知らせる
|
||||||
@@ -774,7 +774,7 @@
|
|||||||
// 開いている間に座った人は最後に加え、いなくなった人は外す(並べた順は崩さない)。
|
// 開いている間に座った人は最後に加え、いなくなった人は外す(並べた順は崩さない)。
|
||||||
// 次のハンドで席番号が振り直されたら読み込み直す
|
// 次のハンドで席番号が振り直されたら読み込み直す
|
||||||
function syncSeating(s) {
|
function syncSeating(s) {
|
||||||
if (!s.isHost) return seatingSheet.close();
|
if (!s.isHost) return seatingPopup.close();
|
||||||
if (seatDrag) return;
|
if (seatDrag) return;
|
||||||
const occupied = occupiedSeats(s);
|
const occupied = occupiedSeats(s);
|
||||||
const renumbered =
|
const renumbered =
|
||||||
@@ -962,10 +962,10 @@
|
|||||||
$('seating-open').addEventListener('click', () => {
|
$('seating-open').addEventListener('click', () => {
|
||||||
if (!state || !state.isHost) return;
|
if (!state || !state.isHost) return;
|
||||||
loadSeating(state);
|
loadSeating(state);
|
||||||
renderSeating(null); // 中身を入れてから開く(シートの高さを測るため)
|
renderSeating(null);
|
||||||
seatingSheet.open();
|
seatingPopup.open();
|
||||||
});
|
});
|
||||||
$('seating-cancel').addEventListener('click', () => seatingSheet.close());
|
$('seating-cancel').addEventListener('click', () => seatingPopup.close());
|
||||||
$('seating-save').addEventListener('click', () => {
|
$('seating-save').addEventListener('click', () => {
|
||||||
if (!socket || !state) return;
|
if (!socket || !state) return;
|
||||||
const later = Boolean(state.hand); // ハンドがある間(結果の表示中を含む)は次のハンドから
|
const later = Boolean(state.hand); // ハンドがある間(結果の表示中を含む)は次のハンドから
|
||||||
@@ -978,7 +978,7 @@
|
|||||||
if (state) syncSeating(state);
|
if (state) syncSeating(state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
seatingSheet.close();
|
seatingPopup.close();
|
||||||
toast(later ? '次のハンドから新しい席順になります' : '席順を変更しました');
|
toast(later ? '次のハンドから新しい席順になります' : '席順を変更しました');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-5
@@ -157,11 +157,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 席順の変更(ホストのみ)。自分から時計回りの並びを、行をつかんで並べ替える -->
|
<!-- 席順の変更(ホストのみ・画面の中央に出るポップアップ)。自分から時計回りの並びを、行をつかんで並べ替える -->
|
||||||
<div class="sheet-backdrop" id="seating-modal" hidden>
|
<div class="popup-backdrop" id="seating-modal" hidden>
|
||||||
<div class="sheet seating-sheet" role="dialog" aria-modal="true" aria-labelledby="seating-title" tabindex="-1">
|
<div class="popup seating-popup" role="dialog" aria-modal="true" aria-labelledby="seating-title" tabindex="-1">
|
||||||
<div class="sheet-header" data-sheet-drag>
|
<div class="popup-header">
|
||||||
<span class="sheet-grabber" aria-hidden="true"></span>
|
|
||||||
<h2 class="sheet-title" id="seating-title">席順を変更</h2>
|
<h2 class="sheet-title" id="seating-title">席順を変更</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="sheet-scroll">
|
<div class="sheet-scroll">
|
||||||
|
|||||||
Reference in new issue
Block a user