feat(html): add fullscreen button component and icons

Implement HTML fullscreen button following established patterns:
- MediaFullscreenButton web component with hook-style architecture
- HTML icon components: MediaFullscreenEnterIcon, MediaFullscreenExitIcon
- State management via useFullscreenButtonState hook
- Props handling via useFullscreenButtonProps hook
- Explicit enter/exit fullscreen methods (no toggle)
- Proper accessibility with aria-label and data-tooltip

Components integrate seamlessly with existing HTML component architecture
and provide foundation for control bar integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2025-09-12 08:06:08 -07:00
committed by Christian Pillsbury
co-authored by Claude
parent e19b98b120
commit 63a340213f
5 changed files with 145 additions and 1 deletions
+3 -1
View File
@@ -2,4 +2,6 @@ export * as MediaPlayIcon from './media-play-icon.js';
export * as MediaPauseIcon from './media-pause-icon.js';
export * as MediaVolumeHighIcon from './media-volume-high-icon.js';
export * as MediaVolumeLowIcon from './media-volume-low-icon.js';
export * as MediaVolumeOffIcon from './media-volume-off-icon.js';
export * as MediaVolumeOffIcon from './media-volume-off-icon.js';
export * as MediaFullscreenEnterIcon from './media-fullscreen-enter-icon.js';
export * as MediaFullscreenExitIcon from './media-fullscreen-exit-icon.js';
@@ -0,0 +1,20 @@
import { MediaChromeIcon } from './media-chrome-icon.js';
import { SVG_ICONS } from '@vjs-10/icons';
export function getTemplateHTML() {
return /* html */ `
${MediaChromeIcon.getTemplateHTML()}
<style>
:host {
display: var(--media-fullscreen-enter-icon-display, inline-flex);
}
</style>
${SVG_ICONS.fullscreenEnter}
`;
}
export class MediaFullscreenEnterIcon extends MediaChromeIcon {
static getTemplateHTML = getTemplateHTML;
}
customElements.define('media-fullscreen-enter-icon', MediaFullscreenEnterIcon);
@@ -0,0 +1,20 @@
import { MediaChromeIcon } from './media-chrome-icon.js';
import { SVG_ICONS } from '@vjs-10/icons';
export function getTemplateHTML() {
return /* html */ `
${MediaChromeIcon.getTemplateHTML()}
<style>
:host {
display: var(--media-fullscreen-exit-icon-display, inline-flex);
}
</style>
${SVG_ICONS.fullscreenExit}
`;
}
export class MediaFullscreenExitIcon extends MediaChromeIcon {
static getTemplateHTML = getTemplateHTML;
}
customElements.define('media-fullscreen-exit-icon', MediaFullscreenExitIcon);
@@ -0,0 +1,101 @@
import {
toConnectedHTMLComponent,
StateHook,
PropsHook,
} from '../utils/component-factory';
import { MediaChromeButton } from './media-chrome-button';
import { fullscreenButtonStateDefinition } from '@vjs-10/media-store';
export class FullscreenButtonBase extends MediaChromeButton {
_state:
| {
fullscreen: boolean;
requestEnterFullscreen: () => void;
requestExitFullscreen: () => void;
}
| undefined;
handleEvent(event: Event) {
const { type } = event;
const state = this._state;
if (state && type === 'click') {
if (state.fullscreen) {
state.requestExitFullscreen();
} else {
state.requestEnterFullscreen();
}
}
}
get fullscreen() {
return this._state?.fullscreen;
}
_update(props: any, state: any, _mediaStore?: any) {
this._state = state;
/** @TODO Follow up with React vs. W.C. data-* attributes discrepancies (CJP) */
// Make generic
this.toggleAttribute('data-fullscreen', props['data-fullscreen']);
this.setAttribute('role', props['role']);
this.setAttribute('aria-label', props['aria-label']);
this.setAttribute('data-tooltip', props['data-tooltip']);
}
}
/**
* FullscreenButton state hook - equivalent to React's useFullscreenButtonState
* Handles media store state subscription and transformation
*/
export const useFullscreenButtonState: StateHook<{ fullscreen: boolean }> = {
keys: fullscreenButtonStateDefinition.keys,
transform: (rawState, mediaStore) => ({
...fullscreenButtonStateDefinition.stateTransform(rawState),
...fullscreenButtonStateDefinition.createRequestMethods(
mediaStore.dispatch,
),
}),
};
/**
* FullscreenButton props hook - equivalent to React's useFullscreenButtonProps
* Handles element attributes and properties based on state
*/
export const useFullscreenButtonProps: PropsHook<{ fullscreen: boolean }> = (
state,
_element,
) => {
const baseProps: Record<string, any> = {
/** data attributes/props */
['data-fullscreen']: state.fullscreen,
/** @TODO Need another state provider in core for i18n (CJP) */
/** aria attributes/props */
role: 'button',
['aria-label']: state.fullscreen ? 'exit fullscreen' : 'enter fullscreen',
/** tooltip */
['data-tooltip']: state.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
/** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
/** external props spread last to allow for overriding */
// ...props,
};
return baseProps;
};
/**
* Connected FullscreenButton component using hook-style architecture
* Equivalent to React's FullscreenButton = toConnectedComponent(...)
*/
export const FullscreenButton = toConnectedHTMLComponent(
FullscreenButtonBase,
useFullscreenButtonState,
useFullscreenButtonProps,
'FullscreenButton',
);
// NOTE: In this architecture it will be important to decouple component class definitions from their registration in the CustomElementsRegistry. (CJP)
if (!globalThis.customElements.get('media-fullscreen-button')) {
// @ts-ignore - Custom element constructor compatibility
globalThis.customElements.define('media-fullscreen-button', FullscreenButton);
}
export default FullscreenButton;
+1
View File
@@ -5,6 +5,7 @@ export * as MediaThemeDefault from './skins/media-skin-default.js';
export { PlayButton } from './components/media-play-button.js';
export { MuteButton } from './components/media-mute-button.js';
export { VolumeRange } from './components/media-volume-range.js';
export { FullscreenButton } from './components/media-fullscreen-button.js';
export function defineVjsPlayer() {
/** @TODO - Reimplement me (at least as a POC) (CJP) */