fix: use fixed-position overlay for FAB session input (mobile visibility)
This commit is contained in:
+47
-1
@@ -1324,6 +1324,51 @@ function showNewSessionInput(btn) {
|
|||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a fixed-position input overlay for creating a new session from the mobile FAB.
|
||||||
|
* Unlike showNewSessionInput (which inserts inline into btn.parentNode), this renders
|
||||||
|
* a fixed-position overlay appended directly to document.body — ensuring it is always
|
||||||
|
* visible on mobile regardless of body/view overflow:hidden constraints.
|
||||||
|
*/
|
||||||
|
function showFabSessionInput() {
|
||||||
|
const fab = $('new-session-fab');
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'fab-input-overlay';
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.className = 'new-session-input';
|
||||||
|
input.placeholder = 'Session name\u2026';
|
||||||
|
input.autocomplete = 'off';
|
||||||
|
input.spellcheck = false;
|
||||||
|
|
||||||
|
overlay.appendChild(input);
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||||
|
if (fab) fab.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
const name = input.value.trim();
|
||||||
|
cleanup();
|
||||||
|
if (name) createNewSession(name);
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function() {
|
||||||
|
setTimeout(cleanup, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fab) fab.style.display = 'none';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new tmux session via POST /api/sessions.
|
* Create a new tmux session via POST /api/sessions.
|
||||||
* Shows a toast with the created session name, calls pollSessions() to refresh,
|
* Shows a toast with the created session name, calls pollSessions() to refresh,
|
||||||
@@ -1357,7 +1402,7 @@ function bindStaticEventListeners() {
|
|||||||
var sidebarNewSessionBtn = $('sidebar-new-session-btn');
|
var sidebarNewSessionBtn = $('sidebar-new-session-btn');
|
||||||
if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); });
|
if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); });
|
||||||
var newSessionFab = $('new-session-fab');
|
var newSessionFab = $('new-session-fab');
|
||||||
if (newSessionFab) on(newSessionFab, 'click', function() { showNewSessionInput(newSessionFab); });
|
if (newSessionFab) on(newSessionFab, 'click', showFabSessionInput);
|
||||||
on($('sidebar-toggle-btn'), 'click', toggleSidebar);
|
on($('sidebar-toggle-btn'), 'click', toggleSidebar);
|
||||||
on($('sidebar-collapse-btn'), 'click', toggleSidebar);
|
on($('sidebar-collapse-btn'), 'click', toggleSidebar);
|
||||||
bindSidebarClickAway();
|
bindSidebarClickAway();
|
||||||
@@ -1583,6 +1628,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
patchServerSetting,
|
patchServerSetting,
|
||||||
// Header + button with inline name input
|
// Header + button with inline name input
|
||||||
showNewSessionInput,
|
showNewSessionInput,
|
||||||
|
showFabSessionInput,
|
||||||
createNewSession,
|
createNewSession,
|
||||||
// Test-only helpers
|
// Test-only helpers
|
||||||
_setCurrentSessions,
|
_setCurrentSessions,
|
||||||
|
|||||||
@@ -821,6 +821,10 @@ body {
|
|||||||
.session-sidebar {
|
.session-sidebar {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-session-fab:active {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
@@ -1222,13 +1226,27 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
transition: transform 150ms ease, box-shadow 150ms ease;
|
transition: transform 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-session-fab:active {
|
.new-session-fab:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.new-session-fab:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed-position input overlay triggered by mobile FAB —
|
||||||
|
appended to body so position:fixed escapes overflow:hidden on views */
|
||||||
|
.fab-input-overlay {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
Responsive overlay sidebar at <960px
|
Responsive overlay sidebar at <960px
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
@@ -1858,3 +1858,73 @@ def test_js_fab_exported() -> None:
|
|||||||
assert "showNewSessionInput" in _JS, (
|
assert "showNewSessionInput" in _JS, (
|
||||||
"showNewSessionInput must exist in app.js (used by FAB click handler)"
|
"showNewSessionInput must exist in app.js (used by FAB click handler)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Mobile FAB — dedicated fixed-position input (quality fix)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_js_show_fab_session_input_exists() -> None:
|
||||||
|
"""showFabSessionInput function must exist in app.js (dedicated FAB input overlay)."""
|
||||||
|
assert "function showFabSessionInput" in _JS, (
|
||||||
|
"showFabSessionInput must be defined in app.js — "
|
||||||
|
"FAB needs a fixed-position overlay, not inline body insertion"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_js_show_fab_session_input_appends_to_body() -> None:
|
||||||
|
"""showFabSessionInput must append the overlay to document.body (fixed positioning)."""
|
||||||
|
match = re.search(
|
||||||
|
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "showFabSessionInput function not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "document.body" in body, (
|
||||||
|
"showFabSessionInput must append to document.body "
|
||||||
|
"(fixed-position overlays must be direct body children to escape overflow:hidden)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_js_show_fab_session_input_handles_enter() -> None:
|
||||||
|
"""showFabSessionInput must call createNewSession on Enter key."""
|
||||||
|
match = re.search(
|
||||||
|
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "showFabSessionInput function not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "Enter" in body, "showFabSessionInput must handle Enter key"
|
||||||
|
assert "createNewSession" in body, (
|
||||||
|
"showFabSessionInput must call createNewSession on Enter"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_js_show_fab_session_input_handles_escape() -> None:
|
||||||
|
"""showFabSessionInput must handle Escape key to cancel."""
|
||||||
|
match = re.search(
|
||||||
|
r"function showFabSessionInput\s*\(\s*\)\s*\{(.*?)(?=\nfunction |\nasync function |\n// )",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "showFabSessionInput function not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "Escape" in body, "showFabSessionInput must handle Escape key"
|
||||||
|
|
||||||
|
|
||||||
|
def test_js_fab_click_calls_show_fab_session_input() -> None:
|
||||||
|
"""bindStaticEventListeners must bind FAB click to showFabSessionInput (not inline body insert)."""
|
||||||
|
match = re.search(
|
||||||
|
r"function bindStaticEventListeners\s*\(\s*\)\s*\{(.*?)\n\}",
|
||||||
|
_JS,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
assert match, "bindStaticEventListeners function not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "showFabSessionInput" in body, (
|
||||||
|
"bindStaticEventListeners must call showFabSessionInput for FAB click — "
|
||||||
|
"the old showNewSessionInput(fab) inserts into body which is clipped by overflow:hidden"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user