Skip to content

Commit f25b7c2

Browse files
committed
feat(core): the press paint's timers follow the theme
PRESS_PROFILES name the timing.press motion roles behind their release and flash clocks (releaseRole, flashRole). The delegated press consumer resolves them under the pressed element's theme with resolveMotion, the flash when the touch lands and the release when it starts, so the fading timer lasts as long as the CSS fade the theme sets. The content-touch delay and the scroll brake stay table constants: they arbitrate, they do not move anything.
1 parent b496085 commit f25b7c2

5 files changed

Lines changed: 67 additions & 4 deletions

File tree

‎.changeset/press-paint-theme.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@astryxdesign/core': patch
3+
---
4+
5+
[experimental] The delegated press consumer's release fade and quick-tap flash follow the pressed element's theme: `PRESS_PROFILES` (in `@astryxdesign/core/gestures/experimental`) name the `timing.press` motion roles behind their clocks (`releaseRole`, `flashRole`), resolved with `resolveMotion` when the touch lands and when the release starts, so the `fading` timer matches the CSS fade a theme sets.
6+
7+
@vjeux

‎packages/core/src/gestures/PressTracker.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,16 @@ export interface PressProfile {
120120
readonly highlightDelay: PressHighlightDelay;
121121
readonly cancelTravel?: number;
122122
readonly quickTapFlash: number;
123-
/** The release (`fading`) clock a painter uses, ms. */
123+
/** The release (`fading`) clock a painter uses, ms: the table's value. */
124124
readonly releaseMs: number;
125+
/**
126+
* The `timing.press` motion role behind `releaseMs`: a painter resolves
127+
* it under the pressed element's theme, so its fade timer matches the CSS
128+
* fade the theme sets. Without it, `releaseMs` is used as is.
129+
*/
130+
readonly releaseRole?: 'buttonReleaseMs' | 'highlightOutMs';
131+
/** The motion role behind `quickTapFlash`, resolved the same way. */
132+
readonly flashRole?: 'flashMs';
125133
}
126134

127135
/** @experimental */
@@ -131,13 +139,16 @@ export const PRESS_PROFILES = {
131139
highlightDelay: 'inScrollers',
132140
quickTapFlash: PRESS.quickTapFlash.value,
133141
releaseMs: timing.press.buttonReleaseMs,
142+
releaseRole: 'buttonReleaseMs',
143+
flashRole: 'flashMs',
134144
},
135145
/** A table or collection cell: delayed highlight, cancelled by the scroll. */
136146
cell: {
137147
highlightDelay: 'always',
138148
cancelTravel: UIKIT_GESTURES.pan.hysteresis.value,
139149
quickTapFlash: 0,
140150
releaseMs: timing.press.highlightOutMs,
151+
releaseRole: 'highlightOutMs',
141152
},
142153
} as const satisfies Record<string, PressProfile>;
143154

‎packages/core/src/gestures/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ assistive-technology activation keep working.
165165
lift straight into the release, and the release takes the list's
166166
deselect clock.
167167

