@rcarls/rc-scroller 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # @rcarls/rc-scroller
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 5c006de: Ship component CSS minified. Package builds now run the CSS in Lit `css` styles
8
+ and light-DOM structural styles through Lightning CSS, removing comments and
9
+ whitespace while keeping source maps that resolve to each template. Rendered
10
+ styles are unchanged.
11
+
12
+ `@rcarls/rc-common` adds `cssText`, an identity tag that marks a plain CSS
13
+ string for minification. `rc-chip`, `rc-disclosure`, and `rc-accordion` now
14
+ depend on `@rcarls/rc-common` for it.
15
+
16
+ - a6553ff: Batch layout sampling into a shared animation-frame read phase so mounting a
17
+ large component route no longer alternates forced layout reads with component
18
+ writes. Closed anchored popups now skip placement work until they open, while
19
+ scroller boundaries, carousel positioning, flow state, and opted-in initial
20
+ scroll observation settle before the next paint.
21
+
22
+ Add `RafScheduler.schedulePhased()`, live `AnchorController.disabled` getters,
23
+ and the `ScrollObserverController.initialEvaluation` timing option.
24
+
25
+ - 2abffd2: Compute the `at-block-*` and `at-inline-*` boundary attributes along logical
26
+ axes, so they are correct in vertical writing modes as well as RTL, and
27
+ recompute them at most once per animation frame instead of on every scroll
28
+ and resize callback.
29
+ - Updated dependencies [e3dfa59]
30
+ - Updated dependencies [50d5610]
31
+ - Updated dependencies [9ee769c]
32
+ - Updated dependencies [1179f24]
33
+ - Updated dependencies [595fe37]
34
+ - Updated dependencies [38cd00c]
35
+ - Updated dependencies [5c006de]
36
+ - Updated dependencies [a6553ff]
37
+ - Updated dependencies [2205478]
38
+ - Updated dependencies [4341311]
39
+ - Updated dependencies [899bb5e]
40
+ - Updated dependencies [25fcfe0]
41
+ - Updated dependencies [51f6403]
42
+ - Updated dependencies [9caca6b]
43
+ - Updated dependencies [b764a3c]
44
+ - Updated dependencies [54d9ead]
45
+ - Updated dependencies [b9175e9]
46
+ - Updated dependencies [c8b762b]
47
+ - Updated dependencies [55e1e7f]
48
+ - Updated dependencies [f1ca82d]
49
+ - Updated dependencies [b87f27b]
50
+ - Updated dependencies [32ebead]
51
+ - Updated dependencies [bfefedd]
52
+ - Updated dependencies [c130173]
53
+ - Updated dependencies [11680b6]
54
+ - Updated dependencies [223d36c]
55
+ - Updated dependencies [7b91a44]
56
+ - Updated dependencies [aa04ead]
57
+ - Updated dependencies [ac65b5b]
58
+ - Updated dependencies [afcfef8]
59
+ - Updated dependencies [11ef1ad]
60
+ - @rcarls/rc-common@0.7.0
61
+
3
62
  ## 0.6.0
4
63
 
5
64
  ### Minor Changes
package/README.md CHANGED
@@ -32,6 +32,11 @@ scrollable at all (nothing more to reveal counts as already at both of its
32
32
  edges). Style an edge affordance (a fade, a shadow, a border) from outside
33
33
  the shadow root by targeting the attribute directly:
34
34
 
