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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Shows a toast with the created session name, calls pollSessions() to refresh,
|
||||
@@ -1357,7 +1402,7 @@ function bindStaticEventListeners() {
|
||||
var sidebarNewSessionBtn = $('sidebar-new-session-btn');
|
||||
if (sidebarNewSessionBtn) on(sidebarNewSessionBtn, 'click', function() { showNewSessionInput(sidebarNewSessionBtn); });
|
||||
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-collapse-btn'), 'click', toggleSidebar);
|
||||
bindSidebarClickAway();
|
||||
@@ -1583,6 +1628,7 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
patchServerSetting,
|
||||
// Header + button with inline name input
|
||||
showNewSessionInput,
|
||||
showFabSessionInput,
|
||||
createNewSession,
|
||||
// Test-only helpers
|
||||
_setCurrentSessions,
|
||||
|
||||
@@ -821,6 +821,10 @@ body {
|
||||
.session-sidebar {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.new-session-fab:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -1222,13 +1226,27 @@ body {
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
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 {
|
||||
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
|
||||
============================================================ */
|
||||
|
||||
@@ -1858,3 +1858,73 @@ def test_js_fab_exported() -> None:
|
||||
assert "showNewSessionInput" in _JS, (
|
||||
"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