168+
The release and the flash are motion roles (`timing.press.buttonReleaseMs`,
169+
`.highlightOutMs`, `.flashMs`): a theme retunes them, and the delegated
170+
press consumer resolves them under the pressed element's theme when the
171+
touch lands and when the release starts, so its `fading` timer lasts as long
172+
as the CSS fade the theme sets. The arbitration windows (the content-touch
173+
delay, the scroll brake) are not motion and stay the table's.
174+
168175
`paintPress(element, 'on' | 'held' | 'fading' | 'none', {releaseMs})` is
169176
the one writer of `data-astryx-press`. It owns the release timer and writes
170177
only over values the caller may replace (by default none, `on` and

‎packages/core/src/gestures/__tests__/delegatedPress.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,31 @@ describe('installPressAttribute', () => {
183183
expect(types.every(type => type.startsWith('touch'))).toBe(true);
184184
});
185185

186+
it("fades a release and flashes a quick tap on the theme's press timings", () => {
187+
const {theme, row, cell} = mount(
188+
`<div id="theme" data-astryx-theme="slow">
189+
<div id="row" data-astryx-pressable>Row</div>
190+
<div id="cell" data-astryx-pressable="cell">Cell</div>
191+
</div>`,
192+
);
193+
theme.style.setProperty('--motion-timing-press-button-release-ms', '400ms');
194+
theme.style.setProperty('--motion-timing-press-highlight-out-ms', '320ms');
195+
touch('touchstart', row, {id: 1, x: 5, y: 5}, 0);
196+
touch('touchend', row, {id: 1, x: 5, y: 5}, 80);
197+
vi.advanceTimersByTime(399);
198+
expect(row.getAttribute('data-astryx-press')).toBe('fading');
199+
vi.advanceTimersByTime(1);
200+
expect(row.hasAttribute('data-astryx-press')).toBe(false);
201+
202+
touch('touchstart', cell, {id: 2, x: 5, y: 5}, 1000);
203+
vi.advanceTimersByTime(UIKIT_GESTURES.press.contentTouchDelay.value);
204+
touch('touchend', cell, {id: 2, x: 5, y: 5}, 1300);
205+
vi.advanceTimersByTime(319);
206+
expect(cell.getAttribute('data-astryx-press')).toBe('fading');
207+
vi.advanceTimersByTime(1);
208+
expect(cell.hasAttribute('data-astryx-press')).toBe(false);
209+
});
210+
186211
it('presses a cell with the cell profile: delayed, cancelled by travel, its own release', () => {
187212
const {row} = mount('<div id="row" data-astryx-pressable="cell">Row</div>');
188213
touch('touchstart', row, {id: 1, x: 5, y: 5}, 0);

‎packages/core/src/gestures/delegatedPress.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
* - the marker's value picks the press profile (PRESS_PROFILES): empty or
3333
* `control` for a button-like control, `cell` for a row, which waits the
3434
* content-touch delay everywhere, is cancelled by travel past the pan
35-
* hysteresis, and releases on its own clock.
35+
* hysteresis, and releases on its own clock;
36+
* - the release fade and the quick-tap flash follow the element's theme:
37+
* they are resolved from the profile's motion roles (`resolveMotion`) when
38+
* the touch lands and when the release starts, so the `fading` timer
39+
* lasts exactly as long as the CSS fade the theme sets.
3640
*
3741
* Install it once per attribute (installs are counted, and the first one's
3842
* options hold); do not install it on the same attribute as the older
@@ -42,6 +46,8 @@
4246
*/
4347

4448
import {useEffect, useState} from 'react';
49+
import {resolveMotion} from '../motion/resolveMotion';
50+
import {timing} from '../motion/uikit/timing';
4551
import {PRESSABLE_ATTRIBUTE, PRESSED_ATTRIBUTE} from '../utils/pressFeedback';
4652
import {getGestureHost} from './GestureHost';
4753
import {paintPress} from './pressPaint';
@@ -155,7 +161,11 @@ function addPressType(
155161
// control does not.
156162
paintPress(element, 'fading', {
157163
attribute: pressed,
158-
releaseMs: profile.releaseMs,
164+
// The theme's value when the release starts, as the CSS fade reads it.
165+
releaseMs:
166+
profile.releaseRole == null
167+
? profile.releaseMs
168+
: resolveMotion(element, timing.press, profile.releaseRole),
159169
});
160170
} else {
161171
paintPress(element, 'none', {attribute: pressed});
@@ -180,7 +190,10 @@ function addPressType(
180190
trackingExpansion: options.trackingExpansion,
181191
highlightDelay: profile.highlightDelay,
182192
cancelTravel: profile.cancelTravel,
183-
quickTapFlash: profile.quickTapFlash,
193+
quickTapFlash:
194+
profile.flashRole == null
195+
? profile.quickTapFlash
196+
: resolveMotion(element, timing.press, profile.flashRole),
184197
isBraking: () => host.isScrollBraking(element),
185198
onControlEvent: (event, touch) => {
186199
last = event;

0 commit comments

Comments
 (0)