Keep the scroll fade at the screen edge on the queue's filter row
The fade that hints at more filters to the right was an element inside the scrolling row, so it scrolled away with the chips instead of staying at the edge; the comment on it claimed the opposite. It is now a mask on the strip itself, applied only while there is more to scroll to (fadeOnOverflow in ui.js sets data-more), so it stays put and disappears at the end instead of dimming the last chip. The Team and Admin tab strips had an always-on mask from the earlier "Sources is cut off" fix, which dimmed their last tab even when fully scrolled; they use the same mechanism now. Web UI only: no endpoint or JSON shape changed, so nothing to mirror in terdut-tui.
This commit is contained in:
@@ -395,16 +395,13 @@ input:focus, textarea:focus { outline: none; border-color: var(--accent); box-sh
|
|||||||
.chip .count { margin-left: 4px; opacity: 0.7; }
|
.chip .count { margin-left: 4px; opacity: 0.7; }
|
||||||
/* Nothing in it: step back so the chips that have something stand out. */
|
/* Nothing in it: step back so the chips that have something stand out. */
|
||||||
.chip .count.zero { opacity: 0.4; }
|
.chip .count.zero { opacity: 0.4; }
|
||||||
/* An overlay, not a flex item: absolute against .chips' own (non-scrolling)
|
/* A scrolling strip fades on the right edge while there is more to scroll to
|
||||||
box stays flush with its real right edge regardless of scroll position,
|
(fadeOnOverflow in ui.js sets data-more). A mask on the strip itself, not an
|
||||||
which turned out not to be true of position:sticky here — as a flex
|
element inside it: anything inside a scroller scrolls away with the content,
|
||||||
item, its sticky offset interacted with the row's gap and its own
|
which is what the old overlay did. */
|
||||||
negative margin, landing short of the edge by about one gap's width. */
|
[data-more] {
|
||||||
.chips-fade {
|
-webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
|
||||||
position: absolute; top: 0; right: 0; bottom: 0;
|
mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent);
|
||||||
width: 24px;
|
|
||||||
background: linear-gradient(to right, transparent, var(--bg));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- lists ---------- */
|
/* ---------- lists ---------- */
|
||||||
@@ -886,7 +883,6 @@ kbd {
|
|||||||
/* The pane is 340-420px wide and a mouse cannot scroll a row whose scrollbar
|
/* The pane is 340-420px wide and a mouse cannot scroll a row whose scrollbar
|
||||||
is hidden, so the chips wrap here instead: Archived stays reachable. */
|
is hidden, so the chips wrap here instead: Archived stays reachable. */
|
||||||
.pane-list .chips { flex-wrap: wrap; overflow-x: visible; }
|
.pane-list .chips { flex-wrap: wrap; overflow-x: visible; }
|
||||||
.chips-fade { display: none; }
|
|
||||||
/* With nothing selected there is no detail to show next to, so the list
|
/* With nothing selected there is no detail to show next to, so the list
|
||||||
takes the whole row instead of leaving the second column as dead space
|
takes the whole row instead of leaving the second column as dead space
|
||||||
around the placeholder text. Selecting an incident (.has-detail) drops
|
around the placeholder text. Selecting an incident (.has-detail) drops
|
||||||
@@ -1236,11 +1232,6 @@ button.rota-week:hover { background: var(--surface-2); color: var(--text); }
|
|||||||
overflow-x: auto; scrollbar-width: none;
|
overflow-x: auto; scrollbar-width: none;
|
||||||
}
|
}
|
||||||
.subnav::-webkit-scrollbar { display: none; }
|
.subnav::-webkit-scrollbar { display: none; }
|
||||||
/* A fade on the right edge says "there is more" where the strip overflows;
|
|
||||||
the desktop width fits every entry, so it is phone-only. */
|
|
||||||
@media (max-width: 899px) {
|
|
||||||
.subnav { -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); }
|
|
||||||
}
|
|
||||||
.subnav-link {
|
.subnav-link {
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 8px 12px; margin-bottom: -1px;
|
padding: 8px 12px; margin-bottom: -1px;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
// gate.
|
// gate.
|
||||||
|
|
||||||
import * as api from './api.js';
|
import * as api from './api.js';
|
||||||
import { h, clear, spinner, confirm, menuCard, ssoBadge, SSO_MANAGED } from './ui.js';
|
import { h, clear, spinner, confirm, menuCard, ssoBadge, SSO_MANAGED, fadeOnOverflow } from './ui.js';
|
||||||
import { state, myID } from './state.js';
|
import { state, myID } from './state.js';
|
||||||
|
|
||||||
const view = () => document.getElementById('view-admin');
|
const view = () => document.getElementById('view-admin');
|
||||||
@@ -111,6 +111,7 @@ function subnav() {
|
|||||||
})));
|
})));
|
||||||
// On a phone the strip overflows; bring the open section into view so a
|
// On a phone the strip overflows; bring the open section into view so a
|
||||||
// tab past the edge (Sources, Switches) is never the one that is hidden.
|
// tab past the edge (Sources, Switches) is never the one that is hidden.
|
||||||
|
fadeOnOverflow(nav);
|
||||||
requestAnimationFrame(() => nav.querySelector('[aria-current]')
|
requestAnimationFrame(() => nav.querySelector('[aria-current]')
|
||||||
?.scrollIntoView({ inline: 'center', block: 'nearest' }));
|
?.scrollIntoView({ inline: 'center', block: 'nearest' }));
|
||||||
return nav;
|
return nav;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// The incident queue: filter chips and a list of incident rows.
|
// The incident queue: filter chips and a list of incident rows.
|
||||||
|
|
||||||
import * as api from './api.js';
|
import * as api from './api.js';
|
||||||
import { h, clear, badge, severityBadge, emptyState, spinner } from './ui.js';
|
import { h, clear, badge, severityBadge, emptyState, spinner, fadeOnOverflow } from './ui.js';
|
||||||
import { ago, until, isFuture, severityClass, labelSummary } from './format.js';
|
import { ago, until, isFuture, severityClass, labelSummary } from './format.js';
|
||||||
import { state, myID, onTeamChange } from './state.js';
|
import { state, myID, onTeamChange } from './state.js';
|
||||||
import * as onboarding from './onboarding.js';
|
import * as onboarding from './onboarding.js';
|
||||||
@@ -118,6 +118,10 @@ function chipCount(id) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the first time the chips are drawn: the strip element is the same one
|
||||||
|
// for the life of the page, so one scroll listener and one observer will do.
|
||||||
|
let chipsFade;
|
||||||
|
|
||||||
function renderChips() {
|
function renderChips() {
|
||||||
const el = document.getElementById('queue-filters');
|
const el = document.getElementById('queue-filters');
|
||||||
const chips = FILTERS.map((f) => {
|
const chips = FILTERS.map((f) => {
|
||||||
@@ -135,9 +139,9 @@ function renderChips() {
|
|||||||
// There are no team chips here: the team selector in the sidebar (and in the
|
// There are no team chips here: the team selector in the sidebar (and in the
|
||||||
// phone's top bar) is the one place the team is chosen, and chips for it
|
// phone's top bar) is the one place the team is chosen, and chips for it
|
||||||
// would be the same choice offered twice.
|
// would be the same choice offered twice.
|
||||||
chips.push(h('span', { class: 'chips-fade', 'aria-hidden': 'true' }));
|
|
||||||
|
|
||||||
clear(el, chips);
|
clear(el, chips);
|
||||||
|
chipsFade ??= fadeOnOverflow(el);
|
||||||
|
chipsFade();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList() {
|
function renderList() {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
// than no form, but it is not the thing enforcing anything.
|
// than no form, but it is not the thing enforcing anything.
|
||||||
|
|
||||||
import * as api from './api.js';
|
import * as api from './api.js';
|
||||||
import { h, clear, spinner, confirm, icon, openSheet, closeSheet, menuCard, badge, labelChip, ssoBadge, SSO_MANAGED } from './ui.js';
|
import { h, clear, spinner, confirm, icon, openSheet, closeSheet, menuCard, badge, labelChip, ssoBadge, SSO_MANAGED, fadeOnOverflow } from './ui.js';
|
||||||
import { state, currentTeam, onTeamChange, users as allUsers, myID } from './state.js';
|
import { state, currentTeam, onTeamChange, users as allUsers, myID } from './state.js';
|
||||||
import { isoDate, addDays, mondayOf, isoWeek, initial, ago, when, duration } from './format.js';
|
import { isoDate, addDays, mondayOf, isoWeek, initial, ago, when, duration } from './format.js';
|
||||||
|
|
||||||
@@ -177,6 +177,7 @@ function subnav() {
|
|||||||
})));
|
})));
|
||||||
// On a phone the strip overflows; bring the open section into view so a
|
// On a phone the strip overflows; bring the open section into view so a
|
||||||
// tab past the edge (Sources, Switches) is never the one that is hidden.
|
// tab past the edge (Sources, Switches) is never the one that is hidden.
|
||||||
|
fadeOnOverflow(nav);
|
||||||
requestAnimationFrame(() => nav.querySelector('[aria-current]')
|
requestAnimationFrame(() => nav.querySelector('[aria-current]')
|
||||||
?.scrollIntoView({ inline: 'center', block: 'nearest' }));
|
?.scrollIntoView({ inline: 'center', block: 'nearest' }));
|
||||||
return nav;
|
return nav;
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ export function clear(el, ...children) {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marks a horizontally scrolling strip with data-more while there is more of it
|
||||||
|
// to the right, which the CSS turns into a fade on that edge. The fade is a
|
||||||
|
// mask on the strip itself, so it stays put at the edge of the screen: a fade
|
||||||
|
// drawn as a child of the scroller scrolls away with the content. Returns the
|
||||||
|
// update function, for a strip whose contents change under it.
|
||||||
|
export function fadeOnOverflow(el) {
|
||||||
|
const update = () => {
|
||||||
|
el.toggleAttribute('data-more', el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
|
||||||
|
};
|
||||||
|
el.addEventListener('scroll', update, { passive: true });
|
||||||
|
if (typeof ResizeObserver === 'function') new ResizeObserver(update).observe(el);
|
||||||
|
update();
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
// Stroke icons, 24×24. Built as SVG nodes so the CSP needs no inline anything.
|
// Stroke icons, 24×24. Built as SVG nodes so the CSP needs no inline anything.
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
alertTriangle: ['M12 4l9.5 16.5h-19z', 'M12 10v4.5', 'M12 17.5h.01'],
|
alertTriangle: ['M12 4l9.5 16.5h-19z', 'M12 10v4.5', 'M12 17.5h.01'],
|
||||||
|
|||||||
Reference in New Issue
Block a user