@excom/gesture-handler 0.1.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/.rush/temp/chunked-rush-logs/gesture-handler.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/gesture-handler.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/gesture-handler.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/gesture-handler.ts +1211 -0
- package/index.css +5 -0
- package/index.ts +29 -0
- package/package.json +52 -0
- package/rush-logs/gesture-handler.apply-exports.cache.log +1 -0
- package/rush-logs/gesture-handler.apply-exports.log +1 -0
- package/rush-logs/gesture-handler.build_docs.cache.log +1 -0
- package/rush-logs/gesture-handler.build_docs.log +1 -0
- package/rush-logs/gesture-handler.build_package-metas.cache.log +1 -0
- package/rush-logs/gesture-handler.build_package-metas.log +1 -0
- package/src/gesture-handler.css +207 -0
- package/support/custom-elements.json +989 -0
- package/support/demos/carousel.html +44 -0
- package/support/demos/pinch.html +10 -0
- package/support/demos/sheet.html +50 -0
- package/support/demos/swipe.html +24 -0
- package/support/dist-docs/gesture-handler.md +339 -0
- package/support/docs/README.md +92 -0
- package/support/package-meta.json +489 -0
- package/support/tests/carousel.view.test.ts +67 -0
- package/support/tests/gesture-handler.test.ts +625 -0
- package/support/tests/pointer-utils.ts +62 -0
- package/support/tests/sheet.view.test.ts +112 -0
- package/support/tests/swipe.view.test.ts +44 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
{
|
|
2
|
+
"shortName": "gesture-handler",
|
|
3
|
+
"package": {
|
|
4
|
+
"name": "@excom/gesture-handler",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "<gesture-handler> custom element",
|
|
7
|
+
"peerDependencies": {},
|
|
8
|
+
"excom": {
|
|
9
|
+
"packageType": "kit-element"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"demos": {
|
|
13
|
+
"carousel": "<gesture-handler gesture-types=\"pan-x swipe\" pointer-types=\"touch pen mouse\" progress-axis=\"left\"\n range-ref=\":scope > content-carousel\" progress-min=\"-1\" progress-max=\"1\" snap-points=\"-1 0 1\"\n overshoot-resistance=\"0.2\">\n <quark-sheet>\n :scope {\n /* no wrap-around while dragging: clamp the ends */\n &:has(content-carousel-slide[is-active]:first-of-type) { progress-min: 0; }\n &:not(:has(content-carousel-slide[is-active]:first-of-type)) { progress-min: -1; }\n &:has(content-carousel-slide[is-active]:last-of-type) { progress-max: 0; }\n &:not(:has(content-carousel-slide[is-active]:last-of-type)) { progress-max: 1; }\n @on gesture-handler-start {\n content-carousel { is-scrubbing: \"\"; }\n }\n /* commit once the glide lands: the slide swap and the un-scrub paint together */\n @on gesture-handler-snap {\n /* snap-points \"-1 0 1\" → index 0 = previous, 1 = stay, 2 = next */\n $snap: event.detail.index;\n content-carousel {\n is-scrubbing: none;\n content-carousel-slide[is-active] + content-carousel-slide {\n is-active: $snap == 2 or preserve;\n }\n content-carousel-slide:has(+ content-carousel-slide[is-active]) {\n is-active: $snap == 0 or preserve;\n }\n content-carousel-slide[is-active] {\n is-active: $snap == 1 or none;\n }\n }\n }\n }\n </quark-sheet>\n <content-carousel slide-animation=\"track\">\n <content-carousel-slide is-active>\n <h2>One</h2>\n </content-carousel-slide>\n <content-carousel-slide>\n <h2>Two</h2>\n </content-carousel-slide>\n <content-carousel-slide>\n <h2>Three</h2>\n </content-carousel-slide>\n </content-carousel>\n</gesture-handler>",
|
|
14
|
+
"pinch": "<gesture-handler gesture-types=\"pan pinch rotate\" pointer-types=\"touch pen mouse\">\n <figure>Drag, pinch, twist</figure>\n <style>\n #demo-gesture-handler-pinch > :first-child figure {\n translate: var(--gesture-dx, 0px) var(--gesture-dy, 0px);\n scale: var(--gesture-scale, 1);\n rotate: var(--gesture-rotate, 0deg);\n }\n </style>\n</gesture-handler>\n",
|
|
15
|
+
"sheet": "<gesture-handler gesture-types=\"pan-y swipe\" pointer-types=\"touch pen mouse\" from-ref=\":scope > content-drawer > header\"\n handoff-ref=\":scope > content-drawer\" progress-axis=\"up\" range-ref=\":scope > content-drawer\" snap-points=\"0 1\">\n <quark-sheet>\n :scope {\n /* dragging an open sheet starts from fully open */\n &:has(> content-drawer[is-open]) {\n progress-offset: 1;\n }\n &:not(:has(> content-drawer[is-open])) {\n progress-offset: 0;\n }\n @on gesture-handler-start {\n content-drawer { is-scrubbing: \"\"; }\n }\n @on gesture-handler-end {\n content-drawer { is-open: event.detail.snap == 1; is-scrubbing: none; }\n }\n }\n </quark-sheet>\n <button type=\"button\" command=\"--open\" commandfor=\"demo-gesture-sheet\">Open sheet</button>\n <content-drawer id=\"demo-gesture-sheet\" class=\"absolute\">\n <header><h2>Drag me</h2></header>\n <p>Two ways to dismiss it. Drag the header down and let go, or flick it —\n that is the <code>from-ref</code> grab handle.</p>\n <p>Or pull the text itself down: <code>handoff-ref</code> lets the sheet's\n own content scroll first, and only once it is back at the top does the\n next pull down become the same drag. Pulling up, or pulling down while\n the text is scrolled, keeps scrolling as usual.</p>\n <p>Scroll on. The sheet only takes the drag over at the very top, which is\n how a native sheet behaves.</p>\n <p>Keep going.</p>\n <p>Nearly there.</p>\n <p>Now scroll back up and pull once more — the sheet follows your finger\n and snaps to <code>0</code> or <code>1</code> on release.</p>\n </content-drawer>\n <style>\n #demo-gesture-handler-sheet > :first-child {\n /* short enough that the text scrolls: the handoff needs a scroller */\n content-drawer { max-block-size: 11rem; }\n /* the drag handle owns its touches; the content keeps scrolling */\n content-drawer > header {\n position: sticky;\n top: 0;\n touch-action: none;\n cursor: grab;\n background-color: inherit;\n }\n }\n </style>\n</gesture-handler>\n",
|
|
16
|
+
"swipe": "<gesture-handler gesture-types=\"pan-x swipe tap\" pointer-types=\"touch pen mouse\" range-px=\"240\" progress-min=\"-1\" progress-max=\"1\"\n snap-points=\"-1 0 1\" overshoot-resistance=\"0.4\">\n <quark-sheet>\n :scope {\n @on gesture-handler-snap {\n article { data-is-dismissed: event.detail.value != 0; }\n }\n @on gesture-handler-tap {\n article { data-is-dismissed: none; }\n }\n }\n </quark-sheet>\n <article>\n <h2>Swipe me away</h2>\n <p>Flick left or right. Tap to bring it back.</p>\n </article>\n <style>\n #demo-gesture-handler-swipe > :first-child article {\n translate: calc(var(--gesture-progress, 0) * 240px);\n rotate: calc(var(--gesture-progress, 0) * 8deg);\n &[data-is-dismissed] { opacity: 0.25; }\n }\n </style>\n</gesture-handler>\n"
|
|
17
|
+
},
|
|
18
|
+
"readme": "<h1 id=\"md-gesture-handler\">gesture-handler</h1>\n<p>Let users drag, swipe, pinch and flick your UI — sheets, carousels, cards and images follow the finger, in CSS, with no per-frame script.</p>\n<p><include-content data-demo=\"sheet\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Swipe / pan / pinch / rotate / tap / long-press</strong> Each recognized gesture is a tag-prefixed event</li>\n<li><strong>Follow the finger in CSS</strong> Every frame lands in <code>--gesture-*</code> custom properties: scrubbable bottom sheets, swipe-to-dismiss, pinch-to-zoom, pull-to-refresh, parallax</li>\n<li><strong>Snap & fling</strong> <code>snap-points</code> with velocity projection, a CSS transition on release (<code>--gesture-snap-duration</code> / <code>--gesture-snap-ease</code>), <code>gesture-handler-snap</code> when it lands</li>\n<li><strong>Drivable</strong> Bounds, <code>progress-offset</code> and <code>is-disabled</code> are attributes a Quark rule sets from the driven element's state</li>\n<li><strong>Scoped starts</strong> <code>from-ref</code> for drag handles, <code>from-edge</code> for edge swipes</li>\n<li><strong>Scroll handoff</strong> <code>handoff-ref</code> lets a sheet's own scrolling content take the drag over when it runs out of scroll — a native-feeling pull-to-close</li>\n<li><strong>Native scrolling kept</strong> <code>touch-action</code> follows <code>gesture-types</code>, so the page still scrolls where you don't pan</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Wrap the surface the user touches. Pick the gestures with <code>gesture-types</code>; read the finger from <code>--gesture-*</code> in CSS (custom properties inherit, so any descendant can <code>var()</code> them); commit State on <code>gesture-handler-end</code> or <code>-snap</code> from a Quark <code>@on</code> block.</p>\n<include-content data-language=\"html\"><template><gesture-handler gesture-types=\"pan-y swipe\" progress-axis=\"up\" range-ref=\":scope > content-drawer\" snap-points=\"0 1\">\n <quark-sheet>\n :scope {\n @on gesture-handler-start { content-drawer { is-scrubbing: \"\"; } }\n @on gesture-handler-end { content-drawer { is-open: event.detail.snap == 1; is-scrubbing: none; } }\n }\n </quark-sheet>\n <content-drawer>…</content-drawer>\n</gesture-handler></template></include-content>\n<p><code>--gesture-progress</code> is the travel along <code>progress-axis</code> as a fraction of the range (<code>range-ref</code> measures the driven element; <code>range-px</code> is a literal), clamped to <code>progress-min</code>..<code>progress-max</code> with optional <code>overshoot-resistance</code>. Written alongside it every frame: <code>--gesture-dx</code> / <code>-dy</code>, <code>--gesture-x</code> / <code>-y</code>, <code>--gesture-scale</code>, <code>--gesture-rotate</code>, <code>--gesture-vx</code> / <code>-vy</code>, <code>--gesture-pointers</code>; once per gesture: <code>--gesture-range-px</code>, <code>--gesture-width</code> / <code>-height</code>. Everything else is derived from those in the element's own CSS — <code>--gesture-distance</code>, <code>--gesture-angle</code>, <code>--gesture-progress-px</code>, <code>--gesture-x-ratio</code> / <code>-y-ratio</code> — so <code>var()</code> them the same way.</p>\n<p>Values persist after release until the next gesture starts. On release the default action writes <code>--gesture-progress</code> straight to <code>detail.snap</code> and the element's own <code>transition</code> settles it there over <code>--gesture-snap-duration</code> (<code>200ms</code>) with <code>--gesture-snap-ease</code> (<code>ease-out</code>), firing <code>gesture-handler-snap</code> when it lands; the transition is off while <code>is-active</code>, so the finger itself is never eased, and a new gesture that interrupts the settle simply cancels it (no <code>-snap</code>).</p>\n<h3 id=\"md-driving-nucleus-kit-elements\">Driving Nucleus Kit elements</h3>\n<p>Elements that can be scrubbed expose a <code>--<tag>-…-progress</code> input and an <code>is-scrubbing</code> attribute; both default to the wrapping gesture-handler's <code>--gesture-progress</code>, so no mapping is needed:</p>\n<table>\n<thead>\n<tr>\n<th>Element</th>\n<th>Set up</th>\n<th>While <code>is-scrubbing</code></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>content-drawer</code></td>\n<td><code>progress-axis</code> towards its open side, <code>range-ref</code> the drawer, <code>handoff-ref</code> the drawer too (it scrolls its own content)</td>\n<td>Position follows <code>--content-drawer-open-progress</code> (<code>0</code> closed, <code>1</code> open), no transition</td>\n</tr>\n<tr>\n<td><code>content-carousel</code></td>\n<td><code>slide-animation="track"</code>, <code>progress-min="-1" progress-max="1" snap-points="-1 0 1"</code></td>\n<td>The track follows <code>--content-carousel-progress</code> in slide widths</td>\n</tr>\n</tbody></table>\n<p>The handoff is one Quark commit: the block that writes the final state (<code>is-open</code>, the active slide) also removes <code>is-scrubbing</code>, so the element switches from finger to State in the same paint. Commit on <code>-end</code> when a CSS transition should finish the motion (the drawer), on <code>-snap</code> when the element must be exactly at the snap point first (the carousel).</p>\n<p>Anything else follows the same recipe: read <code>--gesture-*</code> in your own CSS, gate the mapping on a fact your sheet writes on start and clears on end.</p>\n<p>Give a drag handle <code>touch-action: none</code> when using <code>from-ref</code>, so the browser does not scroll it away; without <code>from-ref</code> the element sets <code>touch-action</code> itself from <code>gesture-types</code>. Add <code>mouse</code> to <code>pointer-types</code> for desktop dragging.</p>\n<h3 id=\"md-scroll-handoff\">Scroll handoff</h3>\n<p><code>handoff-ref</code> names the scroll container(s) inside the surface whose <em>overscroll</em> starts a gesture (a <code>:scope</code>-relative selector; a comma list matches several). A pointer that goes down in one of them scrolls natively as usual. Only when its <strong>first</strong> move runs along <code>progress-axis</code>, the container is at its scroll limit that way, and <code>progress-offset</code> still has room to travel in that direction does the element cancel the native scroll for the rest of the touch and take the drag over — the same gesture, the same <code>--gesture-*</code> values and the same <code>-start</code> / <code>-end</code> / <code>-snap</code> events as a drag from a handle:</p>\n<include-content data-language=\"html\"><template><gesture-handler gesture-types=\"pan-y swipe\" progress-axis=\"up\" snap-points=\"0 1\"\n from-ref=\":scope > content-drawer > header\" handoff-ref=\":scope > content-drawer\"\n range-ref=\":scope > content-drawer\"></template></include-content>\n<p>An open sheet (<code>progress-offset: 1</code>) closes either from its header or by pulling its text down once the text is back at the top; pulling up, or pulling down mid-scroll, keeps scrolling. <code>handoff-ref</code> is additive — <code>from-ref</code> and <code>from-edge</code> starts are unchanged, and a <code>from-ref</code> handle inside a handoff container still starts on pointerdown. The element keeps <code>touch-action</code> out of the way while <code>handoff-ref</code> is set (the containers must be able to scroll), so give handles their own <code>touch-action: none</code>.</p>\n<p>Mouse drags (<code>pointer-types="… mouse"</code>) take the same route with no native scroll to cancel: the first <code>pointermove</code> inside the container starts the gesture when the container is at its limit.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-swipeable-carousel\">Swipeable carousel</h4>\n<p><code>slide-animation="track"</code> turns <code>content-carousel</code> into a draggable track. <code>progress-axis="left"</code> makes a leftward drag pull the next slide in; the <code>:has()</code> rules narrow the bounds at the first and last slide so nothing wraps mid-drag; the <code>gesture-handler-snap</code> block swaps <code>is-active</code> and un-scrubs in one commit.</p>\n<p><include-content data-demo=\"carousel\"></include-content></p>\n<h4 id=\"md-pinch-rotate-drag\">Pinch, rotate, drag</h4>\n<p>Two fingers (a trackpad or touch screen) for <code>--gesture-scale</code> and <code>--gesture-rotate</code>; one for <code>--gesture-dx</code> / <code>--gesture-dy</code>. Pure CSS mapping, no sheet. Each gesture starts from the resting values.</p>\n<p><include-content data-demo=\"pinch\"></include-content></p>\n<h4 id=\"md-swipe-to-dismiss\">Swipe to dismiss</h4>\n<p><code>snap-points="-1 0 1"</code> with a literal <code>range-px</code>: a flick past <code>swipe-min-velocity</code> snaps the card off to the side, and the <code>gesture-handler-snap</code> block records the fact. A tap starts a new gesture (values reset) and clears it.</p>\n<p><include-content data-demo=\"swipe\"></include-content></p>\n",
|
|
19
|
+
"docs": {
|
|
20
|
+
"readme": "<h1 id=\"md-gesture-handler\">gesture-handler</h1>\n<p>Let users drag, swipe, pinch and flick your UI — sheets, carousels, cards and images follow the finger, in CSS, with no per-frame script.</p>\n<p><include-content data-demo=\"sheet\"></include-content></p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Swipe / pan / pinch / rotate / tap / long-press</strong> Each recognized gesture is a tag-prefixed event</li>\n<li><strong>Follow the finger in CSS</strong> Every frame lands in <code>--gesture-*</code> custom properties: scrubbable bottom sheets, swipe-to-dismiss, pinch-to-zoom, pull-to-refresh, parallax</li>\n<li><strong>Snap & fling</strong> <code>snap-points</code> with velocity projection, a CSS transition on release (<code>--gesture-snap-duration</code> / <code>--gesture-snap-ease</code>), <code>gesture-handler-snap</code> when it lands</li>\n<li><strong>Drivable</strong> Bounds, <code>progress-offset</code> and <code>is-disabled</code> are attributes a Quark rule sets from the driven element's state</li>\n<li><strong>Scoped starts</strong> <code>from-ref</code> for drag handles, <code>from-edge</code> for edge swipes</li>\n<li><strong>Scroll handoff</strong> <code>handoff-ref</code> lets a sheet's own scrolling content take the drag over when it runs out of scroll — a native-feeling pull-to-close</li>\n<li><strong>Native scrolling kept</strong> <code>touch-action</code> follows <code>gesture-types</code>, so the page still scrolls where you don't pan</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Wrap the surface the user touches. Pick the gestures with <code>gesture-types</code>; read the finger from <code>--gesture-*</code> in CSS (custom properties inherit, so any descendant can <code>var()</code> them); commit State on <code>gesture-handler-end</code> or <code>-snap</code> from a Quark <code>@on</code> block.</p>\n<include-content data-language=\"html\"><template><gesture-handler gesture-types=\"pan-y swipe\" progress-axis=\"up\" range-ref=\":scope > content-drawer\" snap-points=\"0 1\">\n <quark-sheet>\n :scope {\n @on gesture-handler-start { content-drawer { is-scrubbing: \"\"; } }\n @on gesture-handler-end { content-drawer { is-open: event.detail.snap == 1; is-scrubbing: none; } }\n }\n </quark-sheet>\n <content-drawer>…</content-drawer>\n</gesture-handler></template></include-content>\n<p><code>--gesture-progress</code> is the travel along <code>progress-axis</code> as a fraction of the range (<code>range-ref</code> measures the driven element; <code>range-px</code> is a literal), clamped to <code>progress-min</code>..<code>progress-max</code> with optional <code>overshoot-resistance</code>. Written alongside it every frame: <code>--gesture-dx</code> / <code>-dy</code>, <code>--gesture-x</code> / <code>-y</code>, <code>--gesture-scale</code>, <code>--gesture-rotate</code>, <code>--gesture-vx</code> / <code>-vy</code>, <code>--gesture-pointers</code>; once per gesture: <code>--gesture-range-px</code>, <code>--gesture-width</code> / <code>-height</code>. Everything else is derived from those in the element's own CSS — <code>--gesture-distance</code>, <code>--gesture-angle</code>, <code>--gesture-progress-px</code>, <code>--gesture-x-ratio</code> / <code>-y-ratio</code> — so <code>var()</code> them the same way.</p>\n<p>Values persist after release until the next gesture starts. On release the default action writes <code>--gesture-progress</code> straight to <code>detail.snap</code> and the element's own <code>transition</code> settles it there over <code>--gesture-snap-duration</code> (<code>200ms</code>) with <code>--gesture-snap-ease</code> (<code>ease-out</code>), firing <code>gesture-handler-snap</code> when it lands; the transition is off while <code>is-active</code>, so the finger itself is never eased, and a new gesture that interrupts the settle simply cancels it (no <code>-snap</code>).</p>\n<h3 id=\"md-driving-nucleus-kit-elements\">Driving Nucleus Kit elements</h3>\n<p>Elements that can be scrubbed expose a <code>--<tag>-…-progress</code> input and an <code>is-scrubbing</code> attribute; both default to the wrapping gesture-handler's <code>--gesture-progress</code>, so no mapping is needed:</p>\n<table>\n<thead>\n<tr>\n<th>Element</th>\n<th>Set up</th>\n<th>While <code>is-scrubbing</code></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>content-drawer</code></td>\n<td><code>progress-axis</code> towards its open side, <code>range-ref</code> the drawer, <code>handoff-ref</code> the drawer too (it scrolls its own content)</td>\n<td>Position follows <code>--content-drawer-open-progress</code> (<code>0</code> closed, <code>1</code> open), no transition</td>\n</tr>\n<tr>\n<td><code>content-carousel</code></td>\n<td><code>slide-animation="track"</code>, <code>progress-min="-1" progress-max="1" snap-points="-1 0 1"</code></td>\n<td>The track follows <code>--content-carousel-progress</code> in slide widths</td>\n</tr>\n</tbody></table>\n<p>The handoff is one Quark commit: the block that writes the final state (<code>is-open</code>, the active slide) also removes <code>is-scrubbing</code>, so the element switches from finger to State in the same paint. Commit on <code>-end</code> when a CSS transition should finish the motion (the drawer), on <code>-snap</code> when the element must be exactly at the snap point first (the carousel).</p>\n<p>Anything else follows the same recipe: read <code>--gesture-*</code> in your own CSS, gate the mapping on a fact your sheet writes on start and clears on end.</p>\n<p>Give a drag handle <code>touch-action: none</code> when using <code>from-ref</code>, so the browser does not scroll it away; without <code>from-ref</code> the element sets <code>touch-action</code> itself from <code>gesture-types</code>. Add <code>mouse</code> to <code>pointer-types</code> for desktop dragging.</p>\n<h3 id=\"md-scroll-handoff\">Scroll handoff</h3>\n<p><code>handoff-ref</code> names the scroll container(s) inside the surface whose <em>overscroll</em> starts a gesture (a <code>:scope</code>-relative selector; a comma list matches several). A pointer that goes down in one of them scrolls natively as usual. Only when its <strong>first</strong> move runs along <code>progress-axis</code>, the container is at its scroll limit that way, and <code>progress-offset</code> still has room to travel in that direction does the element cancel the native scroll for the rest of the touch and take the drag over — the same gesture, the same <code>--gesture-*</code> values and the same <code>-start</code> / <code>-end</code> / <code>-snap</code> events as a drag from a handle:</p>\n<include-content data-language=\"html\"><template><gesture-handler gesture-types=\"pan-y swipe\" progress-axis=\"up\" snap-points=\"0 1\"\n from-ref=\":scope > content-drawer > header\" handoff-ref=\":scope > content-drawer\"\n range-ref=\":scope > content-drawer\"></template></include-content>\n<p>An open sheet (<code>progress-offset: 1</code>) closes either from its header or by pulling its text down once the text is back at the top; pulling up, or pulling down mid-scroll, keeps scrolling. <code>handoff-ref</code> is additive — <code>from-ref</code> and <code>from-edge</code> starts are unchanged, and a <code>from-ref</code> handle inside a handoff container still starts on pointerdown. The element keeps <code>touch-action</code> out of the way while <code>handoff-ref</code> is set (the containers must be able to scroll), so give handles their own <code>touch-action: none</code>.</p>\n<p>Mouse drags (<code>pointer-types="… mouse"</code>) take the same route with no native scroll to cancel: the first <code>pointermove</code> inside the container starts the gesture when the container is at its limit.</p>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-swipeable-carousel\">Swipeable carousel</h4>\n<p><code>slide-animation="track"</code> turns <code>content-carousel</code> into a draggable track. <code>progress-axis="left"</code> makes a leftward drag pull the next slide in; the <code>:has()</code> rules narrow the bounds at the first and last slide so nothing wraps mid-drag; the <code>gesture-handler-snap</code> block swaps <code>is-active</code> and un-scrubs in one commit.</p>\n<p><include-content data-demo=\"carousel\"></include-content></p>\n<h4 id=\"md-pinch-rotate-drag\">Pinch, rotate, drag</h4>\n<p>Two fingers (a trackpad or touch screen) for <code>--gesture-scale</code> and <code>--gesture-rotate</code>; one for <code>--gesture-dx</code> / <code>--gesture-dy</code>. Pure CSS mapping, no sheet. Each gesture starts from the resting values.</p>\n<p><include-content data-demo=\"pinch\"></include-content></p>\n<h4 id=\"md-swipe-to-dismiss\">Swipe to dismiss</h4>\n<p><code>snap-points="-1 0 1"</code> with a literal <code>range-px</code>: a flick past <code>swipe-min-velocity</code> snaps the card off to the side, and the <code>gesture-handler-snap</code> block records the fact. A tap starts a new gesture (values reset) and clears it.</p>\n<p><include-content data-demo=\"swipe\"></include-content></p>\n"
|
|
21
|
+
},
|
|
22
|
+
"installation": {
|
|
23
|
+
"name": "@excom/gesture-handler",
|
|
24
|
+
"shortName": "gesture-handler",
|
|
25
|
+
"version": "0.1.0",
|
|
26
|
+
"description": "<gesture-handler> custom element",
|
|
27
|
+
"packageType": "kit-element",
|
|
28
|
+
"cdn": "<script src=\"https://unpkg.com/@excom/kit-utils/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/neutron/dist/index.umd.min.js\"></script>\n<script src=\"https://unpkg.com/@excom/gesture-handler@0.1.0/dist/index.umd.min.js\"></script>\n<link rel=\"stylesheet\" href=\"https://unpkg.com/@excom/gesture-handler@0.1.0/dist/index.css\">",
|
|
29
|
+
"install": {
|
|
30
|
+
"npm": "npm install @excom/gesture-handler"
|
|
31
|
+
},
|
|
32
|
+
"imports": {
|
|
33
|
+
"js": "import \"@excom/gesture-handler\";",
|
|
34
|
+
"css": "@import \"@excom/gesture-handler\";",
|
|
35
|
+
"html": "<!-- import path to `node_modules` will depend on your build setup -->\n<script type=\"module\" src=\"/node_modules/@excom/gesture-handler\"></script>\n<link rel=\"stylesheet\" href=\"/node_modules/@excom/gesture-handler\">"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": []
|
|
38
|
+
},
|
|
39
|
+
"elementApis": [
|
|
40
|
+
{
|
|
41
|
+
"tag": "gesture-handler",
|
|
42
|
+
"summary": "Swipe / pan / pinch / tap recognition as events + CSS variables.",
|
|
43
|
+
"kind": "class",
|
|
44
|
+
"attributes": [
|
|
45
|
+
{
|
|
46
|
+
"name": "arm-after",
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Pans only arm after this gesture — <code>long-press</code> for hold-then-drag.",
|
|
49
|
+
"fieldName": "armAfter",
|
|
50
|
+
"surface": "option",
|
|
51
|
+
"values": "\"long-press\""
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "double-tap-ms",
|
|
55
|
+
"type": "number",
|
|
56
|
+
"description": "Max gap (ms) between taps for <code>double-tap</code>.",
|
|
57
|
+
"fieldName": "doubleTapMs",
|
|
58
|
+
"surface": "option",
|
|
59
|
+
"default": "300"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "edge-px",
|
|
63
|
+
"type": "number",
|
|
64
|
+
"description": "Width of <code>from-edge</code> start zone (px).",
|
|
65
|
+
"fieldName": "edgePx",
|
|
66
|
+
"surface": "option",
|
|
67
|
+
"default": "40"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "from-edge",
|
|
71
|
+
"type": "tokenlist",
|
|
72
|
+
"description": "Only start within <code>edge-px</code> of these edges — edge swipes (back nav, pulling a closed sheet up).",
|
|
73
|
+
"fieldName": "fromEdge",
|
|
74
|
+
"surface": "option",
|
|
75
|
+
"values": "\"left\" | \"right\" | \"top\" | \"bottom\""
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "from-ref",
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "Only start when pointerdown is inside this descendant (<code>:scope</code>-relative selector) — a drag handle, the sheet itself. Combined with <code>from-edge</code>, either qualifies.",
|
|
81
|
+
"fieldName": "fromRef",
|
|
82
|
+
"surface": "option",
|
|
83
|
+
"values": "<CSS Selector>"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "gesture-types",
|
|
87
|
+
"type": "tokenlist",
|
|
88
|
+
"description": "Gestures to recognize. <code>pan-x</code> / <code>pan-y</code> one axis (other stays native scroll); <code>pan</code> both; <code>pinch</code> / <code>rotate</code> need two fingers; <code>swipe</code> velocity on release; <code>tap</code> / <code>double-tap</code> / <code>long-press</code> fire events.",
|
|
89
|
+
"fieldName": "gestureTypes",
|
|
90
|
+
"surface": "option",
|
|
91
|
+
"default": "\"pan\"",
|
|
92
|
+
"values": "\"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\""
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "handoff-ref",
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Scroll containers that hand overscroll to the gesture (<code>:scope</code>-relative selector, comma list matches several) — e.g. a sheet closed by pulling its own content down. Pointerdown inside one scrolls natively; gesture takes over only when first move runs along <code>progress-axis</code>, container is at that scroll limit, and <code>progress-offset</code> still has room that way. Additive to <code>from-ref</code> / <code>from-edge</code>.",
|
|
98
|
+
"fieldName": "handoffRef",
|
|
99
|
+
"surface": "option",
|
|
100
|
+
"values": "<CSS Selector>"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "lock-axis",
|
|
104
|
+
"type": "boolean",
|
|
105
|
+
"description": "A free <code>pan</code> locks to its dominant axis once recognized (<code>gesture-type</code> becomes <code>pan-x</code> / <code>pan-y</code>).",
|
|
106
|
+
"fieldName": "lockAxis",
|
|
107
|
+
"surface": "option"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"name": "long-press-ms",
|
|
111
|
+
"type": "number",
|
|
112
|
+
"description": "Hold time (ms) for <code>long-press</code>; also tap time limit.",
|
|
113
|
+
"fieldName": "longPressMs",
|
|
114
|
+
"surface": "option",
|
|
115
|
+
"default": "500"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "max-pointers",
|
|
119
|
+
"type": "number",
|
|
120
|
+
"description": "Extra pointers beyond this many are ignored. Unset = <code>2</code> when <code>pinch</code> / <code>rotate</code> listed, else <code>1</code>.",
|
|
121
|
+
"fieldName": "maxPointers",
|
|
122
|
+
"surface": "option"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "overshoot-resistance",
|
|
126
|
+
"type": "number",
|
|
127
|
+
"description": "Rubber-band past <code>progress-min</code> / <code>progress-max</code>: <code>0</code> clamps, <code>0.3</code> overshoots at a third of travel.",
|
|
128
|
+
"fieldName": "overshootResistance",
|
|
129
|
+
"surface": "option",
|
|
130
|
+
"default": "0"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "pointer-types",
|
|
134
|
+
"type": "tokenlist",
|
|
135
|
+
"description": "Pointer types that can start a gesture. Add <code>mouse</code> for desktop drag.",
|
|
136
|
+
"fieldName": "pointerTypes",
|
|
137
|
+
"surface": "option",
|
|
138
|
+
"default": "\"touch pen\"",
|
|
139
|
+
"values": "\"touch\" | \"pen\" | \"mouse\""
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "progress-axis",
|
|
143
|
+
"type": "string",
|
|
144
|
+
"description": "Direction <code>--gesture-progress</code> grows. Unset = <code>down</code> for a <code>pan-y</code>-only element, else <code>right</code>.",
|
|
145
|
+
"fieldName": "progressAxis",
|
|
146
|
+
"surface": "option",
|
|
147
|
+
"values": "\"up\" | \"down\" | \"left\" | \"right\""
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "progress-max",
|
|
151
|
+
"type": "number",
|
|
152
|
+
"description": "Upper bound of <code>progress</code>.",
|
|
153
|
+
"fieldName": "progressMax",
|
|
154
|
+
"surface": "option",
|
|
155
|
+
"default": "1"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "progress-min",
|
|
159
|
+
"type": "number",
|
|
160
|
+
"description": "Lower bound of <code>progress</code>.",
|
|
161
|
+
"fieldName": "progressMin",
|
|
162
|
+
"surface": "option",
|
|
163
|
+
"default": "0"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "progress-offset",
|
|
167
|
+
"type": "number",
|
|
168
|
+
"description": "Progress the gesture starts from. Set from a rule that reads the driven element's state (<code>1</code> while a sheet is open) so dragging it closed starts full.",
|
|
169
|
+
"fieldName": "progressOffset",
|
|
170
|
+
"surface": "option",
|
|
171
|
+
"default": "0"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "range-px",
|
|
175
|
+
"type": "number",
|
|
176
|
+
"description": "Literal range (px) instead of <code>range-ref</code>.",
|
|
177
|
+
"fieldName": "rangePx",
|
|
178
|
+
"surface": "option"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "range-ref",
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "Element whose size along <code>progress-axis</code> is the range of <code>progress</code> <code>0</code>..<code>1</code> (<code>:scope</code>-relative, read once per gesture) — sheet being dragged, slide being swiped.",
|
|
184
|
+
"fieldName": "rangeRef",
|
|
185
|
+
"surface": "option",
|
|
186
|
+
"values": "<CSS Selector>"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"name": "should-emit-move",
|
|
190
|
+
"type": "boolean",
|
|
191
|
+
"description": "Fire <code>gesture-handler-move</code> every frame. Off by default; <code>--gesture-*</code> is enough for CSS.",
|
|
192
|
+
"fieldName": "shouldEmitMove",
|
|
193
|
+
"surface": "option"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "snap-points",
|
|
197
|
+
"type": "tokenlist",
|
|
198
|
+
"description": "Progress values to settle on after release (<code>0 0.5 1</code>). Target picked from position, fling velocity and swipe direction, reported as <code>detail.snap</code> on <code>-end</code>, settled on by the default action (a CSS transition, <code>--gesture-snap-duration</code> / <code>--gesture-snap-ease</code>).",
|
|
199
|
+
"fieldName": "snapPoints",
|
|
200
|
+
"surface": "option",
|
|
201
|
+
"values": "<number>…"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "swipe-directions",
|
|
205
|
+
"type": "tokenlist",
|
|
206
|
+
"description": "Swipe directions to report. Unset = all four.",
|
|
207
|
+
"fieldName": "swipeDirections",
|
|
208
|
+
"surface": "option",
|
|
209
|
+
"values": "\"left\" | \"right\" | \"up\" | \"down\""
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"name": "swipe-min-velocity",
|
|
213
|
+
"type": "number",
|
|
214
|
+
"description": "Release velocity (px/ms) that counts as a swipe.",
|
|
215
|
+
"fieldName": "swipeMinVelocity",
|
|
216
|
+
"surface": "option",
|
|
217
|
+
"default": "0.5"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "threshold-px",
|
|
221
|
+
"type": "number",
|
|
222
|
+
"description": "Movement (px) before a pan is recognized. Taps / native scroll stay untouched below it.",
|
|
223
|
+
"fieldName": "thresholdPx",
|
|
224
|
+
"surface": "option",
|
|
225
|
+
"default": "8"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"name": "is-disabled",
|
|
229
|
+
"type": "boolean",
|
|
230
|
+
"description": "Ignore new pointers; a gesture in progress is cancelled. State-driven veto.",
|
|
231
|
+
"fieldName": "isDisabled",
|
|
232
|
+
"surface": "hybrid"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "gesture-direction",
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Dominant travel direction of gesture in progress.",
|
|
238
|
+
"fieldName": "gestureDirection",
|
|
239
|
+
"surface": "state",
|
|
240
|
+
"values": "\"left\" | \"right\" | \"up\" | \"down\""
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"name": "gesture-type",
|
|
244
|
+
"type": "string",
|
|
245
|
+
"description": "Recognized gesture in progress, unset before recognition and after release.",
|
|
246
|
+
"fieldName": "gestureType",
|
|
247
|
+
"surface": "state",
|
|
248
|
+
"values": "\"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\""
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "is-active",
|
|
252
|
+
"type": "boolean",
|
|
253
|
+
"description": "A pointer is down on the surface. Set from first pointer until release, so it also covers taps and pre-threshold phase.",
|
|
254
|
+
"fieldName": "isActive",
|
|
255
|
+
"surface": "state"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "last-gesture",
|
|
259
|
+
"type": "string",
|
|
260
|
+
"description": "What last gesture turned out to be — style a "just swiped" state from it.",
|
|
261
|
+
"fieldName": "lastGesture",
|
|
262
|
+
"surface": "state",
|
|
263
|
+
"values": "\"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe-left\" | \"swipe-right\" | \"swipe-up\" | \"swipe-down\" | \"tap\" | \"double-tap\" | \"long-press\""
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"name": "pointer-count",
|
|
267
|
+
"type": "number",
|
|
268
|
+
"description": "Pointers currently down.",
|
|
269
|
+
"fieldName": "pointerCount",
|
|
270
|
+
"surface": "state"
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"events": [
|
|
274
|
+
{
|
|
275
|
+
"name": "gesture-handler-cancel",
|
|
276
|
+
"description": "Recognized gesture cut short: browser took pointer (<code>pointercancel</code>, usually native scroll), <code>is-disabled</code> set, or element left the document. <code>detail</code> = provision. No <code>-end</code>, no glide.",
|
|
277
|
+
"type": "GestureHandlerCancelEvent",
|
|
278
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-cancel\"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: | \"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\" | null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number | null; swipe: \"left\" | \"right\" | \"up\" | \"down\" | null; }; bubbles: true; cancelable: true; composed: true }"
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
"name": "gesture-handler-double-tap",
|
|
282
|
+
"description": "Second tap within <code>double-tap-ms</code> of the previous (after its <code>gesture-handler-tap</code>). <code>detail</code> = <code>{ x, y }</code>. Pair consumed; a third tap starts over.",
|
|
283
|
+
"type": "GestureHandlerPointEvent",
|
|
284
|
+
"typeExpanded": "CustomEvent & { type: | \"gesture-handler-tap\" | \"gesture-handler-double-tap\" | \"gesture-handler-long-press\"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }"
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "gesture-handler-end",
|
|
288
|
+
"description": "Last pointer up after a recognized gesture. After <code>is-active</code> unset, <code>last-gesture</code> + <code>provision</code> set, and after <code>gesture-handler-swipe</code> if one was recognized. <code>detail.snap</code> = <code>snap-points</code> target from position / velocity / swipe (<code>null</code> without <code>snap-points</code>); <code>detail.swipe</code> = swipe direction. Default action: write <code>--gesture-progress</code> = <code>detail.snap</code>, which settles with a CSS transition (<code>--gesture-snap-duration</code> / <code>--gesture-snap-ease</code>), then <code>gesture-handler-snap</code>. <code>preventDefault()</code> leaves values where the finger left them. Persist until next gesture.",
|
|
289
|
+
"type": "GestureHandlerEndEvent",
|
|
290
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-end\"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: | \"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\" | null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number | null; swipe: \"left\" | \"right\" | \"up\" | \"down\" | null; }; bubbles: true; cancelable: true; composed: true }"
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "gesture-handler-long-press",
|
|
294
|
+
"description": "Pointer stayed down without moving for <code>long-press-ms</code>. <code>detail</code> = <code>{ x, y }</code>.",
|
|
295
|
+
"type": "GestureHandlerPointEvent",
|
|
296
|
+
"typeExpanded": "CustomEvent & { type: | \"gesture-handler-tap\" | \"gesture-handler-double-tap\" | \"gesture-handler-long-press\"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"name": "gesture-handler-move",
|
|
300
|
+
"description": "Once per frame while a recognized gesture moves, only with <code>should-emit-move</code>. <code>detail</code> = provision. <code>--gesture-*</code> always updates, event or not.",
|
|
301
|
+
"type": "GestureHandlerMoveEvent",
|
|
302
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-move\"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: | \"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\" | null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number | null; swipe: \"left\" | \"right\" | \"up\" | \"down\" | null; }; bubbles: true; cancelable: true; composed: true }"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"name": "gesture-handler-snap",
|
|
306
|
+
"description": "The settle transition reached the snap point (at once when there is nothing to animate; never when a new gesture interrupts it). <code>detail</code> = <code>{ value, index }</code> into <code>snap-points</code>. Commit here when consumer CSS follows <code>--gesture-progress</code> until the end.",
|
|
307
|
+
"type": "GestureHandlerSnapEvent",
|
|
308
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-snap\"; detail: { value: number; index: number }; bubbles: true; cancelable: true; composed: true }"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "gesture-handler-start",
|
|
312
|
+
"description": "Gesture recognized: pan passed <code>threshold-px</code> (after <code>arm-after</code> if set), or a second finger for <code>pinch</code> / <code>rotate</code>. After <code>gesture-type</code> + <code>provision</code> set. <code>detail</code> = provision. Not cancelable — gate with <code>is-disabled</code>.",
|
|
313
|
+
"type": "GestureHandlerStartEvent",
|
|
314
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-start\"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: | \"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\" | null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number | null; swipe: \"left\" | \"right\" | \"up\" | \"down\" | null; }; bubbles: true; cancelable: true; composed: true }"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"name": "gesture-handler-swipe",
|
|
318
|
+
"description": "On release, velocity along dominant axis ≥ <code>swipe-min-velocity</code> and direction allowed by <code>swipe-directions</code>. <code>detail</code> = <code>{ direction, velocity }</code> (px/ms). Direction twin fires next (<code>gesture-handler-swipe-left</code> / <code>-right</code> / <code>-up</code> / <code>-down</code>).",
|
|
319
|
+
"type": "GestureHandlerSwipeEvent",
|
|
320
|
+
"typeExpanded": "CustomEvent & { type: \"gesture-handler-swipe\" | `gesture-handler-swipe-${\"left\" | \"right\" | \"up\" | \"down\"}`; detail: { direction: \"left\" | \"right\" | \"up\" | \"down\"; velocity: number }; bubbles: true; cancelable: true; composed: true }"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"name": "gesture-handler-tap",
|
|
324
|
+
"description": "Down and up without moving past <code>threshold-px</code>, within <code>long-press-ms</code>. <code>detail</code> = <code>{ x, y }</code> relative to the element.",
|
|
325
|
+
"type": "GestureHandlerPointEvent",
|
|
326
|
+
"typeExpanded": "CustomEvent & { type: | \"gesture-handler-tap\" | \"gesture-handler-double-tap\" | \"gesture-handler-long-press\"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }"
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
"slots": [],
|
|
330
|
+
"cssProperties": [
|
|
331
|
+
{
|
|
332
|
+
"name": "--gesture-angle",
|
|
333
|
+
"description": "Direction of the travel: <code>0deg</code> right, <code>90deg</code> down. Computed from <code>--gesture-dx</code> / <code>--gesture-dy</code>.",
|
|
334
|
+
"syntax": "<angle>",
|
|
335
|
+
"default": "atan2(var(--gesture-dy), var(--gesture-dx))"
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
"name": "--gesture-distance",
|
|
339
|
+
"description": "Length of the travel (px). Computed from <code>--gesture-dx</code> / <code>--gesture-dy</code>.",
|
|
340
|
+
"syntax": "<length>",
|
|
341
|
+
"default": "hypot(var(--gesture-dx), var(--gesture-dy))"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "--gesture-dx",
|
|
345
|
+
"description": "Horizontal travel since the gesture started (px) — drag-to-move, swipe-to-dismiss.",
|
|
346
|
+
"syntax": "<length>",
|
|
347
|
+
"default": "0px"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"name": "--gesture-dy",
|
|
351
|
+
"description": "Vertical travel since the gesture started (px).",
|
|
352
|
+
"syntax": "<length>",
|
|
353
|
+
"default": "0px"
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "--gesture-height",
|
|
357
|
+
"description": "The element's own height, measured when the gesture starts.",
|
|
358
|
+
"syntax": "<length>",
|
|
359
|
+
"default": "0px"
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
"name": "--gesture-pointers",
|
|
363
|
+
"description": "Pointers currently down.",
|
|
364
|
+
"syntax": "<integer>",
|
|
365
|
+
"default": "0"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "--gesture-progress",
|
|
369
|
+
"description": "Travel along <code>progress-axis</code> as a fraction of the range, clamped to <code>progress-min</code>..<code>progress-max</code> — the value a scrubbable sheet, carousel or slider follows. Persists after release until the next gesture.",
|
|
370
|
+
"syntax": "<number>",
|
|
371
|
+
"default": "0"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "--gesture-progress-px",
|
|
375
|
+
"description": "<code>--gesture-progress</code> in px. Computed from <code>--gesture-progress</code> and <code>--gesture-range-px</code>.",
|
|
376
|
+
"syntax": "<length>",
|
|
377
|
+
"default": "calc(var(--gesture-progress) * var(--gesture-range-px))"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"name": "--gesture-range-px",
|
|
381
|
+
"description": "The measured range (<code>range-ref</code> size along the axis, or <code>range-px</code>). Written once, when the gesture starts.",
|
|
382
|
+
"syntax": "<length>",
|
|
383
|
+
"default": "0px"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "--gesture-rotate",
|
|
387
|
+
"description": "Two-finger rotation — rotate-to-adjust.",
|
|
388
|
+
"syntax": "<angle>",
|
|
389
|
+
"default": "0deg"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"name": "--gesture-scale",
|
|
393
|
+
"description": "Two-finger pinch ratio (<code>1</code> = unchanged) — pinch-to-zoom.",
|
|
394
|
+
"syntax": "<number>",
|
|
395
|
+
"default": "1"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"name": "--gesture-snap-duration",
|
|
399
|
+
"description": "How long <code>--gesture-progress</code> takes to settle on its snap point after release.",
|
|
400
|
+
"syntax": "<time>",
|
|
401
|
+
"default": "200ms"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"name": "--gesture-snap-ease",
|
|
405
|
+
"description": "Easing of that settle.",
|
|
406
|
+
"syntax": "<easing-function>",
|
|
407
|
+
"default": "ease-out"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
"name": "--gesture-vx",
|
|
411
|
+
"description": "Horizontal velocity over the last ~100 ms (px/ms).",
|
|
412
|
+
"syntax": "<number>",
|
|
413
|
+
"default": "0"
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"name": "--gesture-vy",
|
|
417
|
+
"description": "Vertical velocity over the last ~100 ms (px/ms).",
|
|
418
|
+
"syntax": "<number>",
|
|
419
|
+
"default": "0"
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
"name": "--gesture-width",
|
|
423
|
+
"description": "The element's own width, measured when the gesture starts.",
|
|
424
|
+
"syntax": "<length>",
|
|
425
|
+
"default": "0px"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
"name": "--gesture-x",
|
|
429
|
+
"description": "Pointer centroid inside the element (px), horizontal — spotlight / parallax effects. Written every frame of a gesture; this is the resting value.",
|
|
430
|
+
"syntax": "<length>",
|
|
431
|
+
"default": "0px"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"name": "--gesture-x-ratio",
|
|
435
|
+
"description": "<code>--gesture-x</code> as a 0..1 fraction of the element's width. Computed from <code>--gesture-x</code> and <code>--gesture-width</code>.",
|
|
436
|
+
"syntax": "<number>",
|
|
437
|
+
"default": "tan(atan2(var(--gesture-x), var(--gesture-width)))"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
"name": "--gesture-y",
|
|
441
|
+
"description": "Pointer centroid inside the element (px), vertical.",
|
|
442
|
+
"syntax": "<length>",
|
|
443
|
+
"default": "0px"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"name": "--gesture-y-ratio",
|
|
447
|
+
"description": "<code>--gesture-y</code> as a 0..1 fraction of the element's height. Computed from <code>--gesture-y</code> and <code>--gesture-height</code>.",
|
|
448
|
+
"syntax": "<number>",
|
|
449
|
+
"default": "tan(atan2(var(--gesture-y), var(--gesture-height)))"
|
|
450
|
+
}
|
|
451
|
+
],
|
|
452
|
+
"cssClasses": [],
|
|
453
|
+
"cssAliases": [
|
|
454
|
+
{
|
|
455
|
+
"name": ":--gesture-handler",
|
|
456
|
+
"selectors": [
|
|
457
|
+
"gesture-handler",
|
|
458
|
+
".tag-gesture-handler"
|
|
459
|
+
],
|
|
460
|
+
"kind": "element",
|
|
461
|
+
"description": ""
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"name": ":--gesture-handler--is-active",
|
|
465
|
+
"selectors": [
|
|
466
|
+
"[is-active]",
|
|
467
|
+
"[data-active]"
|
|
468
|
+
],
|
|
469
|
+
"kind": "state",
|
|
470
|
+
"description": ""
|
|
471
|
+
}
|
|
472
|
+
],
|
|
473
|
+
"listens": [],
|
|
474
|
+
"commands": [],
|
|
475
|
+
"defaultActions": [],
|
|
476
|
+
"expectedChildren": [],
|
|
477
|
+
"provisions": [
|
|
478
|
+
{
|
|
479
|
+
"name": "provision",
|
|
480
|
+
"type": "GestureHandlerProvision",
|
|
481
|
+
"typeExpanded": "{ x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: | \"pan\" | \"pan-x\" | \"pan-y\" | \"pinch\" | \"rotate\" | \"swipe\" | \"tap\" | \"double-tap\" | \"long-press\" | null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number | null; swipe: \"left\" | \"right\" | \"up\" | \"down\" | null; }",
|
|
482
|
+
"description": "Last <code>-start</code> / <code>-end</code> / <code>-cancel</code> snapshot (<code>type</code>, travel, velocity, progress, <code>snap</code>, <code>swipe</code>, …). Not an attribute; per-frame values live in <code>--gesture-*</code>.",
|
|
483
|
+
"fieldName": "provision"
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
"exportedFiles": {}
|
|
489
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import "@excom/quark-sheet";
|
|
2
|
+
import "@excom/content-carousel";
|
|
3
|
+
import "../../index";
|
|
4
|
+
import {
|
|
5
|
+
afterEach,
|
|
6
|
+
describe,
|
|
7
|
+
expect,
|
|
8
|
+
it,
|
|
9
|
+
wait,
|
|
10
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
11
|
+
import {
|
|
12
|
+
flush,
|
|
13
|
+
mountView,
|
|
14
|
+
readDemo,
|
|
15
|
+
} from "@excom/quark/support/tests/view-helpers";
|
|
16
|
+
import { drag } from "./pointer-utils";
|
|
17
|
+
|
|
18
|
+
describe("carousel view", () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.innerHTML = "";
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("drags the track, snaps to the next slide and clamps at the ends", async () => {
|
|
24
|
+
const { root } = await mountView(readDemo(import.meta.url, "carousel"));
|
|
25
|
+
const handler = root as HTMLGestureHandlerElement;
|
|
26
|
+
const carousel = root.querySelector("content-carousel")!;
|
|
27
|
+
const slides = [...carousel.querySelectorAll("content-carousel-slide")];
|
|
28
|
+
Object.defineProperty(carousel, "offsetWidth", { value: 300 });
|
|
29
|
+
const active = () => slides.findIndex((s) => s.hasAttribute("is-active"));
|
|
30
|
+
expect(active()).toBe(0);
|
|
31
|
+
// first slide: no dragging backwards
|
|
32
|
+
expect(handler.getAttribute("progress-min")).toBe("0");
|
|
33
|
+
expect(handler.getAttribute("progress-max")).toBe("1");
|
|
34
|
+
|
|
35
|
+
// drag left two thirds of a slide, let go: snaps to 1 → slide two
|
|
36
|
+
const release = await drag(carousel, { x: 250, y: 50 }, { x: 50, y: 50 });
|
|
37
|
+
await flush();
|
|
38
|
+
expect(carousel.hasAttribute("is-scrubbing")).toBe(true);
|
|
39
|
+
expect(handler.style.getPropertyValue("--gesture-progress")).toBe("0.6667");
|
|
40
|
+
await wait(150);
|
|
41
|
+
release();
|
|
42
|
+
expect(handler.provision?.snap).toBe(1);
|
|
43
|
+
await wait(0); // the settle has nothing to animate here → -snap at once
|
|
44
|
+
await flush();
|
|
45
|
+
expect(handler.style.getPropertyValue("--gesture-progress")).toBe("1");
|
|
46
|
+
expect(active()).toBe(1);
|
|
47
|
+
expect(carousel.hasAttribute("is-scrubbing")).toBe(false);
|
|
48
|
+
expect(handler.getAttribute("progress-min")).toBe("-1");
|
|
49
|
+
|
|
50
|
+
// a short drag right snaps back to 0: the slide stays
|
|
51
|
+
const stay = await drag(carousel, { x: 100, y: 50 }, { x: 130, y: 50 });
|
|
52
|
+
await wait(150);
|
|
53
|
+
stay();
|
|
54
|
+
await wait(0);
|
|
55
|
+
await flush();
|
|
56
|
+
expect(active()).toBe(1);
|
|
57
|
+
|
|
58
|
+
// drag right past half: previous slide
|
|
59
|
+
const back = await drag(carousel, { x: 50, y: 50 }, { x: 250, y: 50 });
|
|
60
|
+
await wait(150);
|
|
61
|
+
back();
|
|
62
|
+
await wait(0);
|
|
63
|
+
await flush();
|
|
64
|
+
expect(active()).toBe(0);
|
|
65
|
+
expect(handler.getAttribute("progress-min")).toBe("0");
|
|
66
|
+
});
|
|
67
|
+
});
|