@swiftbrowser/web 0.9.7 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/layout/engine.ts +14 -9
- package/src/protocol.ts +18 -1
- package/src/renderer.ts +80 -10
- package/src/scrollPosition.ts +44 -0
- package/src/styles.css +15 -2
package/package.json
CHANGED
package/src/layout/engine.ts
CHANGED
|
@@ -3357,6 +3357,12 @@ export class LayoutPass {
|
|
|
3357
3357
|
const searchOnTop = parts.search !== null && cctx.bottomBar > 0;
|
|
3358
3358
|
const searchRow = searchOnTop ? SEARCH_TOP_ROW : 0;
|
|
3359
3359
|
const searchBottom = parts.search && !searchOnTop ? SEARCH_BOTTOM_HEIGHT + searchMargin : 0;
|
|
3360
|
+
// The on-screen keyboard (device mode) covers the screen's bottom. A
|
|
3361
|
+
// bottom search field rises above it, as iOS 26 docks it on the
|
|
3362
|
+
// keyboard, so what is typed into it can be seen; the bars stay put,
|
|
3363
|
+
// covered.
|
|
3364
|
+
const keyboard = reachesBottom ? this.keyboard : 0;
|
|
3365
|
+
const searchLift = searchBottom > 0 ? keyboard : 0;
|
|
3360
3366
|
const contentTop = barHeight + largeHeight + searchRow;
|
|
3361
3367
|
const content: Rect = {
|
|
3362
3368
|
x,
|
|
@@ -3375,7 +3381,7 @@ export class LayoutPass {
|
|
|
3375
3381
|
const marginH = searchOnTop ? SEARCH_FIELD_MARGIN_H : SEARCH_BOTTOM_MARGIN_H;
|
|
3376
3382
|
chrome['searchField'] = {
|
|
3377
3383
|
x: x + marginH,
|
|
3378
|
-
y: searchOnTop ? y + barHeight + largeHeight + SEARCH_TOP_ABOVE : y + size.height - searchBottom,
|
|
3384
|
+
y: searchOnTop ? y + barHeight + largeHeight + SEARCH_TOP_ABOVE : y + size.height - searchLift - searchBottom,
|
|
3379
3385
|
width: Math.max(0, size.width - 2 * marginH),
|
|
3380
3386
|
height: searchOnTop ? SEARCH_TOP_HEIGHT : SEARCH_BOTTOM_HEIGHT,
|
|
3381
3387
|
};
|
|
@@ -3675,12 +3681,11 @@ export class LayoutPass {
|
|
|
3675
3681
|
// Under a tab bar the bar (which includes the bottom safe area) is what
|
|
3676
3682
|
// non-scrolling content must clear; a bottom toolbar already excluded it.
|
|
3677
3683
|
const clears = bottomBarHeight === 0 && y + size.height >= screen.height ? Math.max(safeArea.bottom, cctx.bottomBar) : 0;
|
|
3678
|
-
// The
|
|
3679
|
-
//
|
|
3680
|
-
//
|
|
3681
|
-
//
|
|
3682
|
-
const
|
|
3683
|
-
const bottomInset = Math.max(clears, keyboard - bottomCover);
|
|
3684
|
+
// The keyboard covers the bars (but not the search field, lifted above
|
|
3685
|
+
// it): the content stops above it as on iOS, and a scroll view runs under
|
|
3686
|
+
// only the part of a bar still showing above it, or under the whole
|
|
3687
|
+
// search field.
|
|
3688
|
+
const bottomInset = Math.max(clears, keyboard - (searchLift > 0 ? 0 : bottomCover));
|
|
3684
3689
|
// Phase 8: `.ignoresSafeArea` content grows up under the bar (and the
|
|
3685
3690
|
// large title / search field), which is transparent over it.
|
|
3686
3691
|
const contentCtx: Context = {
|
|
@@ -3692,7 +3697,7 @@ export class LayoutPass {
|
|
|
3692
3697
|
...(bottomCover > 0 ? { bottomBar: bottomCover, bottomEdge: y + size.height } : {}),
|
|
3693
3698
|
};
|
|
3694
3699
|
if (keyboard > 0) {
|
|
3695
|
-
contentCtx.bottomBar = Math.max(0, (bottomCover > 0 ? bottomCover : cctx.bottomBar) - keyboard);
|
|
3700
|
+
contentCtx.bottomBar = searchLift > 0 ? bottomCover : Math.max(0, (bottomCover > 0 ? bottomCover : cctx.bottomBar) - keyboard);
|
|
3696
3701
|
contentCtx.bottomEdge = y + size.height - keyboard;
|
|
3697
3702
|
}
|
|
3698
3703
|
if (scroller) {
|
|
@@ -3716,7 +3721,7 @@ export class LayoutPass {
|
|
|
3716
3721
|
// content clears there.
|
|
3717
3722
|
if (!large) contentCtx.barAbove = { top: content.y, height: content.y - y, x, width: size.width };
|
|
3718
3723
|
if (keyboard > 0) {
|
|
3719
|
-
contentCtx.bottomBar = Math.max(0, (bottomCover > 0 ? bottomCover + clears : cctx.bottomBar) - keyboard);
|
|
3724
|
+
contentCtx.bottomBar = searchLift > 0 ? bottomCover + clears : Math.max(0, (bottomCover > 0 ? bottomCover + clears : cctx.bottomBar) - keyboard);
|
|
3720
3725
|
} else if (bottomCover > 0) {
|
|
3721
3726
|
contentCtx.bottomBar = bottomCover + clears;
|
|
3722
3727
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -1869,6 +1869,14 @@ export interface ScrollViewProps {
|
|
|
1869
1869
|
* past it while scrolling and snapping as before.
|
|
1870
1870
|
*/
|
|
1871
1871
|
clipDisabled?: boolean;
|
|
1872
|
+
/**
|
|
1873
|
+
* `.scrollPosition(id:)`: when scrolling settles, the page sends
|
|
1874
|
+
* `scrollposition` to the scroll target (`scrollID`) at the view's anchor
|
|
1875
|
+
* point, once per change of target (docs/ops-protocol.md, "Scrolling to a view").
|
|
1876
|
+
*/
|
|
1877
|
+
reportsPosition?: boolean;
|
|
1878
|
+
/** With `reportsPosition`: the point of the visible area the reported target is at; the leading edge (`{x: 0, y: 0}`) when omitted. */
|
|
1879
|
+
positionAnchor?: UnitPoint;
|
|
1872
1880
|
}
|
|
1873
1881
|
|
|
1874
1882
|
export interface Style {
|
|
@@ -1940,6 +1948,15 @@ export interface RefreshEvent {
|
|
|
1940
1948
|
type: 'refresh';
|
|
1941
1949
|
id: ElementId;
|
|
1942
1950
|
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Sent to a scroll target (`scrollID`) inside a `scrollview` with
|
|
1953
|
+
* `reportsPosition`: scrolling settled with this target at the view's
|
|
1954
|
+
* anchor point. Sent once per change of target.
|
|
1955
|
+
*/
|
|
1956
|
+
export interface ScrollPositionEvent {
|
|
1957
|
+
type: 'scrollposition';
|
|
1958
|
+
id: ElementId;
|
|
1959
|
+
}
|
|
1943
1960
|
/**
|
|
1944
1961
|
* A movable `listrow` was dragged to a new place: `to` is the index among
|
|
1945
1962
|
* its container's children (the `list`'s, or its `section`'s) before which
|
|
@@ -1985,7 +2002,7 @@ export interface DropEvent {
|
|
|
1985
2002
|
}
|
|
1986
2003
|
|
|
1987
2004
|
/** The Phase 8 events (merged into `RendererEvent` above). */
|
|
1988
|
-
export type InteractionEvent = DragEvent | PinchEvent | LongPressEvent | DeleteEvent | RefreshEvent | MoveEvent | DragStartEvent | DragEndEvent | DropEvent;
|
|
2005
|
+
export type InteractionEvent = DragEvent | PinchEvent | LongPressEvent | DeleteEvent | RefreshEvent | ScrollPositionEvent | MoveEvent | DragStartEvent | DragEndEvent | DropEvent;
|
|
1989
2006
|
// Phase 8: alerts, controls, effects
|
|
1990
2007
|
// (docs/ops-protocol.md, "Phase 8: interaction"; gestures, swipe actions,
|
|
1991
2008
|
// context menus, list rows and refresh live in their own block)
|
package/src/renderer.ts
CHANGED
|
@@ -151,6 +151,7 @@ import { VideoEvents, applyVideo, stopVideos } from './video';
|
|
|
151
151
|
import { PinchController } from './pinch';
|
|
152
152
|
import { applyCanvas, createCanvasElement } from './canvasDrawing';
|
|
153
153
|
import { scrollToTarget } from './scrollTo';
|
|
154
|
+
import { POSITION_SETTLE_MS, anchorPoint, positionTarget, type PositionCandidate } from './scrollPosition';
|
|
154
155
|
import { applyDefaultScrollAnchor, keepScrollAnchors } from './scrollAnchor';
|
|
155
156
|
import { EMPTY_CATALOG, assetColorVar, type AssetCatalog } from './assets';
|
|
156
157
|
import { HIERARCHY_OPACITY, colorToCSS, effectsCSS, styleToCSS, type PaintCSS } from './styles';
|
|
@@ -360,6 +361,10 @@ export class Renderer {
|
|
|
360
361
|
*/
|
|
361
362
|
private rootSize: { width: number; height: number } | null = null;
|
|
362
363
|
private readonly resizeObserver: ResizeObserver | null = null;
|
|
364
|
+
/** `reportsPosition` scroll views mid-scroll: the timer that calls the scroll settled (src/scrollPosition.ts). */
|
|
365
|
+
private readonly positionTimers = new Map<HTMLElement, number>();
|
|
366
|
+
/** The scroll target each `reportsPosition` scroll view last reported, so a settle at the same one says nothing. */
|
|
367
|
+
private readonly reportedPositions = new WeakMap<HTMLElement, number>();
|
|
363
368
|
/**
|
|
364
369
|
* `host` elements (src/hostContent.ts): their content elements, observed
|
|
365
370
|
* for size changes, and the measurements of the current layout pass.
|
|
@@ -855,6 +860,8 @@ export class Renderer {
|
|
|
855
860
|
root.addEventListener('focusout', this.onFocusChange);
|
|
856
861
|
root.addEventListener('scroll', this.onScrollDismissKeyboard, true);
|
|
857
862
|
root.addEventListener('scroll', this.queueLazyCheck, { capture: true, passive: true });
|
|
863
|
+
root.addEventListener('scroll', this.onPositionScroll, { capture: true, passive: true });
|
|
864
|
+
root.addEventListener('scrollend', this.onPositionScrollEnd, { capture: true, passive: true });
|
|
858
865
|
this.videoEvents = new VideoEvents(root, (event) => this.onEvent(event));
|
|
859
866
|
// Phase 8: pointer gestures (the sheet drag among them) are delegated too.
|
|
860
867
|
const host: InteractionHost = {
|
|
@@ -1448,24 +1455,27 @@ export class Renderer {
|
|
|
1448
1455
|
height += row.kind === 'caption' ? MENU_CAPTION_HEIGHT : row.kind === 'separator' ? MENU_SEPARATOR_HEIGHT : PICKER_MENU_ROW;
|
|
1449
1456
|
}
|
|
1450
1457
|
// Anchor to the control: trailing edges aligned, below it when there is
|
|
1451
|
-
// room, else above; clamped inside the screen.
|
|
1458
|
+
// room, else above; clamped inside the screen. A menu taller than the
|
|
1459
|
+
// room on either side (a picker of every hour) takes the roomier side,
|
|
1460
|
+
// as tall as that room, and scrolls inside, opened at its checked row.
|
|
1452
1461
|
const rootRect = this.root.getBoundingClientRect();
|
|
1453
1462
|
const anchorRect = anchor.getBoundingClientRect();
|
|
1454
1463
|
const width = Math.min(PICKER_MENU_WIDTH, Math.max(0, rootRect.width - 2 * PICKER_MENU_MARGIN));
|
|
1455
1464
|
let left = anchorRect.right - rootRect.left - width;
|
|
1456
1465
|
left = Math.max(PICKER_MENU_MARGIN, Math.min(left, rootRect.width - width - PICKER_MENU_MARGIN));
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
top = Math.max(PICKER_MENU_MARGIN, top);
|
|
1466
|
+
const belowTop = anchorRect.bottom - rootRect.top + PICKER_MENU_GAP;
|
|
1467
|
+
const roomBelow = rootRect.height - PICKER_MENU_MARGIN - belowTop;
|
|
1468
|
+
const roomAbove = anchorRect.top - rootRect.top - PICKER_MENU_GAP - PICKER_MENU_MARGIN;
|
|
1469
|
+
const below = height <= roomBelow || (height > roomAbove && roomBelow >= roomAbove);
|
|
1470
|
+
const shown = Math.min(height, Math.max(0, below ? roomBelow : roomAbove));
|
|
1471
|
+
const top = below ? belowTop : anchorRect.top - rootRect.top - PICKER_MENU_GAP - shown;
|
|
1472
|
+
menu.dataset['sbPlacement'] = below ? 'below' : 'above';
|
|
1465
1473
|
menu.style.left = `${Math.round(left)}px`;
|
|
1466
|
-
menu.style.top = `${Math.round(top)}px`;
|
|
1474
|
+
menu.style.top = `${Math.round(Math.max(PICKER_MENU_MARGIN, top))}px`;
|
|
1467
1475
|
menu.style.width = `${Math.round(width)}px`;
|
|
1476
|
+
if (shown < height) menu.style.maxHeight = `${Math.round(shown)}px`;
|
|
1468
1477
|
this.root.append(menu);
|
|
1478
|
+
if (shown < height) menu.querySelector('[aria-checked="true"]')?.scrollIntoView({ block: 'nearest' });
|
|
1469
1479
|
anchor.setAttribute('aria-expanded', 'true');
|
|
1470
1480
|
this.menu = { el: menu, anchor, anchorId: id };
|
|
1471
1481
|
document.addEventListener('click', this.onDocumentClick, true);
|
|
@@ -1562,6 +1572,10 @@ export class Renderer {
|
|
|
1562
1572
|
this.root.removeEventListener('focusin', this.onFocusChange);
|
|
1563
1573
|
this.root.removeEventListener('focusout', this.onFocusChange);
|
|
1564
1574
|
this.root.removeEventListener('scroll', this.onScrollDismissKeyboard, true);
|
|
1575
|
+
this.root.removeEventListener('scroll', this.onPositionScroll, true);
|
|
1576
|
+
this.root.removeEventListener('scrollend', this.onPositionScrollEnd, true);
|
|
1577
|
+
for (const timer of this.positionTimers.values()) window.clearTimeout(timer);
|
|
1578
|
+
this.positionTimers.clear();
|
|
1565
1579
|
this.interaction.dispose();
|
|
1566
1580
|
this.pressEffects.dispose();
|
|
1567
1581
|
this.externalDrops.dispose();
|
|
@@ -1649,6 +1663,60 @@ export class Renderer {
|
|
|
1649
1663
|
}
|
|
1650
1664
|
}
|
|
1651
1665
|
|
|
1666
|
+
// -------------------------------------------------------------------------
|
|
1667
|
+
// `.scrollPosition(id:)` reading back (src/scrollPosition.ts)
|
|
1668
|
+
// -------------------------------------------------------------------------
|
|
1669
|
+
|
|
1670
|
+
/** A `reportsPosition` scroll view scrolled: it has settled when no further scroll follows for a moment. */
|
|
1671
|
+
private readonly onPositionScroll = (event: Event): void => {
|
|
1672
|
+
const el = event.target;
|
|
1673
|
+
if (!(el instanceof HTMLElement) || el.dataset['sbReportsPosition'] === undefined) return;
|
|
1674
|
+
const pending = this.positionTimers.get(el);
|
|
1675
|
+
if (pending !== undefined) window.clearTimeout(pending);
|
|
1676
|
+
this.positionTimers.set(
|
|
1677
|
+
el,
|
|
1678
|
+
window.setTimeout(() => this.reportPosition(el), POSITION_SETTLE_MS),
|
|
1679
|
+
);
|
|
1680
|
+
};
|
|
1681
|
+
|
|
1682
|
+
/** The browser says the scroll ended (Chrome, Firefox; Safari waits for the timer). */
|
|
1683
|
+
private readonly onPositionScrollEnd = (event: Event): void => {
|
|
1684
|
+
const el = event.target;
|
|
1685
|
+
if (!(el instanceof HTMLElement) || el.dataset['sbReportsPosition'] === undefined) return;
|
|
1686
|
+
this.reportPosition(el);
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* Which scroll target of a settled scroll view is at its anchor point: the
|
|
1691
|
+
* `scrollID` boxes inside it whose nearest scrolling ancestor it is, along
|
|
1692
|
+
* the axis it scrolls on (the vertical one when it scrolls both ways). Sent
|
|
1693
|
+
* to that target when it is not the one reported last.
|
|
1694
|
+
*/
|
|
1695
|
+
private reportPosition(el: HTMLElement): void {
|
|
1696
|
+
const pending = this.positionTimers.get(el);
|
|
1697
|
+
if (pending !== undefined) window.clearTimeout(pending);
|
|
1698
|
+
this.positionTimers.delete(el);
|
|
1699
|
+
const node = this.nodes.get(Number(el.dataset[ID_ATTR]));
|
|
1700
|
+
if (!node || !el.isConnected) return;
|
|
1701
|
+
const horizontal = node.props['axes'] === 'horizontal';
|
|
1702
|
+
const anchor = node.props['positionAnchor'] as { x?: unknown; y?: unknown } | undefined;
|
|
1703
|
+
const fraction = horizontal ? anchor?.x : anchor?.y;
|
|
1704
|
+
const box = el.getBoundingClientRect();
|
|
1705
|
+
const point = anchorPoint(horizontal ? { start: box.left, length: box.width } : { start: box.top, length: box.height }, typeof fraction === 'number' ? fraction : 0);
|
|
1706
|
+
const candidates: PositionCandidate[] = [];
|
|
1707
|
+
for (const target of el.querySelectorAll<HTMLElement>('[data-sb-id]')) {
|
|
1708
|
+
const id = Number(target.dataset[ID_ATTR]);
|
|
1709
|
+
const style = (this.nodes.get(id)?.props as Partial<StyledProps> | undefined)?.style;
|
|
1710
|
+
if (!style?.scrollID || target.parentElement?.closest('.sb-scrollview, .sb-list') !== el) continue;
|
|
1711
|
+
const rect = target.getBoundingClientRect();
|
|
1712
|
+
candidates.push(horizontal ? { id, start: rect.left, length: rect.width } : { id, start: rect.top, length: rect.height });
|
|
1713
|
+
}
|
|
1714
|
+
const target = positionTarget(candidates, point);
|
|
1715
|
+
if (target === null || this.reportedPositions.get(el) === target) return;
|
|
1716
|
+
this.reportedPositions.set(el, target);
|
|
1717
|
+
this.onEvent({ type: 'scrollposition', id: target });
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1652
1720
|
/** A `styled` whose style carries a `scrollTo` not seen before is scrolled to after this pass's layout. */
|
|
1653
1721
|
private noteScroll(id: ElementId, kind: ElementKind, props: ElementProps): void {
|
|
1654
1722
|
if (kind !== 'styled') return;
|
|
@@ -3134,6 +3202,8 @@ function applyKindProps(el: HTMLElement, kind: ElementKind, props: ElementProps,
|
|
|
3134
3202
|
if (scroll.snap === 'viewAligned' || scroll.snap === 'paging') el.dataset['sbSnap'] = scroll.snap;
|
|
3135
3203
|
else delete el.dataset['sbSnap'];
|
|
3136
3204
|
if (scroll.dismissesKeyboard === true) el.dataset['sbDismissesKeyboard'] = 'true';
|
|
3205
|
+
if (scroll.reportsPosition === true) el.dataset['sbReportsPosition'] = '';
|
|
3206
|
+
else delete el.dataset['sbReportsPosition'];
|
|
3137
3207
|
applyEdgeEffect(el, scroll.edgeEffect);
|
|
3138
3208
|
applyDefaultScrollAnchor(el, scroll.defaultScrollAnchor);
|
|
3139
3209
|
break;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.scrollPosition(id:)` reading back (docs/ops-protocol.md, "Scrolling to a
|
|
3
|
+
* view"): a `scrollview` with `reportsPosition` tells the app which scroll
|
|
4
|
+
* target (`scrollID`) is at its anchor point once scrolling settles, as
|
|
5
|
+
* SwiftUI sets the binding to the id of the view at the anchor. The renderer
|
|
6
|
+
* gathers the targets' extents along the axis and asks `positionTarget`
|
|
7
|
+
* which one is there; the event goes to that target's element.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** A scroll target's extent along the scroll axis, in the same coordinates as the viewport. */
|
|
11
|
+
export interface PositionCandidate {
|
|
12
|
+
id: number;
|
|
13
|
+
start: number;
|
|
14
|
+
length: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A settled scroll is one with no `scroll` event for this long (`scrollend`, where the browser has it, ends the wait). */
|
|
18
|
+
export const POSITION_SETTLE_MS = 120;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The target at `point` along the axis: the one whose extent holds the point
|
|
22
|
+
* (the last of several, as a later sibling draws over an earlier one), else
|
|
23
|
+
* the first one starting after it (the point is in a gap or before the
|
|
24
|
+
* content), else the last one (the point is past the end). Null without
|
|
25
|
+
* candidates; candidates of no extent are hidden and left out.
|
|
26
|
+
*/
|
|
27
|
+
export function positionTarget(candidates: readonly PositionCandidate[], point: number): number | null {
|
|
28
|
+
const shown = candidates.filter((c) => c.length > 0);
|
|
29
|
+
if (shown.length === 0) return null;
|
|
30
|
+
let holding: PositionCandidate | null = null;
|
|
31
|
+
let next: PositionCandidate | null = null;
|
|
32
|
+
for (const c of shown) {
|
|
33
|
+
if (c.start <= point && point < c.start + c.length) holding = c;
|
|
34
|
+
else if (c.start > point && (!next || c.start < next.start)) next = c;
|
|
35
|
+
}
|
|
36
|
+
if (holding) return holding.id;
|
|
37
|
+
if (next) return next.id;
|
|
38
|
+
return shown.reduce((last, c) => (c.start >= last.start ? c : last)).id;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The anchor point of a viewport along the axis: its start plus the anchor's fraction of its length. */
|
|
42
|
+
export function anchorPoint(viewport: { start: number; length: number }, fraction: number): number {
|
|
43
|
+
return viewport.start + Math.min(1, Math.max(0, fraction)) * viewport.length;
|
|
44
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -1945,11 +1945,18 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
1945
1945
|
backdrop-filter: blur(40px) saturate(180%);
|
|
1946
1946
|
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
|
1947
1947
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.22);
|
|
1948
|
-
|
|
1948
|
+
/* Taller than the room it was given (`max-height`), it scrolls inside. */
|
|
1949
|
+
overflow: hidden auto;
|
|
1950
|
+
overscroll-behavior: contain;
|
|
1951
|
+
scrollbar-width: none;
|
|
1949
1952
|
transform-origin: 50% 0;
|
|
1950
1953
|
animation: sb-menu-in 0.18s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
1951
1954
|
}
|
|
1952
1955
|
|
|
1956
|
+
.sb-picker-menu::-webkit-scrollbar {
|
|
1957
|
+
display: none;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1953
1960
|
.sb-picker-menu-exit {
|
|
1954
1961
|
pointer-events: none;
|
|
1955
1962
|
animation: sb-menu-out 0.15s ease-in both;
|
|
@@ -2968,9 +2975,15 @@ svg[data-sb-symbol="progress.indicator"] > line:nth-child(8) { animation-delay:
|
|
|
2968
2975
|
/* Swipe rows (`swipeactions`, deletable `listrow`): the content slot paints
|
|
2969
2976
|
the row background and slides over the action buttons behind it. */
|
|
2970
2977
|
.sb-swipeactions,
|
|
2971
|
-
.sb-listrow
|
|
2978
|
+
.sb-listrow,
|
|
2979
|
+
.sb-swipeactions {
|
|
2972
2980
|
overflow: hidden;
|
|
2973
2981
|
touch-action: pan-y;
|
|
2982
|
+
/* The swipe content's z-index (below) orders it over the actions within
|
|
2983
|
+
the row only: without its own stacking context it ranked with the
|
|
2984
|
+
screen's bar (z-index 1), and a row scrolled under the bar painted over
|
|
2985
|
+
the title. */
|
|
2986
|
+
isolation: isolate;
|
|
2974
2987
|
}
|
|
2975
2988
|
|
|
2976
2989
|
.sb-swipeactions > .sb-swipe-content,
|