An x in a search box clears it (#291)
Once something is typed: at the box's right end, inside it. One click handler for every box, which clears it and fires its input event, so each box's own handler brings its list back whole. The browser's own x, drawn by some browsers and not others, gives way to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1951,6 +1951,22 @@ test('a page brought back into view catches up with what was read elsewhere', as
|
|||||||
await expect(badge).toHaveText(String(before - 1));
|
await expect(badge).toHaveText(String(before - 1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a search box has an x that clears it once something is typed', async ({ page }) => {
|
||||||
|
// #291: inside the box, at its right end, and the list comes back whole.
|
||||||
|
const box = page.locator('#feedFilter');
|
||||||
|
const x = page.locator('.searchwrap .clearx');
|
||||||
|
const all = await page.locator('#feedlist .feed').count();
|
||||||
|
await expect(x).toBeHidden();
|
||||||
|
await box.fill('zzz-nothing');
|
||||||
|
await expect(x).toBeVisible();
|
||||||
|
await expect(page.locator('#feedlist .feed')).toHaveCount(0);
|
||||||
|
await x.click();
|
||||||
|
await expect(box).toHaveValue('');
|
||||||
|
await expect(box).toBeFocused();
|
||||||
|
await expect(x).toBeHidden();
|
||||||
|
await expect(page.locator('#feedlist .feed')).toHaveCount(all);
|
||||||
|
});
|
||||||
|
|
||||||
test('a feed\'s settings say what downloads, take words as chips, and keep what is saved', async ({ page }) => {
|
test('a feed\'s settings say what downloads, take words as chips, and keep what is saved', async ({ page }) => {
|
||||||
// The redesigned dialog (#167): the download choice, word chips, the fold, and a save that holds.
|
// The redesigned dialog (#167): the download choice, word chips, the fold, and a save that holds.
|
||||||
const open = async () => {
|
const open = async () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- A search box has an x at its right end once something is typed in it, which clears it: items, the feed list, the log in Admin and a Signal's icons (#291).
|
||||||
- A feed's, an OPML subscription's or a Patreon creator's settings have In All Subscriptions: off keeps its items out of All Subscriptions and its unread count, still there on its own page, in Signals and in Currently Listening (#288).
|
- A feed's, an OPML subscription's or a Patreon creator's settings have In All Subscriptions: off keeps its items out of All Subscriptions and its unread count, still there on its own page, in Signals and in Currently Listening (#288).
|
||||||
- A feed's settings have a Signals switch beside All Subscriptions, to keep its items out of every Signal (#289).
|
- A feed's settings have a Signals switch beside All Subscriptions, to keep its items out of every Signal (#289).
|
||||||
- An item sent as both HTML and Markdown, as Scripting News's are, has a choice beside its buttons to show it as the publisher's HTML or as the Markdown, kept from one item to the next (#285).
|
- An item sent as both HTML and Markdown, as Scripting News's are, has a choice beside its buttons to show it as the publisher's HTML or as the Markdown, kept from one item to the next (#285).
|
||||||
|
|||||||
17
web/app.css
17
web/app.css
@@ -537,7 +537,7 @@ a{color:var(--accent)}
|
|||||||
position:sticky;top:0;
|
position:sticky;top:0;
|
||||||
}
|
}
|
||||||
#topbar .grow{flex:1}
|
#topbar .grow{flex:1}
|
||||||
#topbar #epSearch{width:260px;flex:none}
|
#topbar #epBox{width:260px;flex:none}
|
||||||
.tgroup{display:flex;flex:none;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--panel2)}
|
.tgroup{display:flex;flex:none;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:var(--panel2)}
|
||||||
.tgroup button,.tgroup a{padding:5px 11px;min-width:34px;color:var(--dim);font-size:14px;line-height:1.3}
|
.tgroup button,.tgroup a{padding:5px 11px;min-width:34px;color:var(--dim);font-size:14px;line-height:1.3}
|
||||||
.tgroup a{display:inline-grid;place-items:center}
|
.tgroup a{display:inline-grid;place-items:center}
|
||||||
@@ -567,6 +567,17 @@ input[type=search],input[type=text],input[type=password],input[type=number],sele
|
|||||||
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;
|
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;
|
||||||
}
|
}
|
||||||
input:focus,select:focus{outline:0;border-color:var(--accent)}
|
input:focus,select:focus{outline:0;border-color:var(--accent)}
|
||||||
|
/* A search box and the x that clears it, inside its right end, there only once something is typed
|
||||||
|
(#291). The browser's own x, in some browsers and not others, gives way to it. */
|
||||||
|
.clearable{position:relative;display:flex;align-items:center;min-width:0}
|
||||||
|
/* With [type=search], or the phone's padding for every text box, later on, takes the room back. */
|
||||||
|
.clearable>input[type=search]{flex:1;min-width:0;padding-right:32px}
|
||||||
|
.clearable>input::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none}
|
||||||
|
.clearx{position:absolute;right:4px;top:50%;transform:translateY(-50%);display:grid;place-items:center;width:26px;height:26px;padding:0;border-radius:50%;color:var(--faint)}
|
||||||
|
.clearx:hover{color:var(--fg)}
|
||||||
|
.clearx svg{width:11px;height:11px}
|
||||||
|
.clearable:has(>input:placeholder-shown) .clearx{display:none}
|
||||||
|
.inline .clearable{flex:1}
|
||||||
/* The wider left gutter is the folder triangle's; everything in the list shifts with it, so the
|
/* The wider left gutter is the folder triangle's; everything in the list shifts with it, so the
|
||||||
feeds still line up with the places above. */
|
feeds still line up with the places above. */
|
||||||
#feedlist{overflow-y:auto;padding:0 8px 12px 16px;flex:1;min-height:0}
|
#feedlist{overflow-y:auto;padding:0 8px 12px 16px;flex:1;min-height:0}
|
||||||
@@ -1251,7 +1262,7 @@ body:has(#player.on) #status{padding-bottom:3px}
|
|||||||
padding-left:calc(8px + var(--safe-l));padding-right:calc(8px + var(--safe-r));
|
padding-left:calc(8px + var(--safe-l));padding-right:calc(8px + var(--safe-r));
|
||||||
}
|
}
|
||||||
#topbar .grow,.tgroup.item{display:none}
|
#topbar .grow,.tgroup.item{display:none}
|
||||||
#topbar #epSearch{width:auto;flex:1;min-width:0}
|
#topbar #epBox{width:auto;flex:1;min-width:0}
|
||||||
.tgroup button,.tgroup a{padding:4px 8px;min-width:32px}
|
.tgroup button,.tgroup a{padding:4px 8px;min-width:32px}
|
||||||
.fhead.slim .acts{flex:1 1 100%}
|
.fhead.slim .acts{flex:1 1 100%}
|
||||||
.iconbtn{width:38px;height:38px}
|
.iconbtn{width:38px;height:38px}
|
||||||
@@ -1359,7 +1370,7 @@ body:has(#player.on) #status{padding-bottom:3px}
|
|||||||
input[type=search],input[type=text],input[type=password],input[type=number],select{
|
input[type=search],input[type=text],input[type=password],input[type=number],select{
|
||||||
font-size:17px;background:var(--panel2);border-color:var(--edge);border-radius:10px;padding:9px 12px;
|
font-size:17px;background:var(--panel2);border-color:var(--edge);border-radius:10px;padding:9px 12px;
|
||||||
}
|
}
|
||||||
#topbar #epSearch{font-size:16px;border-radius:999px;padding:9px 14px}
|
#topbar #epSearch{font-size:16px;border-radius:999px;padding:9px 34px 9px 14px}
|
||||||
input:focus,select:focus{border-color:var(--accent)}
|
input:focus,select:focus{border-color:var(--accent)}
|
||||||
|
|
||||||
/* The feed's name as the page's large title, the way Podcasts heads a show. */
|
/* The feed's name as the page's large title, the way Podcasts heads a show. */
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<button id="tbFlag" title="Pin the selected item, so it is never deleted" aria-label="Pin" data-icon="pin" disabled></button>
|
<button id="tbFlag" title="Pin the selected item, so it is never deleted" aria-label="Pin" data-icon="pin" disabled></button>
|
||||||
</div>
|
</div>
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
<input type="search" id="epSearch" placeholder="Search items…">
|
<span class="clearable" id="epBox"><input type="search" id="epSearch" placeholder="Search items…"><button type="button" class="clearx" title="Clear" aria-label="Clear" data-icon="close"></button></span>
|
||||||
<div class="tgroup" id="admintools">
|
<div class="tgroup" id="admintools">
|
||||||
<button id="prefs" title="Settings" aria-label="Settings" data-icon="settings"></button>
|
<button id="prefs" title="Settings" aria-label="Settings" data-icon="settings"></button>
|
||||||
<a id="admin" href="/admin" title="Admin: the server's settings, accounts and the log" aria-label="Admin" data-icon="admin"></a>
|
<a id="admin" href="/admin" title="Admin: the server's settings, accounts and the log" aria-label="Admin" data-icon="admin"></a>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div id="shell">
|
<div id="shell">
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
<div class="searchwrap"><span class="clearable"><input type="search" id="feedFilter" placeholder="Filter feeds…"><button type="button" class="clearx" title="Clear" aria-label="Clear" data-icon="close"></button></span></div>
|
||||||
<div id="feedlist"></div>
|
<div id="feedlist"></div>
|
||||||
<div class="sidefoot">
|
<div class="sidefoot">
|
||||||
<div class="who"><span id="who"></span><button id="signout" title="Sign out" aria-label="Sign out" data-icon="signout"></button></div>
|
<div class="who"><span id="who"></span><button id="signout" title="Sign out" aria-label="Sign out" data-icon="signout"></button></div>
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ function drawLogView(){
|
|||||||
<option value="WARN">Warnings and errors</option>
|
<option value="WARN">Warnings and errors</option>
|
||||||
<option value="ERROR">Errors only</option>
|
<option value="ERROR">Errors only</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="search" id="logq" class="grow" placeholder="Filter…">
|
${searchBox('<input type="search" id="logq" placeholder="Filter…">','grow')}
|
||||||
<label class="check" style="margin:0"><input type="checkbox" id="logfollow" checked> Follow</label>
|
<label class="check" style="margin:0"><input type="checkbox" id="logfollow" checked> Follow</label>
|
||||||
<button class="btn ico" id="logcopy" title="Copy what is showing" aria-label="Copy what is showing">${ICON.copy}</button>
|
<button class="btn ico" id="logcopy" title="Copy what is showing" aria-label="Copy what is showing">${ICON.copy}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ function signalModal(sg?: Signal){
|
|||||||
aria-expanded="false"></button>
|
aria-expanded="false"></button>
|
||||||
<input type="text" id="sgname" value="${esc(sg?.name||'')}" placeholder="Formula 1"></div>
|
<input type="text" id="sgname" value="${esc(sg?.name||'')}" placeholder="Formula 1"></div>
|
||||||
<div id="sgpicker" hidden>
|
<div id="sgpicker" hidden>
|
||||||
<div class="inline"><input type="search" id="sgiconq" placeholder="Search icons: robot, car, music…" autocomplete="off">
|
<div class="inline">${searchBox('<input type="search" id="sgiconq" placeholder="Search icons: robot, car, music…" autocomplete="off">')}
|
||||||
<button type="button" class="dlink" id="sgjev" title="Pick an icon for me">Auto</button>
|
<button type="button" class="dlink" id="sgjev" title="Pick an icon for me">Auto</button>
|
||||||
<button type="button" class="dlink" id="sgnoicon">Initials</button></div>
|
<button type="button" class="dlink" id="sgnoicon">Initials</button></div>
|
||||||
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
|
<div class="icongrid" id="sgicons" role="listbox" aria-label="Icons"></div>
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ const EQ='<span class="eq" aria-hidden="true"><i></i><i></i><i></i></span>';
|
|||||||
// The page's own buttons name their icon; this draws it in, ahead of any label they carry.
|
// The page's own buttons name their icon; this draws it in, ahead of any label they carry.
|
||||||
for(const b of $$('[data-icon]')) b.insertAdjacentHTML('afterbegin',ICON[b.dataset.icon!]);
|
for(const b of $$('[data-icon]')) b.insertAdjacentHTML('afterbegin',ICON[b.dataset.icon!]);
|
||||||
|
|
||||||
|
/// A search box with an x at its right end that clears it, shown once something is typed (#291).
|
||||||
|
const searchBox=(input: string, cls='')=>
|
||||||
|
`<span class="clearable ${cls}">${input}<button type="button" class="clearx" title="Clear" aria-label="Clear">${ICON.close}</button></span>`;
|
||||||
|
// Every box's x, wherever it is drawn: cleared as if the typing were deleted, so the box's own input
|
||||||
|
// handler runs and its list comes back whole.
|
||||||
|
document.addEventListener('click',(e: MouseEvent)=>{
|
||||||
|
const x=(e.target as Element).closest?.('.clearx');
|
||||||
|
const box=x?.parentElement?.querySelector('input');
|
||||||
|
if(!box) return;
|
||||||
|
box.value='';
|
||||||
|
box.dispatchEvent(new Event('input',{bubbles:true}));
|
||||||
|
box.focus();
|
||||||
|
});
|
||||||
|
|
||||||
/// An episode whose DeadAir copy is still being made: it plays as it is until then, and the line
|
/// An episode whose DeadAir copy is still being made: it plays as it is until then, and the line
|
||||||
/// becomes how much shorter it is once the copy is ready (the dead_air event).
|
/// becomes how much shorter it is once the copy is ready (the dead_air event).
|
||||||
const DEAD_PENDING='<span class="dapend" title="DeadAir is still working on this episode. Until it is done, it plays at its full length.">Cutting dead air…</span>';
|
const DEAD_PENDING='<span class="dapend" title="DeadAir is still working on this episode. Until it is done, it plays at its full length.">Cutting dead air…</span>';
|
||||||
|
|||||||
Reference in New Issue
Block a user