Skip to content

Commit 5de514f

Browse files
committed
feat(player): new noScrubGesture prop on default layout
1 parent d533263 commit 5de514f

8 files changed

Lines changed: 87 additions & 47 deletions

File tree

packages/react/src/components/layouts/default/media-layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export interface DefaultLayoutProps<Slots = unknown> extends PrimitivePropsWithR
7272
* enabled by default as it provides a better user experience for touch devices.
7373
*/
7474
noModal?: boolean;
75+
/**
76+
* Whether to disable scrubbing by touch swiping left or right on the player canvas.
77+
*/
78+
noScrubGesture: boolean;
7579
/**
7680
* The minimum width of the slider to start displaying slider chapters when available.
7781
*/
@@ -126,6 +130,7 @@ export function createDefaultMediaLayout({
126130
noGestures = false,
127131
noKeyboardActionDisplay = false,
128132
noModal = false,
133+
noScrubGesture,
129134
seekStep = 10,
130135
showMenuDelay,
131136
showTooltipDelay = 700,
@@ -160,6 +165,7 @@ export function createDefaultMediaLayout({
160165
className={`vds-${type}-layout` + (className ? ` ${className}` : '')}
161166
data-match={isMatch ? '' : null}
162167
data-size={isSmallLayout ? 'sm' : null}
168+
data-no-scrub-gesture={noScrubGesture ? '' : null}
163169
ref={forwardRef}
164170
>
165171
{canRender && isMatch ? (
@@ -173,6 +179,7 @@ export function createDefaultMediaLayout({
173179
noGestures,
174180
noKeyboardActionDisplay,
175181
noModal,
182+
noScrubGesture,
176183
showMenuDelay,
177184
showTooltipDelay,
178185
sliderChaptersMinWidth,

packages/react/src/components/layouts/default/shared-layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ function DefaultTimeSlider() {
331331
const [instance, setInstance] = React.useState<TimeSliderInstance | null>(null),
332332
[width, setWidth] = React.useState(0),
333333
$src = useMediaState('currentSrc'),
334-
{ thumbnails, sliderChaptersMinWidth, disableTimeSlider, seekStep } = useDefaultLayoutContext(),
334+
{ thumbnails, sliderChaptersMinWidth, disableTimeSlider, seekStep, noScrubGesture } =
335+
useDefaultLayoutContext(),
335336
label = useDefaultLayoutWord('Seek'),
336337
$RemotionSliderThumbnail = useSignal(RemotionSliderThumbnail);
337338

@@ -347,6 +348,7 @@ function DefaultTimeSlider() {
347348
className="vds-time-slider vds-slider"
348349
aria-label={label}
349350
disabled={disableTimeSlider}
351+
noSwipeGesture={noScrubGesture}
350352
keyStep={seekStep}
351353
ref={setInstance}
352354
>

packages/vidstack/player/styles/default/layouts/video.css

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -400,25 +400,25 @@
400400
visibility: hidden;
401401
}
402402

403-
:where([data-preview] .vds-video-layout[data-size='sm'])
404-
:where(
405-
.vds-button,
406-
.vds-slider:not(.vds-time-slider),
407-
.vds-time,
408-
.vds-chapter-title,
409-
.vds-time-divider,
410-
.vds-captions,
411-
.vds-live-button
412-
) {
413-
opacity: 0;
414-
}
415-
416403
:where(.vds-video-layout[data-size='sm'] .vds-time-slider) {
417404
transition: transform 0.1s linear;
418405
}
419406

420407
@media (pointer: coarse) {
421-
:where([data-preview] .vds-video-layout[data-size='sm'] .vds-time-slider) {
408+
:where([data-preview] .vds-video-layout:not([data-no-scrub-gesture]))
409+
:where(
410+
.vds-button,
411+
.vds-slider:not(.vds-time-slider),
412+
.vds-time,
413+
.vds-chapter-title,
414+
.vds-time-divider,
415+
.vds-captions,
416+
.vds-live-button
417+
) {
418+
opacity: 0;
419+
}
420+
421+
:where([data-preview] .vds-video-layout:not([data-no-scrub-gesture]) .vds-time-slider) {
422422
--track-height: var(--video-sm-slider-focus-track-height, 12px);
423423
transform: translateY(-6px);
424424
transition: transform 0.1s linear;

packages/vidstack/src/components/layouts/default/default-layout.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class DefaultLayout extends Component<DefaultLayoutProps> {
4040
this.setAttributes({
4141
'data-match': this._when,
4242
'data-size': () => (this._smallWhen() ? 'sm' : null),
43+
'data-no-scrub-gesture': this.$props.noScrubGesture,
4344
});
4445

4546
const self = this;

packages/vidstack/src/components/layouts/default/props.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import type { ThumbnailSrc } from '../../ui/thumbnails/thumbnail-loader';
33
import type { DefaultLayoutTranslations } from './translations';
44

55
export const defaultLayoutProps: DefaultLayoutProps = {
6-
when: false,
7-
smallWhen: false,
8-
thumbnails: null,
96
customIcons: false,
10-
translations: null,
11-
menuGroup: 'bottom',
12-
noModal: false,
13-
sliderChaptersMinWidth: 325,
147
disableTimeSlider: false,
8+
menuGroup: 'bottom',
159
noGestures: false,
1610
noKeyboardActionDisplay: false,
11+
noModal: false,
12+
noScrubGesture: false,
1713
seekStep: 10,
14+
sliderChaptersMinWidth: 325,
15+
smallWhen: false,
16+
thumbnails: null,
17+
translations: null,
18+
when: false,
1819
};
1920

2021
export interface DefaultLayoutProps {
@@ -52,6 +53,10 @@ export interface DefaultLayoutProps {
5253
* enabled by default as it provides a better user experience for touch devices.
5354
*/
5455
noModal: boolean;
56+
/**
57+
* Whether to disable scrubbing by touch swiping left or right on the player canvas.
58+
*/
59+
noScrubGesture: boolean;
5560
/**
5661
* The minimum width of the slider to start displaying slider chapters when available.
5762
*/

packages/vidstack/src/components/ui/sliders/slider/events-controller.ts

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import throttle from 'just-throttle';
2-
import { effect, ViewController } from 'maverick.js';
2+
import { effect, ViewController, type ReadSignal } from 'maverick.js';
33
import { isNull, isNumber, isUndefined, listenEvent } from 'maverick.js/std';
44

55
import type { MediaContext } from '../../../../core';
66
import { isTouchPinchEvent } from '../../../../utils/dom';
7-
import { IS_SAFARI } from '../../../../utils/support';
87
import type {
98
SliderDragEndEvent,
109
SliderDragStartEvent,
@@ -31,7 +30,7 @@ const SliderKeyDirection = {
3130
} as const;
3231

3332
export interface SliderEventDelegate {
34-
_swipeGesture?: boolean;
33+
_swipeGesture?: ReadSignal<boolean>;
3534
_isDisabled(): boolean;
3635
_getStep(): number;
3736
_getKeyStep(): number;
@@ -57,20 +56,30 @@ export class SliderEventsController extends ViewController<
5756
protected override onConnect() {
5857
effect(this._attachEventListeners.bind(this));
5958
effect(this._attachPointerListeners.bind(this));
60-
if (this._delegate._swipeGesture) {
61-
const provider = this._media.player.el?.querySelector(
62-
'media-provider,[data-media-provider]',
63-
) as HTMLElement | null;
64-
if (provider) {
65-
this._provider = provider;
66-
listenEvent(provider, 'touchstart', this._onTouchStart.bind(this), {
67-
passive: true,
68-
});
69-
listenEvent(provider, 'touchmove', this._onTouchMove.bind(this), {
70-
passive: false,
71-
});
72-
}
59+
if (this._delegate._swipeGesture) effect(this._watchSwipeGesture.bind(this));
60+
}
61+
62+
private _watchSwipeGesture() {
63+
const { pointer } = this._media.$state;
64+
65+
if (pointer() !== 'coarse' || !this._delegate._swipeGesture!()) {
66+
this._provider = null;
67+
return;
7368
}
69+
70+
this._provider = this._media.player.el?.querySelector(
71+
'media-provider,[data-media-provider]',
72+
) as HTMLElement | null;
73+
74+
if (!this._provider) return;
75+
76+
listenEvent(this._provider, 'touchstart', this._onTouchStart.bind(this), {
77+
passive: true,
78+
});
79+
80+
listenEvent(this._provider, 'touchmove', this._onTouchMove.bind(this), {
81+
passive: false,
82+
});
7483
}
7584

7685
private _provider: HTMLElement | null = null;
@@ -88,12 +97,14 @@ export class SliderEventsController extends ViewController<
8897
yDiff = touch.clientY - this._touch.clientY,
8998
isDragging = this.$state.dragging();
9099

91-
if (!isDragging && Math.abs(yDiff) > 20) {
100+
if (!isDragging && Math.abs(yDiff) > 5) {
92101
return;
93102
}
94103

95104
if (isDragging) return;
96105

106+
event.preventDefault();
107+
97108
if (Math.abs(xDiff) > 20) {
98109
this._touch = touch;
99110
this._touchStartValue = this.$state.value();
@@ -116,11 +127,9 @@ export class SliderEventsController extends ViewController<
116127
if (this._delegate._isDisabled() || !this.$state.dragging()) return;
117128
listenEvent(document, 'pointerup', this._onDocumentPointerUp.bind(this));
118129
listenEvent(document, 'pointermove', this._onDocumentPointerMove.bind(this));
119-
if (IS_SAFARI) {
120-
listenEvent(document, 'touchmove', this._onDocumentTouchMove.bind(this), {
121-
passive: false,
122-
});
123-
}
130+
listenEvent(document, 'touchmove', this._onDocumentTouchMove.bind(this), {
131+
passive: false,
132+
});
124133
}
125134

126135
private _onFocus() {

packages/vidstack/src/components/ui/sliders/time-slider/time-slider.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class TimeSlider extends Component<
6060
keyStep: 5,
6161
shiftKeyMultiplier: 2,
6262
pauseWhileDragging: false,
63+
noSwipeGesture: false,
6364
seekingRequestThrottle: 100,
6465
};
6566

@@ -71,8 +72,10 @@ export class TimeSlider extends Component<
7172

7273
constructor() {
7374
super();
75+
76+
const { noSwipeGesture } = this.$props;
7477
new SliderController({
75-
_swipeGesture: true,
78+
_swipeGesture: () => !noSwipeGesture(),
7679
_getStep: this._getStep.bind(this),
7780
_getKeyStep: this._getKeyStep.bind(this),
7881
_isDisabled: this._isDisabled.bind(this),
@@ -291,6 +294,12 @@ export interface TimeSliderProps extends SliderControllerProps {
291294
* The amount of milliseconds to throttle media seeking request events being dispatched.
292295
*/
293296
seekingRequestThrottle: number;
297+
/**
298+
* Whether touch swiping left or right on the player canvas should activate the time slider. This
299+
* gesture makes it easier for touch users to drag anywhere on the player left or right to
300+
* seek backwards or forwards, without directly interacting with time slider.
301+
*/
302+
noSwipeGesture: boolean;
294303
}
295304

296305
interface ThrottledSeeking {

packages/vidstack/src/elements/define/layouts/default/shared-layout.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,14 @@ export function DefaultVolumeSlider({ orientation }: { orientation?: SliderOrien
242242
export function DefaultTimeSlider() {
243243
const $ref = signal<Element | undefined>(undefined),
244244
$width = signal(0),
245-
{ thumbnails, translations, sliderChaptersMinWidth, disableTimeSlider, seekStep } =
246-
useDefaultLayoutContext(),
245+
{
246+
thumbnails,
247+
translations,
248+
sliderChaptersMinWidth,
249+
disableTimeSlider,
250+
seekStep,
251+
noScrubGesture,
252+
} = useDefaultLayoutContext(),
247253
$label = $i18n(translations, 'Seek'),
248254
$isDisabled = $signal(disableTimeSlider),
249255
$isChaptersDisabled = $signal(() => $width() < sliderChaptersMinWidth()),
@@ -260,6 +266,7 @@ export function DefaultTimeSlider() {
260266
aria-label=${$label}
261267
key-step=${$signal(seekStep)}
262268
?disabled=${$isDisabled}
269+
?no-swipe-gesture=${$signal(noScrubGesture)}
263270
${ref($ref.set)}
264271
>
265272
<media-slider-chapters class="vds-slider-chapters" ?disabled=${$isChaptersDisabled}>

0 commit comments

Comments
 (0)