@rcarls/rc-virtual-scroller 0.0.0 → 0.7.0

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.
@@ -0,0 +1,211 @@
1
+ import { LitElement } from 'lit';
2
+ import { LogicalAxis } from '@rcarls/rc-common';
3
+ export type RCVirtualScrollerAxis = LogicalAxis;
4
+ export type RCVirtualScrollerAlign = 'start' | 'center' | 'end' | 'nearest';
5
+ export interface RCVirtualScrollerRangeDetail {
6
+ /** First item index the consumer should render, inclusive. */
7
+ readonly start: number;
8
+ /** Item index one past the last the consumer should render. */
9
+ readonly end: number;
10
+ /**
11
+ * Effective items in one line: the explicit override, inferred uniform grid
12
+ * capacity accounting for spans, or one for a non-grid collection.
13
+ */
14
+ readonly itemsPerLine: number;
15
+ /** Line pitch in pixels along the scrolling axis, including the gap. */
16
+ readonly lineSize: number;
17
+ /** `false` while `lineSize` is still the `item-size` estimate, `true` once a real line was measured. */
18
+ readonly measured: boolean;
19
+ }
20
+ declare global {
21
+ interface HTMLElementTagNameMap {
22
+ 'rc-virtual-scroller': RCVirtualScroller;
23
+ }
24
+ interface HTMLElementEventMap {
25
+ 'rc-virtual-scroller-range': CustomEvent<RCVirtualScrollerRangeDetail>;
26
+ }
27
+ }
28
+ /**
29
+ * Reports which slice of a long collection is currently on screen and reserves
30
+ * the scroll space for the rest, so the consumer can render a few dozen items
31
+ * instead of a few thousand.
32
+ *
33
+ * The element is deliberately headless: it never creates, recycles, or
34
+ * positions item DOM. It measures geometry, dispatches
35
+ * `rc-virtual-scroller-range`, and sizes two spacers around the default slot.
36
+ * The consumer slots its own single container element (a `<ul>`, an
37
+ * `<rc-list>`, a `<table>` body) and renders `detail.start`..`detail.end` into
38
+ * it with whatever framework it already uses. That keeps the markup, the CSS
39
+ * layout, and the accessibility semantics with the code that owns them, and it
40
+ * means a native `grid-template-columns: repeat(auto-fill, ...)` keeps working
41
+ * untouched.
42
+ *
43
+ * `axis` is logical. `block` (the default) windows lines stacked the way
44
+ * paragraphs stack, which is vertical in horizontal writing modes and
45
+ * horizontal in vertical ones. `inline` windows a row of items running the way
46
+ * text runs, such as a horizontal shelf; the slotted container then lays its
47
+ * items along the inline axis (`display: flex`, or a grid with
48
+ * `grid-auto-flow: column`). Direction and writing mode are read from the
49
+ * element's own computed style, so RTL and vertical text need no configuration.
50
+ *
51
+ * Virtualization requires sequential DOM-order items with uniform line pitch
52
+ * and capacity (the final line may be partial). Regular grids, uniform numeric
53
+ * spans, lists, and nonwrapping shelves are measured from CSS. Use
54
+ * `items-per-line` for ambiguous grid capacity; it does not enable variable
55
+ * pitch, masonry, dense placement, or reordered items. Detectably unsupported
56
+ * layouts render the full collection and warn in development. `item-size` is
57
+ * only a bootstrap estimate. Hidden layouts retain their last valid window.
58
+ *
59
+ * Accessibility is the consumer's, because the consumer owns the roles. A
60
+ * virtualized set must still tell assistive technology its real size: put
61
+ * `aria-setsize` (the true total) and `aria-posinset` (the item's real index,
62
+ * not its DOM position) on each item, or `aria-rowcount` / `aria-rowindex` for
63
+ * a grid. See the package README. The one part this element does handle is
64
+ * focus: the reported range is always widened to include the focused item, so
65
+ * scrolling never silently drops focus to `<body>`.
66
+ *
67
+ * @slot - The consumer's single container element, holding only the items in the current range.
68
+ *
69
+ * @csspart spacer-start - Space along the scrolling axis standing in for the lines before the range.
70
+ * @csspart spacer-end - Space along the scrolling axis standing in for the lines after the range.
71
+ *
72
+ * @attr axis - Logical axis to window along: `block` (the default) or `inline`.
73
+ * @attr count - Total number of items in the collection, including the ones not rendered.
74
+ * @attr item-size - Estimated line pitch in pixels, used until a real line can be measured.
75
+ * @attr items-per-line - Explicit grid capacity; zero (the default) restores automatic inference.
76
+ * @attr overscan - Extra lines rendered beyond each edge of the viewport. Defaults to `2`.
77
+ * @attr disabled - Reports the whole collection as the range and stops measuring.
78
+ * @attr [first] - Reflected, computed. First item index in the current range.
79
+ * @attr [last] - Reflected, computed. Last item index in the current range, inclusive. `-1` when the range is empty.
80
+ *
81
+ * @fires rc-virtual-scroller-range - The visible range changed, or its geometry was re-measured.
82
+ *
83
+ * @see {@link https://richardcarls.github.io/rc-webcomponents/components/rc-virtual-scroller}
84
+ * @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/feed/ | WAI-ARIA APG: Feed pattern}
85
+ */
86
+ export declare class RCVirtualScroller extends LitElement {
87
+ static styles: import('lit').CSSResult;
88
+ static enabledWarnings: ("migration" | "async-perform-update")[];
89
+ /** Logical axis to window along. */
90
+ axis: RCVirtualScrollerAxis;
91
+ private _count;
92
+ private _itemSize;
93
+ private _overscan;
94
+ private _capacity;
95
+ /** Total collection size, normalized to a nonnegative integer. Invalid values become zero. Host writes are silent. */
96
+ get count(): number;
97
+ set count(value: number);
98
+ /**
99
+ * Estimated line pitch in pixels, used for the first frame and whenever no
100
+ * line is available to measure. Get this roughly right: it is what the very
101
+ * first `rc-virtual-scroller-range` is computed from, and what a restored
102
+ * scroll position lands against. Invalid or nonpositive values become zero.
103
+ * Host writes are silent.
104
+ */
105
+ get itemSize(): number;
106
+ set itemSize(value: number);
107
+ /** Grid capacity override, normalized to a nonnegative integer; zero means automatic. Host writes are silent. */
108
+ get itemsPerLine(): number;
109
+ set itemsPerLine(value: number);
110
+ /** Extra lines beyond each viewport edge, floored to an integer. Invalid/negative values become two. Host writes are silent. */
111
+ get overscan(): number;
112
+ set overscan(value: number);
113
+ /**
114
+ * Reports the whole collection as the range and stops measuring. An escape
115
+ * hatch for short collections, printing, and A/B measurement; the consumer's
116
+ * render path stays the same either way.
117
+ */
118
+ disabled: boolean;
119
+ /**
120
+ * The scroll container to measure against. Defaults to the nearest scrolling
121
+ * ancestor. Set this when the ancestor only becomes scrollable after its own
122
+ * upgrade, or when the intended scrollport is further up than the first
123
+ * `overflow: auto` element.
124
+ */
125
+ scrollTarget: Element | null;
126
+ /** Read-only first reported item index, also exposed as the computed `first` attribute. */
127
+ get first(): number;
128
+ /** Read-only last reported item index, or -1 for an empty range; also exposed as `last`. */
129
+ get last(): number;
130
+ /**
131
+ * The most recently reported range, or `null` before the first measurement.
132
+ * The element measures on its own schedule, which can be before a consumer
133
+ * has attached its listener; render from this once when subscribing, then
134
+ * follow `rc-virtual-scroller-range`. Identical ranges are never re-sent, so
135
+ * a subscriber that skips this can wait for an event that will not come.
136
+ */
137
+ get range(): RCVirtualScrollerRangeDetail | null;
138
+ private _spacerStart;
139
+ private _spacerEnd;
140
+ private readonly _frame;
141
+ private _geometry;
142
+ private _needsSample;
143
+ private _resetGeometry;
144
+ private _unsupported;
145
+ private readonly _warnedLayouts;
146
+ private _$container;
147
+ private _$scrollSource;
148
+ private _contentObserver;
149
+ /** The host's own flow, resolved with the scroll target rather than per frame. */
150
+ private _flow;
151
+ private _resolvedTarget;
152
+ private _resizeObserver;
153
+ private _unobserveLocale;
154
+ private _lastDetail;
155
+ /** Set when the surrounding layout changed enough to re-find the scrollport. */
156
+ private _needsResolve;
157
+ connectedCallback(): void;
158
+ disconnectedCallback(): void;
159
+ protected firstUpdated(): void;
160
+ protected updated(changed: Map<PropertyKey, unknown>): void;
161
+ protected render(): import('lit-html').TemplateResult<1>;
162
+ /**
163
+ * Scrolls the item at `index` into view along `axis`. Accurate to the current
164
+ * line pitch, which means accurate to `item-size` until at least one line has
165
+ * rendered.
166
+ */
167
+ scrollToIndex(index: number, options?: {
168
+ align?: RCVirtualScrollerAlign;
169
+ behavior?: ScrollBehavior;
170
+ }): void;
171
+ /**
172
+ * Re-measures the slotted container and re-evaluates the range. Call this
173
+ * when something outside the element's own observers changed the item size,
174
+ * such as a font load or a theme swap.
175
+ */
176
+ measure(): void;
177
+ /**
178
+ * Schedules an evaluation. `RafScheduler` keeps only the most recently
179
+ * scheduled callback, so every path has to funnel through this single one:
180
+ * a scroll arriving between a resize and its frame must not be able to drop
181
+ * the resize's work.
182
+ */
183
+ private readonly _requestEvaluate;
184
+ private readonly _requestSample;
185
+ /** Requests an evaluation that re-finds the scrollport and flow first. */
186
+ private readonly _requestResolve;
187
+ private _attachScrollTarget;
188
+ private _detachScrollTarget;
189
+ /** The scroll target's visible client box in viewport coordinates. */
190
+ private _portViewportRect;
191
+ private _documentScroller;
192
+ /**
193
+ * Where the visible part of the scroll target sits relative to the host,
194
+ * measured from the host's start edge along `axis` in the host's own flow.
195
+ * Working from the two rects rather than from `scrollTop`/`scrollLeft`
196
+ * keeps the arithmetic identical in every writing mode and direction.
197
+ * Reported in the host's own CSS pixels, the unit its spacers are written in.
198
+ */
199
+ private _view;
200
+ private _effectiveLineSize;
201
+ /** Rebind observers only when their actual targets changed. */
202
+ private _observeLayout;
203
+ private _observingLayout;
204
+ /**
205
+ * Focus retargets to a closed shadow host. Stop at the item that owns it,
206
+ * rather than asking ordinary contains() to cross a shadow boundary.
207
+ */
208
+ private _focusedIndex;
209
+ private _evaluate;
210
+ private _applyRange;
211
+ }
@@ -0,0 +1,2 @@
1
+ export declare const virtualScrollerStyles: import('lit').CSSResult;
2
+ export default virtualScrollerStyles;
@@ -0,0 +1,15 @@
1
+ import { Flow, LogicalAxis } from '@rcarls/rc-common';
2
+ import { Geometry } from './range.js';
3
+ export type LayoutSample = {
4
+ kind: 'ready' | 'incomplete';
5
+ geometry: Geometry;
6
+ } | {
7
+ kind: 'unsupported';
8
+ reason: string;
9
+ };
10
+ /**
11
+ * Sample regular line geometry without changing the consumer's layout.
12
+ * Geometry is reported in the host's CSS pixels, whatever units each
13
+ * measurement is read in.
14
+ */
15
+ export declare function sampleLayout($container: Element, axis: LogicalAxis, flow: Flow, override: number, previous: Geometry, hostZoom?: number): LayoutSample;
package/package.json CHANGED
@@ -1,14 +1,65 @@
1
1
  {
2
2
  "name": "@rcarls/rc-virtual-scroller",
3
- "version": "0.0.0",
4
- "description": "Placeholder published only to register this package name on npm before configuring Trusted Publishing. Contains no code. Do not install or depend on this version.",
3
+ "publishConfig": {
4
+ "access": "public"
5
+ },
6
+ "version": "0.7.0",
7
+ "description": "Headless virtualization wrapper that reports the visible index range for a consumer-owned list or grid and reserves the off-range scroll space.",
5
8
  "repository": {
6
9
  "type": "git",
7
- "url": "git+https://github.com/richardcarls/rc-webcomponents.git"
10
+ "url": "git+https://github.com/richardcarls/rc-webcomponents.git",
11
+ "directory": "packages/rc-virtual-scroller"
8
12
  },
13
+ "homepage": "https://richardcarls.github.io/rc-webcomponents/components/rc-virtual-scroller",
9
14
  "license": "MIT",
10
- "private": false,
11
- "publishConfig": {
12
- "access": "public"
15
+ "type": "module",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "module": "./dist/rc-virtual-scroller.js",
20
+ "exports": {
21
+ ".": {
22
+ "import": {
23
+ "types": "./dist/types/packages/rc-virtual-scroller/src/index.d.ts",
24
+ "default": "./dist/rc-virtual-scroller.js"
25
+ }
26
+ },
27
+ "./define": {
28
+ "import": {
29
+ "types": "./dist/types/packages/rc-virtual-scroller/src/define.d.ts",
30
+ "default": "./dist/rc-virtual-scroller-define.js"
31
+ }
32
+ }
33
+ },
34
+ "types": "./dist/types/packages/rc-virtual-scroller/src/index.d.ts",
35
+ "sideEffects": [
36
+ "./dist/rc-virtual-scroller-define.js"
37
+ ],
38
+ "customElements": "dist/custom-elements.json",
39
+ "scripts": {
40
+ "build": "tsc && vite build && cem analyze",
41
+ "cem:analyze": "cem analyze",
42
+ "preview": "vite preview",
43
+ "test:browser": "vitest --run",
44
+ "test:browser:chrome": "vitest --run --project=chromium",
45
+ "test:browser:firefox": "vitest --run --project=firefox"
46
+ },
47
+ "dependencies": {
48
+ "@rcarls/rc-common": "0.7.0"
49
+ },
50
+ "devDependencies": {
51
+ "@custom-elements-manifest/analyzer": "0.11.0",
52
+ "@vitest/browser-playwright": "4.1.5",
53
+ "lit": "^3.0.0",
54
+ "playwright": "^1.56.0",
55
+ "rollup": "^4.60.2",
56
+ "typescript": "~5.9.3",
57
+ "vite": "^7.1.7",
58
+ "vite-plugin-dts": "^4.5.4",
59
+ "vitest": "^4.0.6",
60
+ "vitest-browser-lit": "^1.0.1"
61
+ },
62
+ "peerDependencies": {
63
+ "lit": "^3.0.0"
13
64
  }
14
- }
65
+ }