diff --git a/packages/html/html-icons/src/index.ts b/packages/html/html-icons/src/index.ts index a0eb7777..1abb24cc 100644 --- a/packages/html/html-icons/src/index.ts +++ b/packages/html/html-icons/src/index.ts @@ -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'; \ No newline at end of file +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'; \ No newline at end of file diff --git a/packages/html/html-icons/src/media-fullscreen-enter-icon.ts b/packages/html/html-icons/src/media-fullscreen-enter-icon.ts new file mode 100644 index 00000000..b9ac632a --- /dev/null +++ b/packages/html/html-icons/src/media-fullscreen-enter-icon.ts @@ -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()} + + ${SVG_ICONS.fullscreenEnter} + `; +} + +export class MediaFullscreenEnterIcon extends MediaChromeIcon { + static getTemplateHTML = getTemplateHTML; +} + +customElements.define('media-fullscreen-enter-icon', MediaFullscreenEnterIcon); \ No newline at end of file diff --git a/packages/html/html-icons/src/media-fullscreen-exit-icon.ts b/packages/html/html-icons/src/media-fullscreen-exit-icon.ts new file mode 100644 index 00000000..1042298b --- /dev/null +++ b/packages/html/html-icons/src/media-fullscreen-exit-icon.ts @@ -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()} + + ${SVG_ICONS.fullscreenExit} + `; +} + +export class MediaFullscreenExitIcon extends MediaChromeIcon { + static getTemplateHTML = getTemplateHTML; +} + +customElements.define('media-fullscreen-exit-icon', MediaFullscreenExitIcon); \ No newline at end of file diff --git a/packages/html/html/src/components/media-fullscreen-button.ts b/packages/html/html/src/components/media-fullscreen-button.ts new file mode 100644 index 00000000..bfcea94c --- /dev/null +++ b/packages/html/html/src/components/media-fullscreen-button.ts @@ -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 = { + /** 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; diff --git a/packages/html/html/src/index.ts b/packages/html/html/src/index.ts index 2185fd2f..34ea94ba 100644 --- a/packages/html/html/src/index.ts +++ b/packages/html/html/src/index.ts @@ -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) */