fix: stop iOS zoom, compact model control, quieter composer, better search
iOS zoom (and the menu button scrolling away with it) - Safari zooms the whole page when a text field under 16px takes focus and never zooms back; while zoomed, fixed chrome sits outside the visual viewport, which is why the menu button had to be scrolled to. All 144 visible text controls were 13-15px. They are 16px on phones now. !important is deliberate: per-component rules like `.demo-field select` outrank any element selector, and a new component would slip through. - The composer set 15px AFTER its own mobile 16px rule, so at equal specificity the base rule won; the size is per breakpoint now. - Pinch-zoom is untouched: no user-scalable=no. Desktop unchanged (13/15px). Composer - "Ready" / "Looking up sources..." / "saved" repeated what the send button already shows by turning into Stop, and on a phone they wrapped under the model control. Kept in the DOM as live regions; nothing is drawn. - The model control shows the word "Model" (76px) instead of ids as long as "openrouter-gemini-3.1-flash-image-preview", and opens the full list on click. The select stays as the state holder, so choosing still saves through the same change event. Search - Nothing typed lists the 19 destinations only; sub-items appear once there is something to match. - Listing, matching and opening now read one attribute list instead of three that could drift, and bedside's emergency sections are searchable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
609305538a
commit
cf7a3161a8
7 changed files with 188 additions and 10 deletions
|
|
@ -95,8 +95,16 @@
|
|||
<span class="assistant-status" id="assistant-status"><span class="assistant-dot"></span><span id="assistant-status-text">Ready</span></span>
|
||||
</div>
|
||||
<div class="assistant-composer-right">
|
||||
<!-- The select stays as the state holder (it persists the
|
||||
choice and holds the allowed list); the button is its
|
||||
compact face, so the composer is not as wide as the
|
||||
longest model id. -->
|
||||
<div class="assistant-model-pill" id="assistant-model-pill" hidden>
|
||||
<select id="assistant-chat-model-select" class="assistant-model-control" aria-label="Chat model" hidden></select>
|
||||
<select id="assistant-chat-model-select" class="assistant-model-control assistant-model-state" aria-label="Chat model" tabindex="-1" aria-hidden="true"></select>
|
||||
<button type="button" id="btn-assistant-model" class="assistant-model-btn" aria-haspopup="listbox" aria-expanded="false" aria-controls="assistant-model-menu" title="Choose the chat model">
|
||||
<i class="fas fa-microchip"></i> <span>Model</span>
|
||||
</button>
|
||||
<div id="assistant-model-menu" class="assistant-model-menu" role="listbox" aria-label="Chat model" hidden></div>
|
||||
</div>
|
||||
<button id="btn-assistant-voice" class="assistant-voice-btn" type="button" title="Voice conversation"><i class="fas fa-headset"></i></button>
|
||||
<button id="btn-assistant-mic" class="assistant-mic" type="button" title="Dictate your question"><i class="fas fa-microphone"></i></button>
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@
|
|||
.assistant-mobile-menu { display:inline-flex; }
|
||||
#btn-assistant-goback span, #btn-assistant-goback { font-size:0; }
|
||||
#btn-assistant-goback { padding:8px; }
|
||||
.assistant-status { margin-left:auto; font-size:11px; }
|
||||
.assistant-status, .assistant-autosave-state { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
|
||||
/* The assistant fills the viewport exactly; nothing behind can scroll */
|
||||
.assistant-layout { display:flex; flex-direction:column; height:calc(100dvh - var(--mobile-bar, 0px)); min-height:0; overflow:hidden; }
|
||||
.assistant-main { flex:1 1 auto; min-height:0; display:flex; }
|
||||
|
|
@ -361,7 +361,11 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; }
|
|||
/* Open WebUI-style composer: a prominent input box with a slim action row below */
|
||||
.assistant-composer { border:1px solid var(--g200); border-radius:20px; box-shadow:0 8px 30px rgba(0,0,0,.08); padding:10px 14px 6px; display:flex; flex-direction:column; gap:2px; }
|
||||
.assistant-composer > label { display:none; }
|
||||
#assistant-input { flex:1 1 auto; border:none; outline:none; resize:none; background:transparent; font-size:15px; line-height:1.5; min-height:56px; max-height:180px; padding:6px 0; }
|
||||
/* 15px is the desktop size. The phone override lives earlier in this file, so
|
||||
at equal specificity this rule used to win and iOS zoomed on focus; the size
|
||||
is set per breakpoint here instead. */
|
||||
#assistant-input { flex:1 1 auto; border:none; outline:none; resize:none; background:transparent; line-height:1.5; min-height:56px; max-height:180px; padding:6px 0; }
|
||||
@media (min-width:641px) { #assistant-input { font-size:15px; } }
|
||||
.assistant-composer-footer { display:flex; align-items:center; gap:2px; border-top:1px solid var(--g100); padding-top:4px; margin-top:2px; }
|
||||
.assistant-composer-footer .assistant-tools { display:flex; align-items:center; gap:2px; margin-right:auto; }
|
||||
.assistant-composer-footer .assistant-attach, .assistant-composer-footer .assistant-voice-btn, .assistant-composer-footer .assistant-mic { width:30px; height:30px; font-size:14px; }
|
||||
|
|
@ -403,8 +407,20 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; }
|
|||
.create-image-history { display:grid; grid-template-columns:repeat(auto-fill,minmax(72px,1fr)); gap:8px; max-height:42vh; overflow-y:auto; margin-top:8px; }
|
||||
.assistant-gallery-running { display:flex; align-items:center; justify-content:center; min-height:72px; border:1px dashed var(--g300); border-radius:8px; color:var(--g500); }
|
||||
/* Composer model pill (Open WebUI-style, its own line above the actions) */
|
||||
.assistant-model-pill { display:flex; align-items:center; }
|
||||
.assistant-model-pill { position:relative; display:flex; align-items:center; }
|
||||
.assistant-model-pill[hidden] { display:none; }
|
||||
/* The select is the state holder only; the button is what people see. */
|
||||
.assistant-model-state { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0; overflow:hidden; clip-path:inset(50%); }
|
||||
.assistant-model-btn { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--g200); background:white; border-radius:999px; padding:5px 11px; font-family:inherit; font-size:11.5px; font-weight:600; color:var(--g600); cursor:pointer; white-space:nowrap; box-shadow:var(--shadow); }
|
||||
.assistant-model-btn:hover, .assistant-model-btn[aria-expanded="true"] { border-color:var(--blue-light); background:var(--blue-light); color:var(--blue); }
|
||||
.assistant-model-btn i { font-size:11px; }
|
||||
/* Opens upward: the composer sits at the foot of the view. */
|
||||
.assistant-model-menu { position:absolute; right:0; bottom:calc(100% + 6px); z-index:40; min-width:220px; max-width:min(320px,80vw); max-height:min(320px,50vh); overflow-y:auto; display:flex; flex-direction:column; gap:2px; padding:6px; background:white; border:1px solid var(--g200); border-radius:12px; box-shadow:0 12px 32px rgba(15,23,42,.16); }
|
||||
.assistant-model-menu[hidden] { display:none; }
|
||||
.assistant-model-option { display:flex; align-items:center; gap:8px; width:100%; border:0; border-radius:8px; padding:8px 10px; background:none; color:var(--g700); font-family:inherit; font-size:12.5px; text-align:left; cursor:pointer; overflow-wrap:anywhere; }
|
||||
.assistant-model-option:hover { background:var(--blue-light); color:var(--blue); }
|
||||
.assistant-model-option i { width:12px; flex:0 0 auto; font-size:10px; color:var(--blue); }
|
||||
.assistant-model-option[aria-selected="false"] i { visibility:hidden; }
|
||||
/* Model choice reads as a control, not a raw form element: the native chevron is
|
||||
replaced with one that matches the composer, and the whole pill responds. */
|
||||
.assistant-model-control { appearance:none; -webkit-appearance:none; border:1px solid var(--g200); background:white; border-radius:999px; padding:5px 26px 5px 11px; font-family:inherit; font-size:11.5px; font-weight:600; color:var(--g600); cursor:pointer; max-width:240px; text-overflow:ellipsis; box-shadow:var(--shadow);
|
||||
|
|
@ -584,3 +600,8 @@ body.assistant-mode-workspace .assistant-rail-workspace { display:flex; }
|
|||
/* Workspace links: the app's .tab-btn, full-bleed with the same inset. */
|
||||
body.assistant-workspace .assistant-rail-link { padding:10px 16px; }
|
||||
}
|
||||
|
||||
/* The composer says it is working through the send button, which becomes Stop.
|
||||
The status pill and the autosave word repeated that beside the model control,
|
||||
so they are announced but not drawn. */
|
||||
.assistant-status, .assistant-autosave-state { position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
|
||||
|
|
|
|||
|
|
@ -1276,6 +1276,21 @@ body.assistant-workspace #assistant-tab.active { width:100%; }
|
|||
/* iOS: taps on controls must not trigger double-tap zoom */
|
||||
button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, label { touch-action: manipulation; }
|
||||
|
||||
/* iOS Safari zooms the whole page when a text field smaller than 16px takes
|
||||
focus, and never zooms back out — which is why the menu button ended up off
|
||||
screen after typing. 16px is the threshold, so every text control gets it on
|
||||
a phone. This is what keeps other chat apps from zooming; no user-scalable=no,
|
||||
which would also block deliberate pinch-zoom. */
|
||||
/* !important is deliberate: roughly 144 controls across twenty components set
|
||||
their own font-size through class rules such as `.demo-field select`, which
|
||||
outrank any plain element selector. This is one platform workaround, not a
|
||||
style choice, so it overrides them all rather than being restated per
|
||||
component (and per new component added later). */
|
||||
@media (max-width:768px) {
|
||||
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]),
|
||||
textarea, select { font-size:16px !important; }
|
||||
}
|
||||
|
||||
/* ── Shared app chrome: the Assistant/Workspace switch and the menu toggle ────
|
||||
These render in BOTH the app sidebar and the assistant rail. They used to live
|
||||
in assistant.css, which only the assistant component loads — so on the main
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// moment rather than clicking into an empty tab.
|
||||
function openSubItem(sub, tabName, attempts) {
|
||||
var host = tabName ? document.getElementById(tabName + '-tab') : document;
|
||||
var selector = '[data-calc="' + sub + '"], [data-subtab="' + sub + '"], [data-section="' + sub + '"]';
|
||||
var selector = SUB_NAV_ATTRS.map(function(attr) { return '[' + attr + '="' + sub + '"]'; }).join(', ');
|
||||
var target = host && host.querySelector(selector);
|
||||
if (target) { target.click(); return; }
|
||||
if (attempts > 0) setTimeout(function() { openSubItem(sub, tabName, attempts - 1); }, 110);
|
||||
|
|
@ -299,13 +299,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// exam regions — is what people actually look for ("bili", not "Calculators").
|
||||
// Read generically from whatever a loaded component exposes, so this covers
|
||||
// every tab that has sub-navigation rather than one hard-coded list.
|
||||
var SUB_NAV_SELECTOR = '[data-calc], [data-subtab], [data-section], .calc-nav-pill';
|
||||
// One list, so what is listed, what is searched and what a result opens can
|
||||
// never drift apart. data-em is the bedside emergency sections.
|
||||
var SUB_NAV_ATTRS = ['data-calc', 'data-subtab', 'data-section', 'data-em'];
|
||||
var SUB_NAV_SELECTOR = SUB_NAV_ATTRS.map(function(attr) { return '[' + attr + ']'; }).join(', ') + ', .calc-nav-pill';
|
||||
|
||||
function subItemsFor(tabEl, tabName, tabTitle) {
|
||||
if (!tabEl || !tabEl.dataset.loaded) return []; // not loaded yet; nothing to read
|
||||
var seen = {};
|
||||
return Array.prototype.map.call(tabEl.querySelectorAll(SUB_NAV_SELECTOR), function(node) {
|
||||
var key = node.getAttribute('data-calc') || node.getAttribute('data-subtab') || node.getAttribute('data-section');
|
||||
var key = SUB_NAV_ATTRS.reduce(function(found, attr) { return found || node.getAttribute(attr); }, null);
|
||||
var title = (node.textContent || '').trim();
|
||||
if (!key || !title || title.length > 48 || seen[key + title]) return null;
|
||||
seen[key + title] = true;
|
||||
|
|
@ -338,7 +341,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// A component only exposes its sub-navigation once loaded, so warm the tabs
|
||||
// that have any. Cheap: each is fetched once and cached by the loader.
|
||||
function warmSearchableTabs() {
|
||||
['calculators', 'wellvisit', 'peguide', 'vaxschedule'].forEach(function(name) {
|
||||
['calculators', 'wellvisit', 'peguide', 'vaxschedule', 'bedside'].forEach(function(name) {
|
||||
var el = document.getElementById(name + '-tab');
|
||||
if (el && el.hasAttribute('data-component') && !el.dataset.loaded) loadComponent(el);
|
||||
});
|
||||
|
|
@ -348,8 +351,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (!searchResults) return;
|
||||
var source = searchSources();
|
||||
var needle = String(term || '').trim().toLowerCase();
|
||||
// With nothing typed the palette lists the main destinations only; the
|
||||
// sub-items (individual calculators, exam sections, schedule tabs) would
|
||||
// bury them, so they appear once there is something to match.
|
||||
var matches = source.items.filter(function(item) {
|
||||
return !needle || String(item.title || '').toLowerCase().indexOf(needle) !== -1;
|
||||
if (!needle) return !item.sub;
|
||||
return String(item.title || '').toLowerCase().indexOf(needle) !== -1;
|
||||
}).slice(0, 40);
|
||||
searchResults.innerHTML = '';
|
||||
if (!matches.length) {
|
||||
|
|
|
|||
|
|
@ -1560,8 +1560,83 @@ import {
|
|||
select.hidden = !show;
|
||||
var pill = document.getElementById('assistant-model-pill');
|
||||
if (pill) pill.hidden = !show;
|
||||
if (select.id === 'assistant-chat-model-select') syncModelMenu();
|
||||
}
|
||||
|
||||
// The composer shows the word "Model", not the model id, which can be as long
|
||||
// as "openrouter-gemini-3.1-flash-image-preview". The list behind it is built
|
||||
// from the select above, which stays the state holder, so choosing here goes
|
||||
// through the same change event that persists every other selection.
|
||||
function syncModelMenu() {
|
||||
var select = document.getElementById('assistant-chat-model-select');
|
||||
var menu = document.getElementById('assistant-model-menu');
|
||||
var button = document.getElementById('btn-assistant-model');
|
||||
if (!select || !menu || !button) return;
|
||||
menu.innerHTML = '';
|
||||
Array.prototype.forEach.call(select.options, function(option) {
|
||||
var row = document.createElement('button');
|
||||
row.type = 'button';
|
||||
row.className = 'assistant-model-option';
|
||||
row.setAttribute('role', 'option');
|
||||
row.setAttribute('data-model-value', option.value);
|
||||
row.setAttribute('aria-selected', option.value === select.value ? 'true' : 'false');
|
||||
var tick = document.createElement('i');
|
||||
tick.className = 'fas fa-check';
|
||||
var text = document.createElement('span');
|
||||
text.textContent = option.textContent;
|
||||
row.appendChild(tick);
|
||||
row.appendChild(text);
|
||||
menu.appendChild(row);
|
||||
});
|
||||
var current = select.options[select.selectedIndex];
|
||||
button.title = current ? 'Chat model: ' + current.textContent : 'Choose the chat model';
|
||||
}
|
||||
|
||||
function closeModelMenu() {
|
||||
var menu = document.getElementById('assistant-model-menu');
|
||||
var button = document.getElementById('btn-assistant-model');
|
||||
if (menu) menu.hidden = true;
|
||||
if (button) button.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function bindModelMenu() {
|
||||
var docEl = typeof document !== 'undefined' ? document.documentElement : null;
|
||||
if (docEl && docEl.dataset && docEl.dataset.modelMenuBound) return;
|
||||
if (docEl && docEl.dataset) docEl.dataset.modelMenuBound = '1';
|
||||
document.addEventListener('click', function(event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) return;
|
||||
var option = target.closest('.assistant-model-option');
|
||||
if (option) {
|
||||
var select = document.getElementById('assistant-chat-model-select');
|
||||
if (select) {
|
||||
select.value = option.getAttribute('data-model-value');
|
||||
// The delegated [data-model-select-kind] listener does the saving.
|
||||
var changed = document.createEvent('Event');
|
||||
changed.initEvent('change', true, false);
|
||||
select.dispatchEvent(changed);
|
||||
syncModelMenu();
|
||||
}
|
||||
closeModelMenu();
|
||||
return;
|
||||
}
|
||||
if (target.closest('#btn-assistant-model')) {
|
||||
var menu = document.getElementById('assistant-model-menu');
|
||||
var button = document.getElementById('btn-assistant-model');
|
||||
if (!menu || !button) return;
|
||||
var opening = menu.hidden;
|
||||
menu.hidden = !opening;
|
||||
button.setAttribute('aria-expanded', opening ? 'true' : 'false');
|
||||
return;
|
||||
}
|
||||
if (!target.closest('.assistant-model-pill')) closeModelMenu();
|
||||
});
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Escape') closeModelMenu();
|
||||
});
|
||||
}
|
||||
function bindModelSelects() {
|
||||
bindModelMenu();
|
||||
// Delegated persistence: any chat/image model select saves immediately,
|
||||
// even when the popup recreates its element.
|
||||
var docEl = typeof document !== 'undefined' ? document.documentElement : null;
|
||||
|
|
|
|||
|
|
@ -169,3 +169,20 @@ test('the phone top bar is solid behind the status bar too', () => {
|
|||
assert.match(css, /\.sidebar\{ width:min\(85vw,320px\); left:calc\(-1 \* min\(85vw,320px\)\); padding:env\(safe-area-inset-top, 0px\) 0 0; \}/);
|
||||
assert.match(read('public/css/assistant.css'), /\.assistant-sources-fab \{[^}]*top:calc\(8px \+ env\(safe-area-inset-top, 0px\)\)/);
|
||||
});
|
||||
|
||||
test('no text control is small enough to make iOS zoom the page', () => {
|
||||
const css = read('public/css/styles.css');
|
||||
// iOS Safari zooms the whole page when a field under 16px takes focus and
|
||||
// never zooms back, which is why fixed chrome (the menu button) ended up off
|
||||
// screen after typing. 16px is the threshold.
|
||||
const rule = css.slice(css.indexOf('/* iOS Safari zooms the whole page'));
|
||||
assert.match(rule, /@media \(max-width:768px\) \{\s*\n\s*input:not\(\[type="checkbox"\]\)/);
|
||||
assert.match(rule, /textarea, select \{ font-size:16px !important; \}/,
|
||||
'it must beat per-component rules such as .demo-field select');
|
||||
// Pinch-zoom stays available: the fix is the font size, not a locked viewport.
|
||||
assert.doesNotMatch(read('public/index.html'), /user-scalable=no|maximum-scale=1/);
|
||||
// The composer set 15px AFTER its own mobile override, so the base rule won.
|
||||
const assistant = read('public/css/assistant.css');
|
||||
assert.doesNotMatch(assistant, /#assistant-input \{ flex:1 1 auto;[^}]*font-size:15px/);
|
||||
assert.match(assistant, /@media \(min-width:641px\) \{ #assistant-input \{ font-size:15px; \} \}/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -515,7 +515,11 @@ test('search reaches the things inside a tab, not just the tab', () => {
|
|||
// People look for "bili", not "Calculators". Sub-navigation is read
|
||||
// generically from whatever a loaded component exposes, so this covers every
|
||||
// tab with sub-navigation rather than one hard-coded list.
|
||||
assert.match(app, /SUB_NAV_SELECTOR = '\[data-calc\], \[data-subtab\], \[data-section\], \.calc-nav-pill'/);
|
||||
// Listing, matching and opening all read one attribute list, so they cannot
|
||||
// drift apart when a component's sub-navigation is added.
|
||||
assert.match(app, /var SUB_NAV_ATTRS = \['data-calc', 'data-subtab', 'data-section', 'data-em'\];/);
|
||||
assert.match(app, /var SUB_NAV_SELECTOR = SUB_NAV_ATTRS\.map\(function\(attr\) \{ return '\[' \+ attr \+ '\]'; \}\)\.join\(', '\) \+ ', \.calc-nav-pill';/);
|
||||
assert.match(app, /var selector = SUB_NAV_ATTRS\.map\(function\(attr\) \{ return '\[' \+ attr \+ '="' \+ sub \+ '"\]'; \}\)\.join\(', '\);/);
|
||||
assert.match(app, /function subItemsFor\(/);
|
||||
assert.match(app, /if \(!tabEl \|\| !tabEl\.dataset\.loaded\) return \[\];/,
|
||||
'an unloaded component exposes nothing, so it is skipped rather than guessed at');
|
||||
|
|
@ -529,6 +533,9 @@ test('search reaches the things inside a tab, not just the tab', () => {
|
|||
|
||||
const calculators = read('public/components/calculators.html');
|
||||
assert.match(calculators, /data-calc="bili"/, 'the labels search reads are real');
|
||||
// Bedside marks its emergency sections with data-em, so they are searchable too.
|
||||
assert.match(read('public/components/bedside.html'), /data-em="sepsis"/);
|
||||
assert.match(app, /\['calculators', 'wellvisit', 'peguide', 'vaxschedule', 'bedside'\]/);
|
||||
});
|
||||
|
||||
test('the menu head reads toggle, mark, search — and account tabs are not listed twice', () => {
|
||||
|
|
@ -610,3 +617,31 @@ test('the create-image chat list never grows wider than its dialog', () => {
|
|||
assert.match(fn, /words\[i\]/, 'cuts on whole words');
|
||||
assert.match(js, /window\.addEventListener\('resize', function \(\) \{ fitSelectOptions\(document\.getElementById\('create-image-chat'\)\); \}\);/);
|
||||
});
|
||||
|
||||
test('the composer shows "Model", not the model id, and hides the status chatter', () => {
|
||||
const html = read('public/components/assistant.html');
|
||||
const css = read('public/css/assistant.css');
|
||||
const js = read('public/js/clinicalAssistant.js');
|
||||
|
||||
// The select stays as the state holder, so the existing change listener still
|
||||
// persists the choice; the button is only its compact face.
|
||||
assert.match(html, /<select id="assistant-chat-model-select"[^>]*assistant-model-state/);
|
||||
assert.match(html, /<button type="button" id="btn-assistant-model"[^>]*aria-haspopup="listbox"/);
|
||||
assert.match(html, /<span>Model<\/span>/);
|
||||
assert.match(css, /\.assistant-model-state \{ position:absolute; width:1px;/);
|
||||
assert.match(css, /\.assistant-model-menu \{ position:absolute; right:0; bottom:calc\(100% \+ 6px\)/, 'opens upward, above the composer');
|
||||
assert.match(js, /function syncModelMenu\(\)/);
|
||||
assert.match(js, /changed\.initEvent\('change', true, false\);/, 'choosing goes through the select, so it saves the usual way');
|
||||
|
||||
// "Ready" / "Looking up sources..." / "saved" repeated what the send button
|
||||
// already shows, and wrapped under the model control on a phone.
|
||||
assert.match(css, /\.assistant-status, \.assistant-autosave-state \{ position:absolute; width:1px;/);
|
||||
assert.match(html, /id="assistant-status"/, 'kept in the DOM so screen readers still hear progress');
|
||||
});
|
||||
|
||||
test('the search palette lists destinations first, sub-items only once typing', () => {
|
||||
const app = read('public/js/app.js');
|
||||
const fn = app.slice(app.indexOf('function renderSearch(term)'), app.indexOf('function openSearch()'));
|
||||
assert.match(fn, /if \(!needle\) return !item\.sub;/, 'nothing typed: main destinations only');
|
||||
assert.match(fn, /return String\(item\.title \|\| ''\)\.toLowerCase\(\)\.indexOf\(needle\) !== -1;/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue