Skip to content

Commit

Permalink
feat(player): keyboard animations setting in default layout
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Feb 25, 2024
1 parent 8ceae6b commit b63fa16
Show file tree
Hide file tree
Showing 17 changed files with 382 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useMediaState } from '../../../hooks/use-media-state';
import { createComputed } from '../../../hooks/use-signals';
import * as Controls from '../../ui/controls';
import { useLayoutName } from '../utils';
import { DefaultLayoutContext, i18n, useDefaultLayoutContext } from './context';
import { i18n, useDefaultLayoutContext } from './context';
import { createDefaultMediaLayout, type DefaultLayoutProps } from './media-layout';
import {
DefaultCaptionButton,
Expand Down Expand Up @@ -136,7 +136,7 @@ AudioLayout.displayName = 'AudioLayout';
* -----------------------------------------------------------------------------------------------*/

function DefaultAudioMenus({ slots }: { slots?: Slots<DefaultLayoutMenuSlotName> }) {
const { isSmallLayout, noModal } = React.useContext(DefaultLayoutContext),
const { isSmallLayout, noModal } = useDefaultLayoutContext(),
placement = noModal ? 'top end' : !isSmallLayout ? 'top end' : null;
return (
<>
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/components/layouts/default/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';

import type { WriteSignal } from 'maverick.js';

import type { DefaultLayoutProps } from './media-layout';

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

export function useDefaultLayoutContext() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ export type DefaultVideoKeyboardActionDisplayWords =
export interface DefaultVideoKeyboardActionDisplayTranslations
extends Pick<DefaultLayoutTranslations, DefaultVideoKeyboardActionDisplayWords> {}

export interface DefaultVideoKeyboardActionDisplayProps extends PrimitivePropsWithRef<'div'> {
icons: DefaultKeyboardActionIcons;
export interface DefaultVideoKeyboardActionDisplayProps
extends Omit<PrimitivePropsWithRef<'div'>, 'disabled'> {
icons?: DefaultKeyboardActionIcons;
noAnimations?: boolean;
translations?: Partial<DefaultVideoKeyboardActionDisplayTranslations> | null;
}

const DefaultVideoKeyboardActionDisplay = React.forwardRef<
HTMLElement,
DefaultVideoKeyboardActionDisplayProps
>(({ icons: Icons, translations, ...props }, forwardRef) => {
>(({ icons: Icons, noAnimations = false, translations, ...props }, forwardRef) => {
const [visible, setVisible] = React.useState(false),
[Icon, setIcon] = React.useState<any>(null),
[count, setCount] = React.useState(0),
Expand Down Expand Up @@ -80,18 +82,19 @@ const DefaultVideoKeyboardActionDisplay = React.forwardRef<
{...props}
className={className}
data-action={actionDataAttr}
data-animated={!noAnimations ? '' : null}
ref={forwardRef as any}
>
<div className="vds-kb-text-wrapper">
<div className="vds-kb-text">{$text}</div>
</div>
{Icon ? (
<div className="vds-kb-bezel" role="status" aria-label={$statusLabel} key={count}>
<div className="vds-kb-bezel" role="status" aria-label={$statusLabel} key={count}>
{Icon && !noAnimations ? (
<div className="vds-kb-icon">
<Icon />
</div>
</div>
) : null}
) : null}
</div>
</Primitive.div>
);
});
Expand Down
10 changes: 6 additions & 4 deletions packages/react/src/components/layouts/default/media-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

import { useMediaContext } from '../../../hooks/use-media-context';
import { useMediaState } from '../../../hooks/use-media-state';
import { createComputed } from '../../../hooks/use-signals';
import { createComputed, createSignal } from '../../../hooks/use-signals';
import type { PrimitivePropsWithRef } from '../../primitives/nodes';
import { DefaultLayoutContext } from './context';
import type { DefaultLayoutIcons } from './icons';
Expand Down Expand Up @@ -99,7 +99,7 @@ export interface DefaultLayoutProps<Slots = unknown> extends PrimitivePropsWithR
/**
* Whether keyboard actions should not be displayed.
*/
noKeyboardActionDisplay?: boolean;
noKeyboardAnimations?: boolean;
/**
* The playback rate options to be displayed in the settings menu.
*/
Expand Down Expand Up @@ -142,7 +142,7 @@ export function createDefaultMediaLayout({
noAudioGainSlider = false,
maxAudioGain = 300,
noGestures = false,
noKeyboardActionDisplay = false,
noKeyboardAnimations = false,
noModal = false,
noScrubGesture,
playbackRates,
Expand All @@ -166,6 +166,7 @@ export function createDefaultMediaLayout({
$smallWhen = createComputed(() => {
return isBoolean(smallLayoutWhen) ? smallLayoutWhen : smallLayoutWhen(media.player.state);
}, [smallLayoutWhen]),
userPrefersKeyboardAnimations = createSignal(true),
isMatch = $viewType === type,
isSmallLayout = $smallWhen(),
isForcedLayout = isBoolean(smallLayoutWhen),
Expand Down Expand Up @@ -194,7 +195,7 @@ export function createDefaultMediaLayout({
maxAudioGain,
noAudioGainSlider,
noGestures,
noKeyboardActionDisplay,
noKeyboardAnimations,
noModal,
noScrubGesture,
showMenuDelay,
Expand All @@ -205,6 +206,7 @@ export function createDefaultMediaLayout({
playbackRates,
thumbnails,
translations,
userPrefersKeyboardAnimations,
}}
>
{renderLayout({ streamType: $streamType, isSmallLayout, isLoadLayout })}
Expand Down
104 changes: 98 additions & 6 deletions packages/react/src/components/layouts/default/shared-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as React from 'react';

import { useSignal } from 'maverick.js/react';
import { uppercaseFirstChar } from 'maverick.js/std';
import { isTrackCaptionKind, type TooltipPlacement } from 'vidstack';
import { isKeyboardClick, uppercaseFirstChar } from 'maverick.js/std';
import { isTrackCaptionKind, type DefaultLayoutWord, type TooltipPlacement } from 'vidstack';

import { useAudioOptions } from '../../../hooks/options/use-audio-options';
import { useCaptionOptions } from '../../../hooks/options/use-caption-options';
Expand Down Expand Up @@ -694,16 +694,14 @@ export { DefaultSettingsMenu };
* -----------------------------------------------------------------------------------------------*/

function DefaultAccessibilitySubmenu() {
const $hasCaptions = useMediaState('hasCaptions'),
label = useDefaultLayoutWord('Accessibility'),
const label = useDefaultLayoutWord('Accessibility'),
{ icons: Icons } = useDefaultLayoutContext();

if (!$hasCaptions) return null;

return (
<Menu.Root className="vds-accessibility-menu vds-menu">
<DefaultSubmenuButton label={label} Icon={Icons.Menu.Accessibility} />
<Menu.Content className="vds-menu-items">
<DefaultMenuKeyboardAnimationCheckbox />
<DefaultFontSubmenu />
</Menu.Content>
</Menu.Root>
Expand All @@ -712,6 +710,100 @@ function DefaultAccessibilitySubmenu() {

DefaultAccessibilitySubmenu.displayName = 'DefaultAccessibilitySubmenu';

/* -------------------------------------------------------------------------------------------------
* DefaultMenuKeyboardAnimationCheckbox
* -----------------------------------------------------------------------------------------------*/

function DefaultMenuKeyboardAnimationCheckbox() {
const label = 'Keyboard Animations',
key = 'vds-player::keyboard-animations',
$viewType = useMediaState('viewType'),
[defaultChecked, setDefaultChecked] = React.useState(false),
{ userPrefersKeyboardAnimations } = useDefaultLayoutContext(),
translatedLabel = useDefaultLayoutWord(label);

React.useEffect(() => {
const checked = !!(localStorage.getItem(key) ?? true);
setDefaultChecked(checked);
userPrefersKeyboardAnimations.set(checked);
}, []);

if ($viewType !== 'video') return null;

function onChange(checked: boolean) {
userPrefersKeyboardAnimations.set(checked);
localStorage.setItem(key, checked ? '1' : '');
}

return (
<div className="vds-menu-item vds-menu-item-checkbox">
<div className="vds-menu-checkbox-label">{translatedLabel}</div>
<DefaultMenuCheckbox label={label} defaultChecked={defaultChecked} onChange={onChange} />
</div>
);
}

DefaultMenuKeyboardAnimationCheckbox.displayName = 'DefaultMenuKeyboardAnimationCheckbox';

/* -------------------------------------------------------------------------------------------------
* DefaultMenuCheckbox
* -----------------------------------------------------------------------------------------------*/

export interface DefaultMenuCheckboxProps {
label: DefaultLayoutWord;
defaultChecked?: boolean;
onChange?(checked: boolean): void;
}

function DefaultMenuCheckbox({
label,
defaultChecked = false,
onChange,
}: DefaultMenuCheckboxProps) {
const [isChecked, setIsChecked] = React.useState(defaultChecked),
[isActive, setIsActive] = React.useState(false),
[isDirty, setIsDirty] = React.useState(false),
ariaLabel = useDefaultLayoutWord(label);

React.useEffect(() => {
if (isDirty) return;
setIsChecked(defaultChecked);
}, [isDirty, defaultChecked]);

function onPress(event?: React.PointerEvent) {
if (event?.button === 1) return;
setIsChecked(!isChecked);
onChange?.(!isChecked);
setIsActive(false);
setIsDirty(true);
}

function onActive(event: React.PointerEvent) {
if (event.button !== 0) return;
setIsActive(true);
}

function onKeyDown(event: React.KeyboardEvent) {
if (isKeyboardClick(event.nativeEvent)) onPress();
}

return (
<div
className="vds-menu-checkbox"
role="menuitemcheckbox"
tabIndex={0}
aria-label={ariaLabel}
aria-checked={isChecked ? 'true' : 'false'}
data-active={isActive ? '' : null}
onPointerUp={onPress}
onPointerDown={onActive}
onKeyDown={onKeyDown}
/>
);
}

DefaultMenuCheckbox.displayName = 'DefaultMenuCheckbox';

/* -------------------------------------------------------------------------------------------------
* DefaultAudioSubmenu
* -----------------------------------------------------------------------------------------------*/
Expand Down
40 changes: 28 additions & 12 deletions packages/react/src/components/layouts/default/video-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from 'react';

import { useSignal } from 'maverick.js/react';

import { useMediaState } from '../../../hooks/use-media-state';
import * as Controls from '../../ui/controls';
import { Gesture } from '../../ui/gesture';
import * as Spinner from '../../ui/spinner';
import { Time } from '../../ui/time';
import { useLayoutName } from '../utils';
import { DefaultLayoutContext } from './context';
import { useDefaultLayoutContext } from './context';
import { DefaultVideoKeyboardActionDisplay } from './keyboard-action-display';
import { createDefaultMediaLayout, type DefaultLayoutProps } from './media-layout';
import {
Expand Down Expand Up @@ -86,19 +88,13 @@ export { DefaultVideoLayout };
* -----------------------------------------------------------------------------------------------*/

function DefaultVideoLargeLayout() {
const { menuGroup, noKeyboardActionDisplay, icons, translations } =
React.useContext(DefaultLayoutContext),
const { menuGroup } = useDefaultLayoutContext(),
baseSlots = useDefaultVideoLayoutSlots(),
slots = { ...baseSlots, ...baseSlots?.largeLayout };
return (
<>
<DefaultVideoGestures />
{!noKeyboardActionDisplay && icons.KeyboardAction ? (
<DefaultVideoKeyboardActionDisplay
icons={icons.KeyboardAction}
translations={translations}
/>
) : null}
<DefaultKeyboardActionDisplay />
{slot(slots, 'bufferingIndicator', <DefaultBufferingIndicator />)}
{slot(slots, 'captions', <DefaultCaptions />)}
<Controls.Root className="vds-controls">
Expand Down Expand Up @@ -220,7 +216,7 @@ DefaultVideoStartDuration.displayName = 'DefaultVideoStartDuration';
* -----------------------------------------------------------------------------------------------*/

function DefaultVideoGestures() {
const { noGestures } = React.useContext(DefaultLayoutContext);
const { noGestures } = useDefaultLayoutContext();

if (noGestures) return null;

Expand Down Expand Up @@ -261,7 +257,7 @@ export { DefaultBufferingIndicator };
* -----------------------------------------------------------------------------------------------*/

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

function DefaultVideoLoadLayout() {
const { isSmallLayout } = React.useContext(DefaultLayoutContext),
const { isSmallLayout } = useDefaultLayoutContext(),
baseSlots = useDefaultVideoLayoutSlots(),
slots = { ...baseSlots, ...baseSlots?.[isSmallLayout ? 'smallLayout' : 'largeLayout'] };
return (
Expand All @@ -313,3 +309,23 @@ function DefaultVideoLoadLayout() {
}

DefaultVideoLoadLayout.displayName = 'DefaultVideoLoadLayout';

/* -------------------------------------------------------------------------------------------------
* DefaultKeyboardActionDisplay
* -----------------------------------------------------------------------------------------------*/

function DefaultKeyboardActionDisplay() {
const { noKeyboardAnimations, icons, translations, userPrefersKeyboardAnimations } =
useDefaultLayoutContext(),
$userPrefersKeyboardAnimations = useSignal(userPrefersKeyboardAnimations);

return (
<DefaultVideoKeyboardActionDisplay
icons={icons.KeyboardAction}
noAnimations={noKeyboardAnimations || !$userPrefersKeyboardAnimations}
translations={translations}
/>
);
}

DefaultKeyboardActionDisplay.displayName = 'DefaultKeyboardActionDisplay';
13 changes: 9 additions & 4 deletions packages/vidstack/player/styles/default/keyboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

:where(.vds-kb-action.hidden) {
display: none;
opacity: 0;
}

/*
Expand Down Expand Up @@ -59,14 +59,19 @@
margin-left: calc(-1 * calc(var(--size) / 2));
margin-right: calc(-1 * calc(var(--size) / 2));
z-index: 20;
background: var(--media-kb-bezel-bg, rgba(0, 0, 0, 0.5));
opacity: 0;
border-radius: var(--media-kb-bezel-border-radius, calc(var(--size) / 2));
animation: var(--media-kb-bezel-animation, vds-bezel-fade 0.35s linear 1 normal forwards);
pointer-events: none;
}

:where(.vds-kb-action[data-animated] .vds-kb-bezel) {
opacity: 1;
background: var(--media-kb-bezel-bg, rgba(0, 0, 0, 0.5));
animation: var(--media-kb-bezel-animation, vds-bezel-fade 0.35s linear 1 normal forwards);
}

:where(.vds-kb-bezel:has(slot:empty)) {
display: none;
opacity: 0;
}

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

0 comments on commit b63fa16

Please sign in to comment.