Skip to content

Commit b63fa16

Browse files
committed
feat(player): keyboard animations setting in default layout
1 parent 8ceae6b commit b63fa16

17 files changed

Lines changed: 382 additions & 84 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { useMediaState } from '../../../hooks/use-media-state';
1616
import { createComputed } from '../../../hooks/use-signals';
1717
import * as Controls from '../../ui/controls';
1818
import { useLayoutName } from '../utils';
19-
import { DefaultLayoutContext, i18n, useDefaultLayoutContext } from './context';
19+
import { i18n, useDefaultLayoutContext } from './context';
2020
import { createDefaultMediaLayout, type DefaultLayoutProps } from './media-layout';
2121
import {
2222
DefaultCaptionButton,
@@ -136,7 +136,7 @@ AudioLayout.displayName = 'AudioLayout';
136136
* -----------------------------------------------------------------------------------------------*/
137137

138138
function DefaultAudioMenus({ slots }: { slots?: Slots<DefaultLayoutMenuSlotName> }) {
139-
const { isSmallLayout, noModal } = React.useContext(DefaultLayoutContext),
139+
const { isSmallLayout, noModal } = useDefaultLayoutContext(),
140140
placement = noModal ? 'top end' : !isSmallLayout ? 'top end' : null;
141141
return (
142142
<>

packages/react/src/components/layouts/default/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as React from 'react';
22

3+
import type { WriteSignal } from 'maverick.js';
4+
35
import type { DefaultLayoutProps } from './media-layout';
46

57
export const DefaultLayoutContext = React.createContext<DefaultLayoutContext>({} as any);
@@ -8,6 +10,7 @@ DefaultLayoutContext.displayName = 'DefaultLayoutContext';
810
interface DefaultLayoutContext extends DefaultLayoutProps {
911
menuContainer?: React.RefObject<HTMLElement | null>;
1012
isSmallLayout: boolean;
13+
userPrefersKeyboardAnimations: WriteSignal<boolean>;
1114
}
1215

1316
export function useDefaultLayoutContext() {

packages/react/src/components/layouts/default/keyboard-action-display.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ export type DefaultVideoKeyboardActionDisplayWords =
2626
export interface DefaultVideoKeyboardActionDisplayTranslations
2727
extends Pick<DefaultLayoutTranslations, DefaultVideoKeyboardActionDisplayWords> {}
2828

29-
export interface DefaultVideoKeyboardActionDisplayProps extends PrimitivePropsWithRef<'div'> {
30-
icons: DefaultKeyboardActionIcons;
29+
export interface DefaultVideoKeyboardActionDisplayProps
30+
extends Omit<PrimitivePropsWithRef<'div'>, 'disabled'> {
31+
icons?: DefaultKeyboardActionIcons;
32+
noAnimations?: boolean;
3133
translations?: Partial<DefaultVideoKeyboardActionDisplayTranslations> | null;
3234
}
3335

3436
const DefaultVideoKeyboardActionDisplay = React.forwardRef<
3537
HTMLElement,
3638
DefaultVideoKeyboardActionDisplayProps
37-
>(({ icons: Icons, translations, ...props }, forwardRef) => {
39+
>(({ icons: Icons, noAnimations = false, translations, ...props }, forwardRef) => {
3840
const [visible, setVisible] = React.useState(false),
3941
[Icon, setIcon] = React.useState<any>(null),
4042
[count, setCount] = React.useState(0),
@@ -80,18 +82,19 @@ const DefaultVideoKeyboardActionDisplay = React.forwardRef<
8082
{...props}
8183
className={className}
8284
data-action={actionDataAttr}
85+
data-animated={!noAnimations ? '' : null}
8386
ref={forwardRef as any}
8487
>
8588
<div className="vds-kb-text-wrapper">
8689
<div className="vds-kb-text">{$text}</div>
8790
</div>
88-
{Icon ? (
89-
<div className="vds-kb-bezel" role="status" aria-label={$statusLabel} key={count}>
91+
<div className="vds-kb-bezel" role="status" aria-label={$statusLabel} key={count}>
92+
{Icon && !noAnimations ? (
9093
<div className="vds-kb-icon">
9194
<Icon />
9295
</div>
93-
</div>
94-
) : null}
96+
) : null}
97+
</div>
9598
</Primitive.div>
9699
);
97100
});

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111

1212
import { useMediaContext } from '../../../hooks/use-media-context';
1313
import { useMediaState } from '../../../hooks/use-media-state';
14-
import { createComputed } from '../../../hooks/use-signals';
14+
import { createComputed, createSignal } from '../../../hooks/use-signals';
1515
import type { PrimitivePropsWithRef } from '../../primitives/nodes';
1616
import { DefaultLayoutContext } from './context';
1717
import type { DefaultLayoutIcons } from './icons';
@@ -99,7 +99,7 @@ export interface DefaultLayoutProps<Slots = unknown> extends PrimitivePropsWithR
9999
/**
100100
* Whether keyboard actions should not be displayed.
101101
*/
102-
noKeyboardActionDisplay?: boolean;
102+
noKeyboardAnimations?: boolean;
103103
/**
104104
* The playback rate options to be displayed in the settings menu.
105105
*/
@@ -142,7 +142,7 @@ export function createDefaultMediaLayout({
142142
noAudioGainSlider = false,
143143
maxAudioGain = 300,
144144
noGestures = false,
145-
noKeyboardActionDisplay = false,
145+
noKeyboardAnimations = false,
146146
noModal = false,
147147
noScrubGesture,
148148
playbackRates,
@@ -166,6 +166,7 @@ export function createDefaultMediaLayout({
166166
$smallWhen = createComputed(() => {
167167
return isBoolean(smallLayoutWhen) ? smallLayoutWhen : smallLayoutWhen(media.player.state);
168168
}, [smallLayoutWhen]),
169+
userPrefersKeyboardAnimations = createSignal(true),
169170
isMatch = $viewType === type,
170171
isSmallLayout = $smallWhen(),
171172
isForcedLayout = isBoolean(smallLayoutWhen),
@@ -194,7 +195,7 @@ export function createDefaultMediaLayout({
194195
maxAudioGain,
195196
noAudioGainSlider,
196197
noGestures,
197-
noKeyboardActionDisplay,
198+
noKeyboardAnimations,
198199
noModal,
199200
noScrubGesture,
200201
showMenuDelay,
@@ -205,6 +206,7 @@ export function createDefaultMediaLayout({
205206
playbackRates,
206207
thumbnails,
207208
translations,
209+
userPrefersKeyboardAnimations,
208210
}}
209211
>
210212
{renderLayout({ streamType: $streamType, isSmallLayout, isLoadLayout })}

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

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import * as React from 'react';
22

33
import { useSignal } from 'maverick.js/react';
4-
import { uppercaseFirstChar } from 'maverick.js/std';
5-
import { isTrackCaptionKind, type TooltipPlacement } from 'vidstack';
4+
import { isKeyboardClick, uppercaseFirstChar } from 'maverick.js/std';
5+
import { isTrackCaptionKind, type DefaultLayoutWord, type TooltipPlacement } from 'vidstack';
66

77
import { useAudioOptions } from '../../../hooks/options/use-audio-options';
88
import { useCaptionOptions } from '../../../hooks/options/use-caption-options';
@@ -694,16 +694,14 @@ export { DefaultSettingsMenu };
694694
* -----------------------------------------------------------------------------------------------*/
695695

696696
function DefaultAccessibilitySubmenu() {
697-
const $hasCaptions = useMediaState('hasCaptions'),
698-
label = useDefaultLayoutWord('Accessibility'),
697+
const label = useDefaultLayoutWord('Accessibility'),
699698
{ icons: Icons } = useDefaultLayoutContext();
700699

701-
if (!$hasCaptions) return null;
702-
703700
return (
704701
<Menu.Root className="vds-accessibility-menu vds-menu">
705702
<DefaultSubmenuButton label={label} Icon={Icons.Menu.Accessibility} />
706703
<Menu.Content className="vds-menu-items">
704+
<DefaultMenuKeyboardAnimationCheckbox />
707705
<DefaultFontSubmenu />
708706
</Menu.Content>
709707
</Menu.Root>
@@ -712,6 +710,100 @@ function DefaultAccessibilitySubmenu() {
712710

713711
DefaultAccessibilitySubmenu.displayName = 'DefaultAccessibilitySubmenu';
714712

713+
/* -------------------------------------------------------------------------------------------------
714+
* DefaultMenuKeyboardAnimationCheckbox
715+
* -----------------------------------------------------------------------------------------------*/
716+
717+
function DefaultMenuKeyboardAnimationCheckbox() {
718+
const label = 'Keyboard Animations',
719+
key = 'vds-player::keyboard-animations',
720+
$viewType = useMediaState('viewType'),
721+
[defaultChecked, setDefaultChecked] = React.useState(false),
722+
{ userPrefersKeyboardAnimations } = useDefaultLayoutContext(),
723+
translatedLabel = useDefaultLayoutWord(label);
724+
725+
React.useEffect(() => {
726+
const checked = !!(localStorage.getItem(key) ?? true);
727+
setDefaultChecked(checked);
728+
userPrefersKeyboardAnimations.set(checked);
729+
}, []);
730+
731+
if ($viewType !== 'video') return null;
732+
733+
function onChange(checked: boolean) {
734+
userPrefersKeyboardAnimations.set(checked);
735+
localStorage.setItem(key, checked ? '1' : '');
736+
}
737+
738+
return (
739+
<div className="vds-menu-item vds-menu-item-checkbox">
740+
<div className="vds-menu-checkbox-label">{translatedLabel}</div>
741+
<DefaultMenuCheckbox label={label} defaultChecked={defaultChecked} onChange={onChange} />
742+
</div>
743+
);
744+
}
745+
746+
DefaultMenuKeyboardAnimationCheckbox.displayName = 'DefaultMenuKeyboardAnimationCheckbox';
747+
748+
/* -------------------------------------------------------------------------------------------------
749+
* DefaultMenuCheckbox
750+
* -----------------------------------------------------------------------------------------------*/
751+
752+
export interface DefaultMenuCheckboxProps {
753+
label: DefaultLayoutWord;
754+
defaultChecked?: boolean;
755+
onChange?(checked: boolean): void;
756+
}
757+
758+
function DefaultMenuCheckbox({
759+
label,
760+
defaultChecked = false,
761+
onChange,
762+
}: DefaultMenuCheckboxProps) {
763+
const [isChecked, setIsChecked] = React.useState(defaultChecked),
764+
[isActive, setIsActive] = React.useState(false),
765+
[isDirty, setIsDirty] = React.useState(false),
766+
ariaLabel = useDefaultLayoutWord(label);
767+
768+
React.useEffect(() => {
769+
if (isDirty) return;
770+
setIsChecked(defaultChecked);
771+
}, [isDirty, defaultChecked]);
772+
773+
function onPress(event?: React.PointerEvent) {
774+
if (event?.button === 1) return;
775+
setIsChecked(!isChecked);
776+
onChange?.(!isChecked);
777+
setIsActive(false);
778+
setIsDirty(true);
779+
}
780+
781+
function onActive(event: React.PointerEvent) {
782+
if (event.button !== 0) return;
783+
setIsActive(true);
784+
}
785+
786+
function onKeyDown(event: React.KeyboardEvent) {
787+
if (isKeyboardClick(event.nativeEvent)) onPress();
788+
}
789+
790+
return (
791+
<div
792+
className="vds-menu-checkbox"
793+
role="menuitemcheckbox"
794+
tabIndex={0}
795+
aria-label={ariaLabel}
796+
aria-checked={isChecked ? 'true' : 'false'}
797+
data-active={isActive ? '' : null}
798+
onPointerUp={onPress}
799+
onPointerDown={onActive}
800+
onKeyDown={onKeyDown}
801+
/>
802+
);
803+
}
804+
805+
DefaultMenuCheckbox.displayName = 'DefaultMenuCheckbox';
806+
715807
/* -------------------------------------------------------------------------------------------------
716808
* DefaultAudioSubmenu
717809
* -----------------------------------------------------------------------------------------------*/

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

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import * as React from 'react';
22

3+
import { useSignal } from 'maverick.js/react';
4+
35
import { useMediaState } from '../../../hooks/use-media-state';
46
import * as Controls from '../../ui/controls';
57
import { Gesture } from '../../ui/gesture';
68
import * as Spinner from '../../ui/spinner';
79
import { Time } from '../../ui/time';
810
import { useLayoutName } from '../utils';
9-
import { DefaultLayoutContext } from './context';
11+
import { useDefaultLayoutContext } from './context';
1012
import { DefaultVideoKeyboardActionDisplay } from './keyboard-action-display';
1113
import { createDefaultMediaLayout, type DefaultLayoutProps } from './media-layout';
1214
import {
@@ -86,19 +88,13 @@ export { DefaultVideoLayout };
8688
* -----------------------------------------------------------------------------------------------*/
8789

8890
function DefaultVideoLargeLayout() {
89-
const { menuGroup, noKeyboardActionDisplay, icons, translations } =
90-
React.useContext(DefaultLayoutContext),
91+
const { menuGroup } = useDefaultLayoutContext(),
9192
baseSlots = useDefaultVideoLayoutSlots(),
9293
slots = { ...baseSlots, ...baseSlots?.largeLayout };
9394
return (
9495
<>
9596
<DefaultVideoGestures />
96-
{!noKeyboardActionDisplay && icons.KeyboardAction ? (
97-
<DefaultVideoKeyboardActionDisplay
98-
icons={icons.KeyboardAction}
99-
translations={translations}
100-
/>
101-
) : null}
97+
<DefaultKeyboardActionDisplay />
10298
{slot(slots, 'bufferingIndicator', <DefaultBufferingIndicator />)}
10399
{slot(slots, 'captions', <DefaultCaptions />)}
104100
<Controls.Root className="vds-controls">
@@ -220,7 +216,7 @@ DefaultVideoStartDuration.displayName = 'DefaultVideoStartDuration';
220216
* -----------------------------------------------------------------------------------------------*/
221217

222218
function DefaultVideoGestures() {
223-
const { noGestures } = React.useContext(DefaultLayoutContext);
219+
const { noGestures } = useDefaultLayoutContext();
224220

225221
if (noGestures) return null;
226222

@@ -261,7 +257,7 @@ export { DefaultBufferingIndicator };
261257
* -----------------------------------------------------------------------------------------------*/
262258

263259
function DefaultVideoMenus({ slots }: { slots?: Slots<DefaultLayoutMenuSlotName> }) {
264-
const { isSmallLayout, noModal, menuGroup } = React.useContext(DefaultLayoutContext),
260+
const { isSmallLayout, noModal, menuGroup } = useDefaultLayoutContext(),
265261
side = menuGroup === 'top' || isSmallLayout ? 'bottom' : ('top' as const),
266262
tooltip = `${side} end` as const,
267263
placement = noModal
@@ -301,7 +297,7 @@ DefaultVideoMenus.displayName = 'DefaultVideoMenus';
301297
* -----------------------------------------------------------------------------------------------*/
302298

303299
function DefaultVideoLoadLayout() {
304-
const { isSmallLayout } = React.useContext(DefaultLayoutContext),
300+
const { isSmallLayout } = useDefaultLayoutContext(),
305301
baseSlots = useDefaultVideoLayoutSlots(),
306302
slots = { ...baseSlots, ...baseSlots?.[isSmallLayout ? 'smallLayout' : 'largeLayout'] };
307303
return (
@@ -313,3 +309,23 @@ function DefaultVideoLoadLayout() {
313309
}
314310

315311
DefaultVideoLoadLayout.displayName = 'DefaultVideoLoadLayout';
312+
313+
/* -------------------------------------------------------------------------------------------------
314+
* DefaultKeyboardActionDisplay
315+
* -----------------------------------------------------------------------------------------------*/
316+
317+
function DefaultKeyboardActionDisplay() {
318+
const { noKeyboardAnimations, icons, translations, userPrefersKeyboardAnimations } =
319+
useDefaultLayoutContext(),
320+
$userPrefersKeyboardAnimations = useSignal(userPrefersKeyboardAnimations);
321+
322+
return (
323+
<DefaultVideoKeyboardActionDisplay
324+
icons={icons.KeyboardAction}
325+
noAnimations={noKeyboardAnimations || !$userPrefersKeyboardAnimations}
326+
translations={translations}
327+
/>
328+
);
329+
}
330+
331+
DefaultKeyboardActionDisplay.displayName = 'DefaultKeyboardActionDisplay';

packages/vidstack/player/styles/default/keyboard.css

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
:where(.vds-kb-action.hidden) {
8-
display: none;
8+
opacity: 0;
99
}
1010

1111
/*
@@ -59,14 +59,19 @@
5959
margin-left: calc(-1 * calc(var(--size) / 2));
6060
margin-right: calc(-1 * calc(var(--size) / 2));
6161
z-index: 20;
62-
background: var(--media-kb-bezel-bg, rgba(0, 0, 0, 0.5));
62+
opacity: 0;
6363
border-radius: var(--media-kb-bezel-border-radius, calc(var(--size) / 2));
64-
animation: var(--media-kb-bezel-animation, vds-bezel-fade 0.35s linear 1 normal forwards);
6564
pointer-events: none;
6665
}
6766

67+
:where(.vds-kb-action[data-animated] .vds-kb-bezel) {
68+
opacity: 1;
69+
background: var(--media-kb-bezel-bg, rgba(0, 0, 0, 0.5));
70+
animation: var(--media-kb-bezel-animation, vds-bezel-fade 0.35s linear 1 normal forwards);
71+
}
72+
6873
:where(.vds-kb-bezel:has(slot:empty)) {
69-
display: none;
74+
opacity: 0;
7075
}
7176

7277
:where(.vds-kb-action[data-action='seek-forward'] .vds-kb-bezel) {

0 commit comments

Comments
 (0)