35
+ Initial boundary state settles in the first animation frame after mount.
36
+ This lets component batches complete their layout reads before any instance
37
+ reflect state; read the properties after that frame when coordinating
38
+ imperative startup work.
39
+
35
40
  ```css
36
41
  rc-scroller[axis='inline'] {
37
42
  mask-image: linear-gradient(
@@ -178,21 +178,81 @@
178
178
  "privacy": "private",
179
179
  "default": "null"
180
180
  },
181
+ {
182
+ "kind": "field",
183
+ "name": "_frame",
184
+ "privacy": "private",
185
+ "readonly": true,
186
+ "default": "new RafScheduler(this)"
187
+ },
181
188
  {
182
189
  "kind": "field",
183
190
  "name": "_onScroll",
184
191
  "privacy": "private",
185
- "readonly": true
192
+ "readonly": true,
193
+ "description": "Scroll and resize can fire several times per frame, and every evaluation\nreads layout, so boundary state is recomputed at most once per frame."
194
+ },
195
+ {
196
+ "kind": "method",
197
+ "name": "_edges",
198
+ "privacy": "private",
199
+ "return": {
200
+ "type": {
201
+ "text": "[start: boolean, end: boolean]"
202
+ }
203
+ },
204
+ "parameters": [
205
+ {
206
+ "name": "axis",
207
+ "type": {
208
+ "text": "LogicalAxis"
209
+ }
210
+ },
211
+ {
212
+ "name": "flow",
213
+ "type": {
214
+ "text": "Flow"
215
+ }
216
+ }
217
+ ]
186
218
  },
187
219
  {
188
220
  "kind": "method",
189
- "name": "_evaluateBoundaries",
221
+ "name": "_queueBoundaryEvaluation",
190
222
  "privacy": "private",
191
223
  "return": {
192
224
  "type": {
193
225
  "text": "void"
194
226
  }
195
227
  }
228
+ },
229
+ {
230
+ "kind": "method",
231
+ "name": "_sampleBoundaries",
232
+ "privacy": "private",
233
+ "return": {
234
+ "type": {
235
+ "text": "BoundaryState"
236
+ }
237
+ }
238
+ },
239
+ {
240
+ "kind": "method",
241
+ "name": "_applyBoundaries",
242
+ "privacy": "private",
243
+ "return": {
244
+ "type": {
245
+ "text": "void"
246
+ }
247
+ },
248
+ "parameters": [
249
+ {
250
+ "name": "state",
251
+ "type": {
252
+ "text": "BoundaryState"
253
+ }
254
+ }
255
+ ]
196
256
  }
197
257
  ],
198
258
  "attributes": [
@@ -0,0 +1,74 @@
1
+ import { css as b, LitElement as u, html as v } from "lit";
2
+ import { property as n } from "lit/decorators.js";
3
+ import { RafScheduler as p, getScrollOffset as f, scrollSize as m, clientSize as y, resolveFlow as k } from "@rcarls/rc-common";
4
+ const _ = b`:host{overflow-inline:hidden;overflow-block:auto;overscroll-behavior-block:var(--rc-scroller-overscroll-behavior,contain);overflow-anchor:var(--rc-scroller-overflow-anchor,auto);scrollbar-width:var(--rc-scroller-scrollbar-width,auto);scrollbar-color:var(--rc-scroller-scrollbar-color,auto);box-sizing:border-box;min-block-size:0;min-inline-size:0;display:block}:host([hidden]){display:none}:host([axis=inline]){overflow-inline:auto;overflow-block:hidden;overscroll-behavior-inline:var(--rc-scroller-overscroll-behavior,contain);overscroll-behavior-block:auto}:host([axis=both]){overflow-inline:auto;overflow-block:auto;overscroll-behavior:var(--rc-scroller-overscroll-behavior,contain)}[part=content]{box-sizing:border-box;min-block-size:0;min-inline-size:0;display:block}:host([layout=content]) [part=content]{grid-template-columns:[fullbleed-start] minmax(var(--rc-scroller-content-padding-inline,1rem), 1fr) [content-start] minmax(0, var(--rc-scroller-content-max-inline-size,100%)) [content-end] minmax(var(--rc-scroller-content-padding-inline,1rem), 1fr) [fullbleed-end];row-gap:var(--rc-scroller-content-row-gap,0);grid-auto-rows:min-content;display:grid}:host([layout=content]) ::slotted(*){grid-column:content;min-inline-size:0}:host([layout=content]) ::slotted([data-rc-scroller-span=fullbleed]){grid-column:fullbleed;inline-size:100%}`;
5
+ var B = Object.defineProperty, l = (c, e, r, i) => {
6
+ for (var t = void 0, s = c.length - 1, d; s >= 0; s--)
7
+ (d = c[s]) && (t = d(e, r, t) || t);
8
+ return t && B(e, r, t), t;
9
+ };
10
+ const h = 4, a = class a extends u {
11
+ constructor() {
12
+ super(...arguments), this.axis = "block", this.layout = "none", this.atBlockStart = !0, this.atBlockEnd = !0, this.atInlineStart = !0, this.atInlineEnd = !0, this._resizeObserver = null, this._frame = new p(this), this._onScroll = () => {
13
+ this._queueBoundaryEvaluation();
14
+ };
15
+ }
16
+ connectedCallback() {
17
+ super.connectedCallback(), this.addEventListener("scroll", this._onScroll, { passive: !0 }), typeof ResizeObserver == "function" && (this._resizeObserver = new ResizeObserver(this._onScroll), this._resizeObserver.observe(this));
18
+ }
19
+ firstUpdated() {
20
+ this._queueBoundaryEvaluation();
21
+ }
22
+ disconnectedCallback() {
23
+ this.removeEventListener("scroll", this._onScroll), this._resizeObserver?.disconnect(), this._resizeObserver = null, super.disconnectedCallback();
24
+ }
25
+ render() {
26
+ return v`<div part="content"><slot></slot></div>`;
27
+ }
28
+ updated(e) {
29
+ (e.has("axis") || e.has("layout")) && this._queueBoundaryEvaluation();
30
+ }
31
+ _edges(e, r) {
32
+ const i = f(this, e, r), t = m(this, e, r) - y(this, e, r);
33
+ return [i <= h, i >= t - h];
34
+ }
35
+ _queueBoundaryEvaluation() {
36
+ this._frame.schedulePhased(
37
+ () => this._sampleBoundaries(),
38
+ (e) => this._applyBoundaries(e)
39
+ );
40
+ }
41
+ _sampleBoundaries() {
42
+ const e = k(this), [r, i] = this.axis !== "inline" ? this._edges("block", e) : [!0, !0], [t, s] = this.axis !== "block" ? this._edges("inline", e) : [!0, !0];
43
+ return { blockStart: r, blockEnd: i, inlineStart: t, inlineEnd: s };
44
+ }
45
+ _applyBoundaries(e) {
46
+ this.atBlockStart = e.blockStart, this.atBlockEnd = e.blockEnd, this.atInlineStart = e.inlineStart, this.atInlineEnd = e.inlineEnd;
47
+ }
48
+ };
49
+ a.styles = _, a.enabledWarnings = (u.enabledWarnings ?? []).filter(
50
+ (e) => e !== "change-in-update"
51
+ );
52
+ let o = a;
53
+ l([
54
+ n({ reflect: !0 })
55
+ ], o.prototype, "axis");
56
+ l([
57
+ n({ reflect: !0 })
58
+ ], o.prototype, "layout");
59
+ l([
60
+ n({ type: Boolean, reflect: !0, attribute: "at-block-start" })
61
+ ], o.prototype, "atBlockStart");
62
+ l([
63
+ n({ type: Boolean, reflect: !0, attribute: "at-block-end" })
64
+ ], o.prototype, "atBlockEnd");
65
+ l([
66
+ n({ type: Boolean, reflect: !0, attribute: "at-inline-start" })
67
+ ], o.prototype, "atInlineStart");
68
+ l([
69
+ n({ type: Boolean, reflect: !0, attribute: "at-inline-end" })
70
+ ], o.prototype, "atInlineEnd");
71
+ export {
72
+ o as R
73
+ };
74
+ //# sourceMappingURL=rc-scroller-DY2mhq-F.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rc-scroller-DY2mhq-F.js","sources":["../src/rc-scroller.styles.ts","../src/rc-scroller.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const scrollerStyles = css`\n :host {\n display: block;\n min-inline-size: 0;\n min-block-size: 0;\n overflow-inline: hidden;\n overflow-block: auto;\n overscroll-behavior-block: var(--rc-scroller-overscroll-behavior, contain);\n overflow-anchor: var(--rc-scroller-overflow-anchor, auto);\n scrollbar-width: var(--rc-scroller-scrollbar-width, auto);\n scrollbar-color: var(--rc-scroller-scrollbar-color, auto);\n box-sizing: border-box;\n }\n\n :host([hidden]) {\n display: none;\n }\n\n :host([axis='inline']) {\n overflow-inline: auto;\n overflow-block: hidden;\n overscroll-behavior-inline: var(--rc-scroller-overscroll-behavior, contain);\n overscroll-behavior-block: auto;\n }\n\n :host([axis='both']) {\n overflow-inline: auto;\n overflow-block: auto;\n overscroll-behavior: var(--rc-scroller-overscroll-behavior, contain);\n }\n\n [part='content'] {\n display: block;\n min-inline-size: 0;\n min-block-size: 0;\n box-sizing: border-box;\n }\n\n :host([layout='content']) [part='content'] {\n display: grid;\n grid-template-columns:\n [fullbleed-start] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)\n [content-start] minmax(0, var(--rc-scroller-content-max-inline-size, 100%))\n [content-end] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)\n [fullbleed-end];\n grid-auto-rows: min-content;\n row-gap: var(--rc-scroller-content-row-gap, 0);\n }\n\n :host([layout='content']) ::slotted(*) {\n grid-column: content;\n min-inline-size: 0;\n }\n\n :host([layout='content']) ::slotted([data-rc-scroller-span='fullbleed']) {\n grid-column: fullbleed;\n inline-size: 100%;\n }\n`;\n\nexport default scrollerStyles;\n","import { LitElement, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport {\n RafScheduler,\n clientSize,\n getScrollOffset,\n resolveFlow,\n scrollSize,\n type Flow,\n type LogicalAxis,\n} from '@rcarls/rc-common';\n\nimport scrollerStyles from './rc-scroller.styles.js';\n\nexport type RCScrollerAxis = 'block' | 'inline' | 'both';\nexport type RCScrollerLayout = 'none' | 'content';\n\n/** Pixels of remaining scroll distance below which an edge counts as reached. */\nconst BOUNDARY_THRESHOLD = 4;\n\ninterface BoundaryState {\n blockStart: boolean;\n blockEnd: boolean;\n inlineStart: boolean;\n inlineEnd: boolean;\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-scroller': RCScroller;\n }\n}\n\n/**\n * Design-system-neutral native scroll region with optional content and\n * fullbleed layout tracks. The custom element host is the scroll container,\n * so native scrolling APIs and events work directly on `<rc-scroller>`.\n *\n * With `layout=\"content\"`, direct children use the centered content track.\n * Add `data-rc-scroller-span=\"fullbleed\"` to a direct child to span the full\n * available inline size. This child attribute is a layout convention rather\n * than component state.\n *\n * The author owns the accessible name and landmark semantics when the region\n * needs them. The component does not add `role` or `tabindex` automatically.\n *\n * @slot - Scrollable content.\n *\n * @csspart content - Wrapper for the default slot and optional layout grid.\n *\n * @attr axis - Scroll axis: `block`, `inline`, or `both`.\n * @attr layout - Layout mode: `none` or `content`.\n * @attr [at-block-start] - Reflected, computed. Present when scrolled to (within a few\n * pixels of) the block-start edge, or when the block axis isn't scrollable\n * (`axis=\"inline\"`) — nothing more to reveal counts as already at both edges.\n * Edges are logical: in RTL the inline start is the right edge, and in\n * vertical writing modes the block axis runs horizontally.\n * @attr [at-block-end] - Reflected, computed. The block-end equivalent of `at-block-start`.\n * @attr [at-inline-start] - Reflected, computed. The inline-start equivalent, always\n * present when the inline axis isn't scrollable (`axis=\"block\"`).\n * @attr [at-inline-end] - Reflected, computed. The inline-end equivalent of `at-inline-start`.\n * Initial boundary attributes settle in the first animation frame after\n * mount so multiple scrollers can share one layout sampling phase.\n *\n * @cssprop [--rc-scroller-overscroll-behavior=contain] - Overscroll behavior on enabled axes.\n * @cssprop [--rc-scroller-overflow-anchor=auto] - Browser scroll anchoring behavior.\n * @cssprop [--rc-scroller-scrollbar-width=auto] - Scrollbar width.\n * @cssprop [--rc-scroller-scrollbar-color=auto] - Scrollbar thumb and track colors.\n * @cssprop [--rc-scroller-content-padding-inline=1rem] - Minimum inline gutter in content layout.\n * @cssprop [--rc-scroller-content-max-inline-size=100%] - Maximum width of the content track.\n * @cssprop [--rc-scroller-content-row-gap=0] - Gap between rows in content layout.\n */\nexport class RCScroller extends LitElement {\n static override styles = scrollerStyles;\n\n // Boundary state can only be measured after render, so the reflected edge\n // properties intentionally schedule one follow-up update.\n static override enabledWarnings = (LitElement.enabledWarnings ?? []).filter(\n (warning) => warning !== 'change-in-update',\n );\n\n /** Scroll axis. */\n @property({ reflect: true })\n axis: RCScrollerAxis = 'block';\n\n /** Optional child layout. */\n @property({ reflect: true })\n layout: RCScrollerLayout = 'none';\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-block-start' })\n atBlockStart = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-block-end' })\n atBlockEnd = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-inline-start' })\n atInlineStart = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-inline-end' })\n atInlineEnd = true;\n\n private _resizeObserver: ResizeObserver | null = null;\n\n private readonly _frame = new RafScheduler(this);\n\n override connectedCallback(): void {\n super.connectedCallback();\n\n this.addEventListener('scroll', this._onScroll, { passive: true });\n\n if (typeof ResizeObserver === 'function') {\n this._resizeObserver = new ResizeObserver(this._onScroll);\n this._resizeObserver.observe(this);\n }\n }\n\n override firstUpdated(): void {\n // Only accurate once the shadow DOM (and slotted content) has actually\n // rendered — connectedCallback runs before Lit's first render, when\n // scrollWidth/clientHeight etc. don't yet reflect real layout. Also\n // covers connecting already scrolled (bfcache restore, fragment nav)\n // rather than waiting for the first scroll/resize event.\n this._queueBoundaryEvaluation();\n }\n\n override disconnectedCallback(): void {\n this.removeEventListener('scroll', this._onScroll);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n\n super.disconnectedCallback();\n }\n\n protected override render() {\n return html`<div part=\"content\"><slot></slot></div>`;\n }\n\n protected override updated(changed: Map<PropertyKey, unknown>): void {\n if (changed.has('axis') || changed.has('layout')) {\n this._queueBoundaryEvaluation();\n }\n }\n\n /**\n * Scroll and resize can fire several times per frame, and every evaluation\n * reads layout, so boundary state is recomputed at most once per frame.\n */\n private readonly _onScroll = (): void => {\n this._queueBoundaryEvaluation();\n };\n\n private _edges(axis: LogicalAxis, flow: Flow): [start: boolean, end: boolean] {\n const offset = getScrollOffset(this, axis, flow);\n const max = scrollSize(this, axis, flow) - clientSize(this, axis, flow);\n\n return [offset <= BOUNDARY_THRESHOLD, offset >= max - BOUNDARY_THRESHOLD];\n }\n\n private _queueBoundaryEvaluation(): void {\n this._frame.schedulePhased(\n () => this._sampleBoundaries(),\n (state) => this._applyBoundaries(state),\n );\n }\n\n private _sampleBoundaries(): BoundaryState {\n // Read at measure time: a dir or writing-mode change on an ancestor fires\n // no event of its own.\n const flow = resolveFlow(this);\n const [blockStart, blockEnd] =\n this.axis !== 'inline' ? this._edges('block', flow) : [true, true];\n const [inlineStart, inlineEnd] =\n this.axis !== 'block' ? this._edges('inline', flow) : [true, true];\n\n return { blockStart, blockEnd, inlineStart, inlineEnd };\n }\n\n private _applyBoundaries(state: BoundaryState): void {\n this.atBlockStart = state.blockStart;\n this.atBlockEnd = state.blockEnd;\n this.atInlineStart = state.inlineStart;\n this.atInlineEnd = state.inlineEnd;\n }\n}\n"],"names":["scrollerStyles","css","BOUNDARY_THRESHOLD","_RCScroller","LitElement","RafScheduler","html","changed","axis","flow","offset","getScrollOffset","max","scrollSize","clientSize","state","resolveFlow","blockStart","blockEnd","inlineStart","inlineEnd","warning","RCScroller","__decorateClass","property"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { R as e } from "./rc-scroller-BvfVYwVD.js";
1
+ import { R as e } from "./rc-scroller-DY2mhq-F.js";
2
2
  customElements.get("rc-scroller") || customElements.define("rc-scroller", e);
3
3
  export {
4
4
  e as RCScroller
@@ -1,4 +1,4 @@
1
- import { R as e } from "./rc-scroller-BvfVYwVD.js";
1
+ import { R as e } from "./rc-scroller-DY2mhq-F.js";
2
2
  export {
3
3
  e as RCScroller
4
4
  };
@@ -28,10 +28,14 @@ declare global {
28
28
  * @attr [at-block-start] - Reflected, computed. Present when scrolled to (within a few
29
29
  * pixels of) the block-start edge, or when the block axis isn't scrollable
30
30
  * (`axis="inline"`) — nothing more to reveal counts as already at both edges.
31
+ * Edges are logical: in RTL the inline start is the right edge, and in
32
+ * vertical writing modes the block axis runs horizontally.
31
33
  * @attr [at-block-end] - Reflected, computed. The block-end equivalent of `at-block-start`.
32
34
  * @attr [at-inline-start] - Reflected, computed. The inline-start equivalent, always
33
35
  * present when the inline axis isn't scrollable (`axis="block"`).
34
36
  * @attr [at-inline-end] - Reflected, computed. The inline-end equivalent of `at-inline-start`.
37
+ * Initial boundary attributes settle in the first animation frame after
38
+ * mount so multiple scrollers can share one layout sampling phase.
35
39
  *
36
40
  * @cssprop [--rc-scroller-overscroll-behavior=contain] - Overscroll behavior on enabled axes.
37
41
  * @cssprop [--rc-scroller-overflow-anchor=auto] - Browser scroll anchoring behavior.
@@ -57,11 +61,19 @@ export declare class RCScroller extends LitElement {
57
61
  /** Reflected, computed. See the class doc comment. */
58
62
  atInlineEnd: boolean;
59
63
  private _resizeObserver;
64
+ private readonly _frame;
60
65
  connectedCallback(): void;
61
66
  firstUpdated(): void;
62
67
  disconnectedCallback(): void;
63
- protected render(): import('lit').TemplateResult<1>;
68
+ protected render(): import('lit-html').TemplateResult<1>;
64
69
  protected updated(changed: Map<PropertyKey, unknown>): void;
70
+ /**
71
+ * Scroll and resize can fire several times per frame, and every evaluation
72
+ * reads layout, so boundary state is recomputed at most once per frame.
73
+ */
65
74
  private readonly _onScroll;
66
- private _evaluateBoundaries;
75
+ private _edges;
76
+ private _queueBoundaryEvaluation;
77
+ private _sampleBoundaries;
78
+ private _applyBoundaries;
67
79
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.0",
6
+ "version": "0.7.0",
7
7
  "description": "Design-system-neutral native scroll region with optional content and fullbleed layout tracks.",
8
8
  "repository": {
9
9
  "type": "git",
@@ -44,6 +44,9 @@
44
44
  "test:browser:chrome": "vitest --run --project=chromium",
45
45
  "test:browser:firefox": "vitest --run --project=firefox"
46
46
  },
47
+ "dependencies": {
48
+ "@rcarls/rc-common": "0.7.0"
49
+ },
47
50
  "devDependencies": {
48
51
  "@custom-elements-manifest/analyzer": "0.11.0",
49
52
  "@vitest/browser-playwright": "4.1.5",
@@ -1,125 +0,0 @@
1
- import { css as b, LitElement as u, html as v } from "lit";
2
- import { property as o } from "lit/decorators.js";
3
- const p = b`
4
- :host {
5
- display: block;
6
- min-inline-size: 0;
7
- min-block-size: 0;
8
- overflow-inline: hidden;
9
- overflow-block: auto;
10
- overscroll-behavior-block: var(--rc-scroller-overscroll-behavior, contain);
11
- overflow-anchor: var(--rc-scroller-overflow-anchor, auto);
12
- scrollbar-width: var(--rc-scroller-scrollbar-width, auto);
13
- scrollbar-color: var(--rc-scroller-scrollbar-color, auto);
14
- box-sizing: border-box;
15
- }
16
-
17
- :host([hidden]) {
18
- display: none;
19
- }
20
-
21
- :host([axis='inline']) {
22
- overflow-inline: auto;
23
- overflow-block: hidden;
24
- overscroll-behavior-inline: var(--rc-scroller-overscroll-behavior, contain);
25
- overscroll-behavior-block: auto;
26
- }
27
-
28
- :host([axis='both']) {
29
- overflow-inline: auto;
30
- overflow-block: auto;
31
- overscroll-behavior: var(--rc-scroller-overscroll-behavior, contain);
32
- }
33
-
34
- [part='content'] {
35
- display: block;
36
- min-inline-size: 0;
37
- min-block-size: 0;
38
- box-sizing: border-box;
39
- }
40
-
41
- :host([layout='content']) [part='content'] {
42
- display: grid;
43
- grid-template-columns:
44
- [fullbleed-start] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)
45
- [content-start] minmax(0, var(--rc-scroller-content-max-inline-size, 100%))
46
- [content-end] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)
47
- [fullbleed-end];
48
- grid-auto-rows: min-content;
49
- row-gap: var(--rc-scroller-content-row-gap, 0);
50
- }
51
-
52
- :host([layout='content']) ::slotted(*) {
53
- grid-column: content;
54
- min-inline-size: 0;
55
- }
56
-
57
- :host([layout='content']) ::slotted([data-rc-scroller-span='fullbleed']) {
58
- grid-column: fullbleed;
59
- inline-size: 100%;
60
- }
61
- `;
62
- var f = Object.defineProperty, l = (h, t, i, n) => {
63
- for (var e = void 0, c = h.length - 1, d; c >= 0; c--)
64
- (d = h[c]) && (e = d(t, i, e) || e);
65
- return e && f(t, i, e), e;
66
- };
67
- const s = 4, a = class a extends u {
68
- constructor() {
69
- super(...arguments), this.axis = "block", this.layout = "none", this.atBlockStart = !0, this.atBlockEnd = !0, this.atInlineStart = !0, this.atInlineEnd = !0, this._resizeObserver = null, this._onScroll = () => this._evaluateBoundaries();
70
- }
71
- connectedCallback() {
72
- super.connectedCallback(), this.addEventListener("scroll", this._onScroll, { passive: !0 }), typeof ResizeObserver == "function" && (this._resizeObserver = new ResizeObserver(this._onScroll), this._resizeObserver.observe(this));
73
- }
74
- firstUpdated() {
75
- this._evaluateBoundaries();
76
- }
77
- disconnectedCallback() {
78
- this.removeEventListener("scroll", this._onScroll), this._resizeObserver?.disconnect(), this._resizeObserver = null, super.disconnectedCallback();
79
- }
80
- render() {
81
- return v`<div part="content"><slot></slot></div>`;
82
- }
83
- updated(t) {
84
- (t.has("axis") || t.has("layout")) && this._evaluateBoundaries();
85
- }
86
- _evaluateBoundaries() {
87
- const t = this.axis !== "inline", i = this.axis !== "block";
88
- if (t) {
89
- const n = this.scrollHeight - this.clientHeight;
90
- this.atBlockStart = this.scrollTop <= s, this.atBlockEnd = this.scrollTop >= n - s;
91
- } else
92
- this.atBlockStart = !0, this.atBlockEnd = !0;
93
- if (i) {
94
- const n = this.scrollWidth - this.clientWidth, e = getComputedStyle(this).direction === "rtl" ? Math.abs(this.scrollLeft) : this.scrollLeft;
95
- this.atInlineStart = e <= s, this.atInlineEnd = e >= n - s;
96
- } else
97
- this.atInlineStart = !0, this.atInlineEnd = !0;
98
- }
99
- };
100
- a.styles = p, a.enabledWarnings = (u.enabledWarnings ?? []).filter(
101
- (t) => t !== "change-in-update"
102
- );
103
- let r = a;
104
- l([
105
- o({ reflect: !0 })
106
- ], r.prototype, "axis");
107
- l([
108
- o({ reflect: !0 })
109
- ], r.prototype, "layout");
110
- l([
111
- o({ type: Boolean, reflect: !0, attribute: "at-block-start" })
112
- ], r.prototype, "atBlockStart");
113
- l([
114
- o({ type: Boolean, reflect: !0, attribute: "at-block-end" })
115
- ], r.prototype, "atBlockEnd");
116
- l([
117
- o({ type: Boolean, reflect: !0, attribute: "at-inline-start" })
118
- ], r.prototype, "atInlineStart");
119
- l([
120
- o({ type: Boolean, reflect: !0, attribute: "at-inline-end" })
121
- ], r.prototype, "atInlineEnd");
122
- export {
123
- r as R
124
- };
125
- //# sourceMappingURL=rc-scroller-BvfVYwVD.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rc-scroller-BvfVYwVD.js","sources":["../src/rc-scroller.styles.ts","../src/rc-scroller.ts"],"sourcesContent":["import { css } from 'lit';\n\nexport const scrollerStyles = css`\n :host {\n display: block;\n min-inline-size: 0;\n min-block-size: 0;\n overflow-inline: hidden;\n overflow-block: auto;\n overscroll-behavior-block: var(--rc-scroller-overscroll-behavior, contain);\n overflow-anchor: var(--rc-scroller-overflow-anchor, auto);\n scrollbar-width: var(--rc-scroller-scrollbar-width, auto);\n scrollbar-color: var(--rc-scroller-scrollbar-color, auto);\n box-sizing: border-box;\n }\n\n :host([hidden]) {\n display: none;\n }\n\n :host([axis='inline']) {\n overflow-inline: auto;\n overflow-block: hidden;\n overscroll-behavior-inline: var(--rc-scroller-overscroll-behavior, contain);\n overscroll-behavior-block: auto;\n }\n\n :host([axis='both']) {\n overflow-inline: auto;\n overflow-block: auto;\n overscroll-behavior: var(--rc-scroller-overscroll-behavior, contain);\n }\n\n [part='content'] {\n display: block;\n min-inline-size: 0;\n min-block-size: 0;\n box-sizing: border-box;\n }\n\n :host([layout='content']) [part='content'] {\n display: grid;\n grid-template-columns:\n [fullbleed-start] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)\n [content-start] minmax(0, var(--rc-scroller-content-max-inline-size, 100%))\n [content-end] minmax(var(--rc-scroller-content-padding-inline, 1rem), 1fr)\n [fullbleed-end];\n grid-auto-rows: min-content;\n row-gap: var(--rc-scroller-content-row-gap, 0);\n }\n\n :host([layout='content']) ::slotted(*) {\n grid-column: content;\n min-inline-size: 0;\n }\n\n :host([layout='content']) ::slotted([data-rc-scroller-span='fullbleed']) {\n grid-column: fullbleed;\n inline-size: 100%;\n }\n`;\n\nexport default scrollerStyles;\n","import { LitElement, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nimport scrollerStyles from './rc-scroller.styles.js';\n\nexport type RCScrollerAxis = 'block' | 'inline' | 'both';\nexport type RCScrollerLayout = 'none' | 'content';\n\n/** Pixels of remaining scroll distance below which an edge counts as reached. */\nconst BOUNDARY_THRESHOLD = 4;\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'rc-scroller': RCScroller;\n }\n}\n\n/**\n * Design-system-neutral native scroll region with optional content and\n * fullbleed layout tracks. The custom element host is the scroll container,\n * so native scrolling APIs and events work directly on `<rc-scroller>`.\n *\n * With `layout=\"content\"`, direct children use the centered content track.\n * Add `data-rc-scroller-span=\"fullbleed\"` to a direct child to span the full\n * available inline size. This child attribute is a layout convention rather\n * than component state.\n *\n * The author owns the accessible name and landmark semantics when the region\n * needs them. The component does not add `role` or `tabindex` automatically.\n *\n * @slot - Scrollable content.\n *\n * @csspart content - Wrapper for the default slot and optional layout grid.\n *\n * @attr axis - Scroll axis: `block`, `inline`, or `both`.\n * @attr layout - Layout mode: `none` or `content`.\n * @attr [at-block-start] - Reflected, computed. Present when scrolled to (within a few\n * pixels of) the block-start edge, or when the block axis isn't scrollable\n * (`axis=\"inline\"`) — nothing more to reveal counts as already at both edges.\n * @attr [at-block-end] - Reflected, computed. The block-end equivalent of `at-block-start`.\n * @attr [at-inline-start] - Reflected, computed. The inline-start equivalent, always\n * present when the inline axis isn't scrollable (`axis=\"block\"`).\n * @attr [at-inline-end] - Reflected, computed. The inline-end equivalent of `at-inline-start`.\n *\n * @cssprop [--rc-scroller-overscroll-behavior=contain] - Overscroll behavior on enabled axes.\n * @cssprop [--rc-scroller-overflow-anchor=auto] - Browser scroll anchoring behavior.\n * @cssprop [--rc-scroller-scrollbar-width=auto] - Scrollbar width.\n * @cssprop [--rc-scroller-scrollbar-color=auto] - Scrollbar thumb and track colors.\n * @cssprop [--rc-scroller-content-padding-inline=1rem] - Minimum inline gutter in content layout.\n * @cssprop [--rc-scroller-content-max-inline-size=100%] - Maximum width of the content track.\n * @cssprop [--rc-scroller-content-row-gap=0] - Gap between rows in content layout.\n */\nexport class RCScroller extends LitElement {\n static override styles = scrollerStyles;\n\n // Boundary state can only be measured after render, so the reflected edge\n // properties intentionally schedule one follow-up update.\n static override enabledWarnings = (LitElement.enabledWarnings ?? []).filter(\n (warning) => warning !== 'change-in-update',\n );\n\n /** Scroll axis. */\n @property({ reflect: true })\n axis: RCScrollerAxis = 'block';\n\n /** Optional child layout. */\n @property({ reflect: true })\n layout: RCScrollerLayout = 'none';\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-block-start' })\n atBlockStart = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-block-end' })\n atBlockEnd = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-inline-start' })\n atInlineStart = true;\n\n /** Reflected, computed. See the class doc comment. */\n @property({ type: Boolean, reflect: true, attribute: 'at-inline-end' })\n atInlineEnd = true;\n\n private _resizeObserver: ResizeObserver | null = null;\n\n override connectedCallback(): void {\n super.connectedCallback();\n\n this.addEventListener('scroll', this._onScroll, { passive: true });\n\n if (typeof ResizeObserver === 'function') {\n this._resizeObserver = new ResizeObserver(this._onScroll);\n this._resizeObserver.observe(this);\n }\n }\n\n override firstUpdated(): void {\n // Only accurate once the shadow DOM (and slotted content) has actually\n // rendered — connectedCallback runs before Lit's first render, when\n // scrollWidth/clientHeight etc. don't yet reflect real layout. Also\n // covers connecting already scrolled (bfcache restore, fragment nav)\n // rather than waiting for the first scroll/resize event.\n this._evaluateBoundaries();\n }\n\n override disconnectedCallback(): void {\n this.removeEventListener('scroll', this._onScroll);\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n\n super.disconnectedCallback();\n }\n\n protected override render() {\n return html`<div part=\"content\"><slot></slot></div>`;\n }\n\n protected override updated(changed: Map<PropertyKey, unknown>): void {\n if (changed.has('axis') || changed.has('layout')) {\n this._evaluateBoundaries();\n }\n }\n\n private readonly _onScroll = (): void => this._evaluateBoundaries();\n\n private _evaluateBoundaries(): void {\n const blockScrollable = this.axis !== 'inline';\n const inlineScrollable = this.axis !== 'block';\n\n if (blockScrollable) {\n const maxScrollTop = this.scrollHeight - this.clientHeight;\n\n this.atBlockStart = this.scrollTop <= BOUNDARY_THRESHOLD;\n this.atBlockEnd = this.scrollTop >= maxScrollTop - BOUNDARY_THRESHOLD;\n } else {\n this.atBlockStart = true;\n this.atBlockEnd = true;\n }\n\n if (inlineScrollable) {\n const maxScrollLeft = this.scrollWidth - this.clientWidth;\n const inlineOffset =\n getComputedStyle(this).direction === 'rtl' ? Math.abs(this.scrollLeft) : this.scrollLeft;\n\n this.atInlineStart = inlineOffset <= BOUNDARY_THRESHOLD;\n this.atInlineEnd = inlineOffset >= maxScrollLeft - BOUNDARY_THRESHOLD;\n } else {\n this.atInlineStart = true;\n this.atInlineEnd = true;\n }\n }\n}\n"],"names":["scrollerStyles","css","BOUNDARY_THRESHOLD","_RCScroller","LitElement","html","changed","blockScrollable","inlineScrollable","maxScrollTop","maxScrollLeft","inlineOffset","warning","RCScroller","__decorateClass","property"],"mappings":"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"}