Repository navigation
chore(core): swipe actions' UIKit values and release rule #8069
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
c784084
spec(AST-094): touch presentations feel native by default
cixzhang 0c35c43
chore(core): the UIKit constants table for touch presentations
vjeux 17c181b
chore(core): UIKit's spring, decay, rubber-band and projection math
vjeux 6bd2851
chore(core): the constants table names only what exists, and dates ev…
vjeux 76be493
fix(core): paging past the last page lands on a real page
vjeux 4b3de64
Merge main into the constants table (lab typechecks with its tests)
vjeux 530315f
Merge #8053 (main merged in) into the motion math
vjeux 7fd8edf
chore(core): swipe actions' UIKit values and release rule
vjeux e7df427
perf(core): the gesture table's sources move to JSDoc, out of the bundle
vjeux 439dcc3
docs(spec): the draft AST-094 copy leaves this stack
vjeux 0bf13da
Merge main (the deploy typecheck gate test reads the step as written)…
vjeux bbbcb60
refactor(core): the platform presets are named for their use
vjeux 7c5f71c
Merge main (67dc45f66d3) into #8054
cixzhang 03f66c3
fix(core): the UIKit table keeps only what a touch presentation plays…
cixzhang 1133bf9
Merge remote-tracking branch 'origin/main' into m8053
cixzhang eded944
style(core): MEASUREMENTS.md table widths after the dropped rows
cixzhang f9d0900
feat(core): the press release's curve is a press value
vjeux 9db9315
docs(core): the velocity weight says a rested finger releases at 0
vjeux 5d3cab6
Merge #8053 (the table keeps only what a touch presentation plays; th…
vjeux 7e36bcd
fix(core): the table's notes point at what exists, and the press entr…
cixzhang c1fa660
Merge #8053's notes and press pins into the press release's curve and…
vjeux 59a748f
Merge #8053 (the table's notes point at what exists; the press entrie…
vjeux 6ae3390
Merge main (#8043's interaction proofs) into the UIKit constants table
vjeux 51b3ecb
Merge #8053 (main with #8043's interaction proofs) into the motion math
vjeux 1f751c9
docs(core): M-S2 feeds scroll.decelerationRate, where the coast readi…
cixzhang 96f8240
Merge #8053 (M-S2 feeds scroll.decelerationRate) into the press relea…
vjeux 1ae9200
Merge #8053 (M-S2 feeds scroll.decelerationRate) into the motion math
vjeux 14935b0
Merge #8054 (1ae92006cf) into #8069
vjeux 7234d44
Merge main (99ad3f6d906) into the motion math
cixzhang 25330a3
Merge #8054 (7234d4409b, main 99ad3f6d90) into #8069
vjeux d3a2e41
fix(core): toSpring's clamp, FR6 for the rubber band's inverse, the p…
cixzhang 3dca43f
Merge main (16770e30070) into the motion math
cixzhang 3918f31
Merge #8054's final head (3dca43f62a) into #8069
vjeux 13ecb14
Merge main (52c1b24fb8, #8054 squashed as b58ca635e6) into #8069
vjeux a269997
Merge main (04b0e64ac4) into #8069
vjeux fa6c198
Merge main (bb12cfbd60) into #8069
vjeux f5f7346
Merge main (ef6d25b715) into #8069
vjeux bbcb3f4
docs(core): the swipe values' changeset counts the releases its tests…
vjeux e805797
Merge main (6aea9bda01) into #8069
vjeux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@astryxdesign/core': patch | ||
| --- | ||
|
|
||
| [chore] Internal: the UIKit values of a swipe-action row (the swipe threshold, the spring the row follows the finger and settles with, the touch claim distance the web needs) and the release rule that picks closed, open or fire (a fast release goes the way it moves, a slow one by where it is, and only a release past the commit point fires), checked against 44 of the 51 releases the UIKit lab read in its batch 10a and against its slow and moving-back releases of batch 38. Not exported from the package entry point. | ||
|
|
||
| @vjeux |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** | ||
| * @file swipeRelease.test.ts | ||
| * @input vitest; swipeRelease.ts and the UIKit constants table | ||
| * @output The release rule against the UIKit lab's releases; the commit | ||
| * point and the resistance past it; the swipe spring is the one UIKit | ||
| * reported | ||
| * @position Unit tests of packages/core/src/Item/swipeRelease.ts | ||
| */ | ||
|
|
||
| import {describe, expect, it} from 'vitest'; | ||
| import { | ||
| dampingRatioOf, | ||
| naturalFrequency, | ||
| settlingDuration, | ||
| toSpring, | ||
| } from '../motion/spring'; | ||
| import {swipeActions} from '../motion/uikit/swipeActions'; | ||
| import {commitPoint, resistedTravel, resolveRelease} from './swipeRelease'; | ||
|
|
||
| describe('resolveRelease', () => { | ||
| // The UIKit lab's releases (batch 10a: plain table, 402 pt rows, two | ||
| // trailing actions, a 156.67 pt open width; the full-swipe point far beyond | ||
| // them), as the swipe pan reported them when the finger lifted: the | ||
| // swipe's travel (past the 25 pt the pan needs to begin; negative when the | ||
| // finger came back past where it began) and its velocity in pt/s, positive | ||
| // toward open. A scripted "closing" release can still be opening when it | ||
| // lifts (the 36 pt one at +458), so the reported values are the inputs. | ||
| const OPEN_WIDTH = 156.67; | ||
| const FAR = 373; | ||
| const RELEASES: [travel: number, velocity: number, lands: string][] = [ | ||
| [35, -19, 'closed'], | ||
| [65, 490, 'open'], | ||
| [125, 1506, 'open'], | ||
| [5, -492, 'closed'], | ||
| [-55, -1532, 'closed'], | ||
| [95, -16, 'open'], | ||
| [125, 478, 'open'], | ||
| [185, 1565, 'open'], | ||
| [65, -468, 'closed'], | ||
| // [115, +135]: closed. Scripted as a 1500 pt/s closing flick, it lifted | ||
| // with the pan reporting a slow opening one: the one release of the 51 | ||
| // neither this rule nor projection explains. | ||
| [-67, -2545, 'closed'], | ||
| [-47.7, -1025, 'closed'], | ||
| [-3, -256, 'closed'], | ||
| [12.3, -22, 'closed'], | ||
| [30.3, 293, 'open'], | ||
| [64, 1058, 'open'], | ||
| [132.3, 1983, 'open'], | ||
| [-67, -2060, 'closed'], | ||
| [-10, -1087, 'closed'], | ||
| [35.7, 458, 'open'], | ||
| [50, -18, 'closed'], | ||
| [63, 286, 'open'], | ||
| [93, 1372, 'open'], | ||
| [170, 2031, 'open'], | ||
| [1.7, -2168, 'closed'], | ||
| [27.3, -996, 'closed'], | ||
| [69.3, -287, 'closed'], | ||
| [87.7, -5, 'open'], | ||
| [100.3, 148, 'open'], | ||
| [147.3, 1271, 'open'], | ||
| [190.3, 608, 'open'], | ||
| [39.3, -2052, 'closed'], | ||
| [65, -983, 'closed'], | ||
| [107, -308, 'closed'], | ||
| [125, -18, 'open'], | ||
| [143, 347, 'open'], | ||
| [185, 948, 'open'], | ||
| [245, 2082, 'open'], | ||
| [42.3, -1975, 'closed'], | ||
| [102.7, -991, 'closed'], | ||
| [144.3, -283, 'closed'], | ||
| [162.3, -19, 'open'], | ||
| [180.3, 271, 'open'], | ||
| [222.7, 1002, 'open'], | ||
| [282.3, 1971, 'open'], | ||
| ]; | ||
|
|
||
| it.each(RELEASES)( | ||
| 'lands where UIKit did: %s pt of swipe at %s pt/s → %s', | ||
| (travel, velocity, lands) => { | ||
| expect( | ||
| resolveRelease({ | ||
| behavior: 'reveal', | ||
| commitAt: FAR, | ||
| openWidth: OPEN_WIDTH, | ||
| travel: Math.max(0, travel), | ||
| velocity, | ||
| }), | ||
| ).toBe(lands); | ||
| }, | ||
| ); | ||
|
|
||
| it('goes the way a release moves from about 50 pt/s, and by where it is below that (batches 38c, 38d)', () => { | ||
| // 204.67 pt open width, released at 0.7 of it: past half. The speeds | ||
| // are the pan's at its end, as UIKit read them. | ||
| const base = { | ||
| behavior: 'reveal' as const, | ||
| commitAt: 325, | ||
| openWidth: 204.67, | ||
| travel: 143, | ||
| }; | ||
| for (const velocity of [-19.9, -21.2, -35.7, -40.7, 0, 56.1]) { | ||
| expect(resolveRelease({...base, velocity})).toBe('open'); | ||
| } | ||
| for (const velocity of [-56.1, -56.5, -68.5, -105, -200, -308]) { | ||
| expect(resolveRelease({...base, velocity})).toBe('closed'); | ||
| } | ||
| }); | ||
|
|
||
| it('closes a release past the open width that is moving back, as UIKit did (batch 38)', () => { | ||
| // 204.67 pt open width; released about 300 pt out while coming back at | ||
| // 300 pt/s: UIKit closed the row, though it would coast to rest past half | ||
| // the open width. | ||
| expect( | ||
| resolveRelease({ | ||
| behavior: 'reveal', | ||
| commitAt: 325, | ||
| openWidth: 204.67, | ||
| travel: 305, | ||
| velocity: -300, | ||
| }), | ||
| ).toBe('closed'); | ||
| // The same release at rest, or still opening, stays open. | ||
| for (const velocity of [0, 300]) { | ||
| expect( | ||
| resolveRelease({ | ||
| behavior: 'reveal', | ||
| commitAt: 325, | ||
| openWidth: 204.67, | ||
| travel: 305, | ||
| velocity, | ||
| }), | ||
| ).toBe('open'); | ||
| } | ||
| }); | ||
|
|
||
| it('fires under reveal on a release past the commit point, whatever the velocity, and never on a flick short of it', () => { | ||
| const base = {behavior: 'reveal' as const, commitAt: 192, openWidth: 144}; | ||
| expect(resolveRelease({...base, travel: 192, velocity: 0})).toBe('perform'); | ||
| expect(resolveRelease({...base, travel: 200, velocity: -2000})).toBe( | ||
| 'perform', | ||
| ); | ||
| expect(resolveRelease({...base, travel: 44, velocity: 3000})).toBe('open'); | ||
| expect(resolveRelease({...base, travel: 191, velocity: 3000})).toBe('open'); | ||
| }); | ||
|
|
||
| it('fires under commit only past the commit point, and never rests', () => { | ||
| const base = {behavior: 'commit' as const, commitAt: 150, openWidth: 72}; | ||
| expect(resolveRelease({...base, travel: 150, velocity: 0})).toBe('perform'); | ||
| expect(resolveRelease({...base, travel: 160, velocity: -1000})).toBe( | ||
| 'perform', | ||
| ); | ||
| expect(resolveRelease({...base, travel: 100, velocity: 0})).toBe('closed'); | ||
| expect(resolveRelease({...base, travel: 40, velocity: 3000})).toBe( | ||
| 'closed', | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('commitPoint and resistedTravel', () => { | ||
| it('commits past the panel, or past the fraction of the row when that is further', () => { | ||
| expect(commitPoint(72, 300)).toBe(150); | ||
| expect(commitPoint(144, 300)).toBe(192); | ||
| }); | ||
|
|
||
| it('follows the finger up to the commit point and resists past it', () => { | ||
| expect(resistedTravel(150, 150)).toBe(150); | ||
| expect(resistedTravel(250, 150)).toBeCloseTo( | ||
| 150 + 100 * swipeActions.overdragResistance, | ||
| 10, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('the swipe spring', () => { | ||
| it('is the one UIKit reported: critically damped at 10 rad/s, settling in 0.9235 s', () => { | ||
| for (const description of [swipeActions.tracking, swipeActions.settle]) { | ||
| const spring = toSpring(description); | ||
| expect(dampingRatioOf(spring)).toBeCloseTo(1, 10); | ||
| expect(naturalFrequency(spring)).toBeCloseTo(10, 10); | ||
| // UIKit's settlingDuration for it, within the 0.02 % the solver's | ||
| // critically damped branch is documented to land. | ||
| expect(settlingDuration(spring)).toBeCloseTo(0.9235, 3); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** | ||
| * @file swipeRelease.ts | ||
| * @input The UIKit constants table (motion/uikit/swipeActions.ts) | ||
| * @output Exports resolveRelease (where a swipe-action release lands: closed, | ||
| * open, or the outermost entry fires), commitPoint and resistedTravel | ||
| * @position Pure pieces of Item's swipe actions, beside useSwipeAction; | ||
| * tested by swipeRelease.test.ts against the UIKit lab's releases | ||
| * | ||
| * A UIKit list row decides a release by its speed first. A release at | ||
| * `releaseVelocity` or faster goes the way it moves: it opens the row when | ||
| * it opens and closes it when it closes, from anywhere short of the commit | ||
| * point. A slower one goes by where it is, against `openFraction` of the | ||
| * open width. So a flick opens the row even from a short drag and closes it | ||
| * from anywhere, and a flick never fires an action: only a release past the | ||
| * commit point does. Travel and velocity here are logical: measured toward | ||
| * the side being uncovered, whatever the writing direction. | ||
| */ | ||
|
|
||
| import {swipeActions} from '../motion/uikit/swipeActions'; | ||
|
|
||
| /** What a swipe does (Item's `swipeBehavior`). */ | ||
| export type SwipeReleaseBehavior = 'reveal' | 'commit'; | ||
|
|
||
| /** Where a release lands. */ | ||
| export type SwipeRelease = 'closed' | 'open' | 'perform'; | ||
|
|
||
| /** | ||
| * Where a release lands. `travel` (px, ≥ 0) and `velocity` (px/s) are | ||
| * logical: positive toward the side being uncovered. A release past the | ||
| * commit point fires the outermost entry, whatever its velocity; under | ||
| * `commit` nothing else fires and nothing rests. Under `reveal` a release at | ||
| * `releaseVelocity` or faster opens the row if it is opening and closes it if | ||
| * it is closing; a slower one opens it past `openFraction` of its open width | ||
| * and closes it short of that. | ||
| */ | ||
| export function resolveRelease({ | ||
| travel, | ||
| velocity, | ||
| openWidth, | ||
| commitAt, | ||
| behavior, | ||
| }: { | ||
| travel: number; | ||
| velocity: number; | ||
| openWidth: number; | ||
| commitAt: number; | ||
| behavior: SwipeReleaseBehavior; | ||
| }): SwipeRelease { | ||
| if (travel >= commitAt) { | ||
| return 'perform'; | ||
| } | ||
| if (behavior === 'commit') { | ||
| return 'closed'; | ||
| } | ||
| if (velocity >= swipeActions.releaseVelocity) { | ||
| return 'open'; | ||
| } | ||
| if (velocity <= -swipeActions.releaseVelocity) { | ||
| return 'closed'; | ||
| } | ||
| return travel >= swipeActions.openFraction * openWidth ? 'open' : 'closed'; | ||
| } | ||
|
|
||
| /** | ||
| * The commit point: `commitPastPanel` past the open width, or | ||
| * `commitFraction` of the row when that is further. | ||
| */ | ||
| export function commitPoint(openWidth: number, rowWidth: number): number { | ||
| return Math.max( | ||
| openWidth + swipeActions.commitPastPanel, | ||
| rowWidth * swipeActions.commitFraction, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * The row's travel for the finger's: one for one up to the commit point, | ||
| * `overdragResistance` per pixel past it. | ||
| */ | ||
| export function resistedTravel(raw: number, commitAt: number): number { | ||
| return raw <= commitAt | ||
| ? raw | ||
| : commitAt + (raw - commitAt) * swipeActions.overdragResistance; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under
reveala fling short of the commit point opens here, and a release past the panel's width that is moving back closes; undercommita fling short of the commit point closes. Currentspec:AST-057FR5 requires a fling to fire the outermost entry and a release past the panel's width to rest open, so beforeuseSwipeActioncalls this, FR5 needs an owner's amendment or this branch needs to conform.