diff --git a/examples/html-demo/src/main.ts b/examples/html-demo/src/main.ts
index 97a682c4..6aa81710 100644
--- a/examples/html-demo/src/main.ts
+++ b/examples/html-demo/src/main.ts
@@ -4,8 +4,11 @@ defineVjsPlayer();
document.body.innerHTML = `
-
-
+
+
+
+
+
-`;
\ No newline at end of file
+`;
diff --git a/packages/core/icons/rollup.config.js b/packages/core/icons/rollup.config.js
index a88370aa..ad6f6980 100644
--- a/packages/core/icons/rollup.config.js
+++ b/packages/core/icons/rollup.config.js
@@ -12,6 +12,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -43,6 +46,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -66,4 +72,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/core/media-store/rollup.config.js b/packages/core/media-store/rollup.config.js
index 0fe567ba..e95bc7c6 100644
--- a/packages/core/media-store/rollup.config.js
+++ b/packages/core/media-store/rollup.config.js
@@ -11,6 +11,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -39,6 +42,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -59,4 +65,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/html/html-icons/rollup.config.js b/packages/html/html-icons/rollup.config.js
index 0fe567ba..e95bc7c6 100644
--- a/packages/html/html-icons/rollup.config.js
+++ b/packages/html/html-icons/rollup.config.js
@@ -11,6 +11,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -39,6 +42,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -59,4 +65,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/html/html/package.json b/packages/html/html/package.json
index e3942d09..a9c69b22 100644
--- a/packages/html/html/package.json
+++ b/packages/html/html/package.json
@@ -17,6 +17,7 @@
],
"scripts": {
"build": "rollup -c && tsc --project tsconfig.build.json",
+ "dev": "rollup -c -w",
"test": "echo \"No tests yet\"",
"clean": "rm -rf dist"
},
@@ -44,4 +45,4 @@
"publishConfig": {
"access": "public"
}
-}
\ No newline at end of file
+}
diff --git a/packages/html/html/rollup.config.js b/packages/html/html/rollup.config.js
index 0fe567ba..e95bc7c6 100644
--- a/packages/html/html/rollup.config.js
+++ b/packages/html/html/rollup.config.js
@@ -11,6 +11,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -39,6 +42,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -59,4 +65,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/html/html/src/components/media-time-range.ts b/packages/html/html/src/components/media-time-range.ts
index 366e9f9b..509089d7 100644
--- a/packages/html/html/src/components/media-time-range.ts
+++ b/packages/html/html/src/components/media-time-range.ts
@@ -5,10 +5,29 @@ import {
} from '../utils/component-factory';
import { timeRangeStateDefinition } from '@vjs-10/media-store';
+// Utility functions for pointer position and seek time calculations
+const calculatePointerRatio = (clientX: number, rect: DOMRect): number => {
+ const x = clientX - rect.left;
+ return Math.max(0, Math.min(100, (x / rect.width) * 100));
+};
+
+const calculateSeekTimeFromRatio = (ratio: number, duration: number): number => {
+ return (ratio / 100) * duration;
+};
+
+const calculateSeekTimeFromPointerEvent = (
+ event: PointerEvent,
+ duration: number
+): number => {
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
+ const ratio = calculatePointerRatio(event.clientX, rect);
+ return calculateSeekTimeFromRatio(ratio, duration);
+};
+
/**
- * @TODO Should we use a base "range" superclass or just duplicate shared code? (CJP)
- **/
-export class TimeRangeBase extends HTMLElement {
+ * TimeRange Root component - Main container with pointer event handling
+ */
+export class TimeRangeRootBase extends HTMLElement {
_state:
| {
currentTime: number;
@@ -16,35 +35,87 @@ export class TimeRangeBase extends HTMLElement {
requestSeek: (time: number) => void;
}
| undefined;
- _input: HTMLInputElement;
+ _trackElement: HTMLElement | null = null;
+ _pointerPosition: number | null = null;
+ _hovering: boolean = false;
+ _dragging: boolean = false;
constructor() {
super();
- /**
- * @TODO This is just a simple placeholder input to demonstrate functionality.
- * A full implementation will need to implement a "compound component" architecture and likely should use templates. (CJP)
- **/
- this._input = document.createElement('input');
- this._input.type = 'range';
- this._input.min = '0';
- this._input.max = '100';
- this._input.step = '0.1';
- this._input.addEventListener('input', this);
- this.appendChild(this._input);
+
+ // Add pointer event listeners
+ this.addEventListener('pointerdown', this);
+ this.addEventListener('pointermove', this);
+ this.addEventListener('pointerup', this);
+ this.addEventListener('pointerenter', this);
+ this.addEventListener('pointerleave', this);
}
handleEvent(event: Event) {
const { type } = event;
const state = this._state;
- if (state) {
- if (type === 'input') {
- const ratio = parseFloat(this._input.value) / 100;
- const seekTime = ratio * state.duration;
- state.requestSeek(seekTime);
- }
+ if (!state) return;
+
+ switch (type) {
+ case 'pointerdown':
+ this._handlePointerDown(event as PointerEvent);
+ break;
+ case 'pointermove':
+ this._handlePointerMove(event as PointerEvent);
+ break;
+ case 'pointerup':
+ this._handlePointerUp(event as PointerEvent);
+ break;
+ case 'pointerenter':
+ this._handlePointerEnter();
+ break;
+ case 'pointerleave':
+ this._handlePointerLeave();
+ break;
}
}
+ private _handlePointerDown(event: PointerEvent) {
+ event.preventDefault();
+ this._dragging = true;
+ const seekTime = calculateSeekTimeFromPointerEvent(event, this._state!.duration);
+ this._state!.requestSeek(seekTime);
+
+ // Capture pointer events
+ this.setPointerCapture(event.pointerId);
+ }
+
+ private _handlePointerMove(event: PointerEvent) {
+ if (!this._trackElement) return;
+
+ const rect = this._trackElement.getBoundingClientRect();
+ const ratio = calculatePointerRatio(event.clientX, rect);
+ this._pointerPosition = ratio;
+
+ if (this._dragging) {
+ const seekTime = calculateSeekTimeFromRatio(ratio, this._state!.duration);
+ this._state!.requestSeek(seekTime);
+ }
+ }
+
+ private _handlePointerUp(event: PointerEvent) {
+ this.releasePointerCapture(event.pointerId);
+
+ if (this._dragging && this._trackElement && this._pointerPosition !== null) {
+ const seekTime = calculateSeekTimeFromRatio(this._pointerPosition, this._state!.duration);
+ this._state!.requestSeek(seekTime);
+ }
+ this._dragging = false;
+ }
+
+ private _handlePointerEnter() {
+ this._hovering = true;
+ }
+
+ private _handlePointerLeave() {
+ this._hovering = false;
+ }
+
get currentTime() {
return this._state?.currentTime;
}
@@ -55,25 +126,106 @@ export class TimeRangeBase extends HTMLElement {
_update(props: any, state: any) {
this._state = state;
- const ratio =
- state.duration > 0 ? (state.currentTime / state.duration) * 100 : 0;
- this._input.value = ratio.toString();
- this._input.max = '100';
- this._input.setAttribute('aria-label', props['aria-label']);
- this._input.setAttribute('aria-valuetext', props['aria-valuetext']);
- this._input.disabled = props.disabled ?? false;
+
+ // Find track element
+ this._trackElement = this.querySelector('media-time-range-track') as HTMLElement;
+
+ // Calculate slider fill percentage
+ const sliderFill = this._dragging && this._pointerPosition !== null
+ ? this._pointerPosition
+ : state.duration > 0
+ ? (state.currentTime / state.duration) * 100
+ : 0;
- // Update data attributes for styling
- this.setAttribute('data-current-time', props['data-current-time']);
- this.setAttribute('data-duration', props['data-duration']);
+ // Update CSS custom properties
+ this.style.setProperty('--slider-fill', `${Math.round(sliderFill)}%`);
+ this.style.setProperty('--slider-pointer',
+ this._hovering && this._pointerPosition !== null
+ ? `${Math.round(this._pointerPosition)}%`
+ : '0%'
+ );
+
+ // Update ARIA attributes
+ this.setAttribute('role', 'slider');
+ this.setAttribute('aria-label', props['aria-label'] || 'Seek');
+ this.setAttribute('aria-valuemin', '0');
+ this.setAttribute('aria-valuemax', '100');
+ this.setAttribute('aria-valuenow', sliderFill.toString());
+ this.setAttribute('aria-valuetext', props['aria-valuetext'] || '');
+
+ // Update data attributes
+ this.setAttribute('data-current-time', state.currentTime.toString());
+ this.setAttribute('data-duration', state.duration.toString());
}
}
/**
- * TimeRange state hook - equivalent to React's useTimeRangeState
+ * TimeRange Track component - Track element that captures pointer events
+ */
+export class TimeRangeTrackBase extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ _update(_props: any, _state: any) {
+ // Track doesn't need much state management
+ }
+}
+
+/**
+ * TimeRange Progress component - Shows current progress
+ */
+export class TimeRangeProgressBase extends HTMLElement {
+ constructor() {
+ super();
+ this.style.position = 'absolute';
+ this.style.width = 'var(--slider-fill, 0%)';
+ this.style.height = '100%';
+ }
+
+ _update(_props: any, _state: any) {
+ // Progress updates are handled by CSS custom properties
+ }
+}
+
+/**
+ * TimeRange Pointer component - Shows hover position
+ */
+export class TimeRangePointerBase extends HTMLElement {
+ constructor() {
+ super();
+ this.style.position = 'absolute';
+ this.style.width = 'var(--slider-pointer, 0%)';
+ this.style.height = '100%';
+ }
+
+ _update(_props: any, _state: any) {
+ // Pointer updates are handled by CSS custom properties
+ }
+}
+
+/**
+ * TimeRange Thumb component - Draggable thumb element
+ */
+export class TimeRangeThumbBase extends HTMLElement {
+ constructor() {
+ super();
+ this.style.position = 'absolute';
+ this.style.top = '50%';
+ this.style.left = 'var(--slider-fill, 0%)';
+ this.style.transform = 'translate(-50%, -50%)';
+ }
+
+ _update(_props: any, _state: any) {
+ // Thumb updates are handled by CSS custom properties
+ }
+}
+
+/**
+ * TimeRange Root state hook - equivalent to React's useTimeRangeRootState
* Handles media store state subscription and transformation
*/
-export const useTimeRangeState: StateHook<{
+export const useTimeRangeRootState: StateHook<{
currentTime: number;
duration: number;
}> = {
@@ -85,10 +237,10 @@ export const useTimeRangeState: StateHook<{
};
/**
- * TimeRange props hook - equivalent to React's useTimeRangeProps
+ * TimeRange Root props hook - equivalent to React's useTimeRangeRootProps
* Handles element attributes and properties based on state
*/
-export const useTimeRangeProps: PropsHook<{
+export const useTimeRangeRootProps: PropsHook<{
currentTime: number;
duration: number;
}> = (state, _element) => {
@@ -108,31 +260,133 @@ export const useTimeRangeProps: PropsHook<{
/** aria attributes/props */
['aria-label']: 'Seek',
['aria-valuetext']: `${currentTimeText} of ${durationText}`,
- /** input props */
- disabled: false,
};
return baseProps;
};
/**
- * @TODO When implementing compound components, this function may need to be swapped out, modified, or augmented in some way or another. (CJP)
+ * TimeRange Track props hook
*/
+export const useTimeRangeTrackProps: PropsHook<{}> = (_state, _element) => {
+ return {};
+};
+
/**
- * Connected TimeRange component using hook-style architecture
- * Equivalent to React's TimeRange = toConnectedComponent(...)
+ * TimeRange Progress props hook
*/
-export const TimeRange = toConnectedHTMLComponent(
- TimeRangeBase,
- useTimeRangeState,
- useTimeRangeProps,
- 'TimeRange',
+export const useTimeRangeProgressProps: PropsHook<{}> = (_state, _element) => {
+ return {};
+};
+
+/**
+ * TimeRange Pointer props hook
+ */
+export const useTimeRangePointerProps: PropsHook<{}> = (_state, _element) => {
+ return {};
+};
+
+/**
+ * TimeRange Thumb props hook
+ */
+export const useTimeRangeThumbProps: PropsHook<{}> = (_state, _element) => {
+ return {};
+};
+
+/**
+ * Connected TimeRange Root component using hook-style architecture
+ */
+export const TimeRangeRoot = toConnectedHTMLComponent(
+ TimeRangeRootBase,
+ useTimeRangeRootState,
+ useTimeRangeRootProps,
+ 'TimeRangeRoot',
);
-// 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-time-range')) {
+/**
+ * Connected TimeRange Track component
+ */
+export const TimeRangeTrack = toConnectedHTMLComponent(
+ TimeRangeTrackBase,
+ { keys: [], transform: () => ({}) },
+ useTimeRangeTrackProps,
+ 'TimeRangeTrack',
+);
+
+/**
+ * Connected TimeRange Progress component
+ */
+export const TimeRangeProgress = toConnectedHTMLComponent(
+ TimeRangeProgressBase,
+ { keys: [], transform: () => ({}) },
+ useTimeRangeProgressProps,
+ 'TimeRangeProgress',
+);
+
+/**
+ * Connected TimeRange Pointer component
+ */
+export const TimeRangePointer = toConnectedHTMLComponent(
+ TimeRangePointerBase,
+ { keys: [], transform: () => ({}) },
+ useTimeRangePointerProps,
+ 'TimeRangePointer',
+);
+
+/**
+ * Connected TimeRange Thumb component
+ */
+export const TimeRangeThumb = toConnectedHTMLComponent(
+ TimeRangeThumbBase,
+ { keys: [], transform: () => ({}) },
+ useTimeRangeThumbProps,
+ 'TimeRangeThumb',
+);
+
+/**
+ * Compound TimeRange component object
+ */
+export const TimeRange = Object.assign(
+ {},
+ {
+ Root: TimeRangeRoot,
+ Track: TimeRangeTrack,
+ Progress: TimeRangeProgress,
+ Pointer: TimeRangePointer,
+ Thumb: TimeRangeThumb,
+ }
+) as {
+ Root: typeof TimeRangeRoot;
+ Track: typeof TimeRangeTrack;
+ Progress: typeof TimeRangeProgress;
+ Pointer: typeof TimeRangePointer;
+ Thumb: typeof TimeRangeThumb;
+};
+
+// Register custom elements
+if (!globalThis.customElements.get('media-time-range-root')) {
// @ts-ignore - Custom element constructor compatibility
- globalThis.customElements.define('media-time-range', TimeRange);
+ globalThis.customElements.define('media-time-range-root', TimeRangeRoot);
+}
+
+if (!globalThis.customElements.get('media-time-range-track')) {
+ // @ts-ignore - Custom element constructor compatibility
+ globalThis.customElements.define('media-time-range-track', TimeRangeTrack);
+}
+
+if (!globalThis.customElements.get('media-time-range-progress')) {
+ // @ts-ignore - Custom element constructor compatibility
+ globalThis.customElements.define('media-time-range-progress', TimeRangeProgress);
+}
+
+if (!globalThis.customElements.get('media-time-range-pointer')) {
+ // @ts-ignore - Custom element constructor compatibility
+ globalThis.customElements.define('media-time-range-pointer', TimeRangePointer);
+}
+
+if (!globalThis.customElements.get('media-time-range-thumb')) {
+ // @ts-ignore - Custom element constructor compatibility
+ globalThis.customElements.define('media-time-range-thumb', TimeRangeThumb);
}
export default TimeRange;
diff --git a/packages/html/html/src/skins/media-skin-default.ts b/packages/html/html/src/skins/media-skin-default.ts
index 2ac99d04..c225c653 100644
--- a/packages/html/html/src/skins/media-skin-default.ts
+++ b/packages/html/html/src/skins/media-skin-default.ts
@@ -23,7 +23,7 @@ export function getTemplateHTML() {
color: rgb(238 238 238);
}
- media-container > [slot=media] {
+ media-container > ::slotted([slot=media]) {
width: 100%;
height: 100%;
}
@@ -103,6 +103,45 @@ export function getTemplateHTML() {
.spacer {
flex-grow: 1;
}
+
+ /* TimeRange Component Styles */
+ media-time-range-root {
+ display: flex;
+ align-items: center;
+ position: relative;
+ min-width: 100px;
+ width: 100%;
+ padding-block: .75rem;
+ margin: 0 .5rem;
+ }
+
+ media-time-range-track {
+ position: relative;
+ width: 100%;
+ height: .375rem;
+ background-color: #e0e0e0;
+ border-radius: .25rem;
+ overflow: hidden;
+ pointer-events: none;
+ }
+
+ media-time-range-thumb {
+ width: .75rem;
+ height: .75rem;
+ background-color: #fff;
+ border-radius: 50%;
+ pointer-events: none;
+ }
+
+ media-time-range-pointer {
+ background-color: rgba(255, 255, 255, .5);
+ pointer-events: none;
+ }
+
+ media-time-range-progress {
+ background-color: #007bff;
+ border-radius: inherit;
+ }
@@ -116,7 +155,13 @@ export function getTemplateHTML() {
-
+
+
+
+
+
+
+
diff --git a/packages/react/react-icons/rollup.config.js b/packages/react/react-icons/rollup.config.js
index 0fe567ba..e95bc7c6 100644
--- a/packages/react/react-icons/rollup.config.js
+++ b/packages/react/react-icons/rollup.config.js
@@ -11,6 +11,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -39,6 +42,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: (id) => {
// Don't externalize relative imports (starts with . or /)
if (id.startsWith('.') || id.startsWith('/')) return false;
@@ -59,4 +65,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/react/react/rollup.config.js b/packages/react/react/rollup.config.js
index 8963d658..8587e273 100644
--- a/packages/react/react/rollup.config.js
+++ b/packages/react/react/rollup.config.js
@@ -12,6 +12,9 @@ module.exports = [
format: 'esm',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: [
'react',
'@vjs-10/react-icons',
@@ -46,6 +49,9 @@ module.exports = [
format: 'cjs',
sourcemap: true,
},
+ watch: {
+ clearScreen: false
+ },
external: [
'react',
'@vjs-10/react-icons',
@@ -72,4 +78,4 @@ module.exports = [
}),
],
},
-];
\ No newline at end of file
+];
diff --git a/packages/react/react/src/components/TimeRange.tsx b/packages/react/react/src/components/TimeRange.tsx
index 8357ad6d..19137c40 100644
--- a/packages/react/react/src/components/TimeRange.tsx
+++ b/packages/react/react/src/components/TimeRange.tsx
@@ -33,29 +33,6 @@ const calculateSeekTimeFromPointerEvent = (
// ROOT COMPONENT
// ============================================================================
-interface TimeRangeRootContextValue {
- currentTime: number;
- duration: number;
- requestSeek: (time: number) => void;
- pointerPosition: number | null;
- setPointerPosition: (position: number | null) => void;
- hovering: boolean;
- setHovering: (hovering: boolean) => void;
- dragging: boolean;
- setDragging: (dragging: boolean) => void;
- setTrackRef: (ref: HTMLDivElement | null) => void;
-}
-
-const TimeRangeRootContext = React.createContext(null);
-
-export const useTimeRangeRootContext = (): TimeRangeRootContextValue => {
- const context = React.useContext(TimeRangeRootContext);
- if (!context) {
- throw new Error('useTimeRangeRootContext must be used within a TimeRange.Root component');
- }
- return context;
-};
-
export const useTimeRangeRootState = (_props: any) => {
const mediaStore = useMediaStore();
const mediaState = useMediaSelector(timeRangeStateDefinition.stateTransform, shallowEqual);
@@ -186,43 +163,11 @@ export const useTimeRangeRootProps = (
type useTimeRangeRootState = typeof useTimeRangeRootState;
type useTimeRangeRootProps = typeof useTimeRangeRootProps;
-type TimeRangeRootState = ReturnType;
type TimeRangeRootProps = ReturnType;
-export const renderTimeRangeRoot = (props: TimeRangeRootProps, state: TimeRangeRootState) => {
- const contextValue: TimeRangeRootContextValue = React.useMemo(
- () => ({
- currentTime: state.currentTime,
- duration: state.duration,
- requestSeek: state.requestSeek,
- pointerPosition: state.pointerPosition,
- setPointerPosition: state.setPointerPosition,
- hovering: state.hovering,
- setHovering: state.setHovering,
- dragging: state.dragging,
- setDragging: state.setDragging,
- setTrackRef: state.setTrackRef,
- }),
- [
- state.currentTime,
- state.duration,
- state.requestSeek,
- state.pointerPosition,
- state.setPointerPosition,
- state.hovering,
- state.setHovering,
- state.dragging,
- state.setDragging,
- state.setTrackRef,
- ]
- );
-
+export const renderTimeRangeRoot = (props: TimeRangeRootProps) => {
return (
-
-
- {props.children}
-
-
+
);
};
@@ -237,8 +182,8 @@ const TimeRangeRoot = toConnectedComponent(
// TRACK COMPONENT
// ============================================================================
-export const useTimeRangeTrackProps = (props: React.PropsWithChildren<{ [k: string]: any }>) => {
- const { setTrackRef } = useTimeRangeRootContext();
+export const useTimeRangeTrackProps = (props: React.PropsWithChildren<{ [k: string]: any }>, context: any) => {
+ const { setTrackRef } = context;
return {
ref: setTrackRef,
diff --git a/packages/react/react/src/utils/component-factory.tsx b/packages/react/react/src/utils/component-factory.tsx
index 894f2958..18aee15d 100644
--- a/packages/react/react/src/utils/component-factory.tsx
+++ b/packages/react/react/src/utils/component-factory.tsx
@@ -7,18 +7,20 @@ export type StateHookFn = (props: TProps) => TState;
export type PropsHookFn = (
props: TProps,
- state: TState,
+ state: TState
) => TResultProps;
export type RenderFn = (
props: TProps,
- state: TState,
+ state: TState
) => React.ReactElement;
+const Context = React.createContext(null);
+
/**
* Generic factory function to create connected components following the hooks pattern
* inspired by Adobe React Spectrum and Base UI architectures.
- *
+ *
* @param useStateHook - Hook that provides component state
* @param usePropsHook - Hook that enhances props with state-derived values
* @param defaultRender - Default render function for the component
@@ -29,12 +31,12 @@ export const toConnectedComponent = <
TProps extends Record,
TState,
TResultProps extends Record,
- TRenderFn extends RenderFn
+ TRenderFn extends RenderFn,
>(
useStateHook: StateHookFn,
usePropsHook: PropsHookFn,
defaultRender: TRenderFn,
- displayName: string,
+ displayName: string
) => {
const ConnectedComponent = ({
render = defaultRender,
@@ -42,7 +44,11 @@ export const toConnectedComponent = <
}: TProps & { render?: TRenderFn }) => {
const connectedState = useStateHook(props as TProps);
const connectedProps = usePropsHook(props as TProps, connectedState);
- return render(connectedProps, connectedState);
+ return (
+
+ {render(connectedProps, connectedState)}
+
+ );
};
ConnectedComponent.displayName = displayName;
@@ -54,13 +60,13 @@ export const toConnectedComponent = <
*/
export type ConnectedComponent<
TProps extends Record,
- TRenderFn extends RenderFn
+ TRenderFn extends RenderFn,
> = React.FC;
/**
* Factory function to create context-based components that don't use toConnectedComponent
* These components rely on context provided by a parent component.
- *
+ *
* @param usePropsHook - Hook that enhances props with context-derived values
* @param defaultRender - Default render function for the component
* @param displayName - Display name for React DevTools
@@ -69,18 +75,19 @@ export type ConnectedComponent<
export const toContextComponent = <
TProps extends Record,
TResultProps extends Record,
- TRenderFn extends (props: TResultProps) => React.ReactElement
+ TRenderFn extends (props: TResultProps, context: any) => React.ReactElement,
>(
- usePropsHook: (props: TProps) => TResultProps,
+ usePropsHook: (props: TProps, context: any) => TResultProps,
defaultRender: TRenderFn,
- displayName: string,
+ displayName: string
) => {
const ContextComponent = ({
render = defaultRender,
...props
}: TProps & { render?: TRenderFn }) => {
- const contextProps = usePropsHook(props as TProps);
- return render(contextProps);
+ const context = React.useContext(Context);
+ const contextProps = usePropsHook(props as TProps, context);
+ return render(contextProps, context);
};
ContextComponent.displayName = displayName;
@@ -92,5 +99,5 @@ export const toContextComponent = <
*/
export type ContextComponent<
TProps extends Record,
- TRenderFn extends (props: any) => React.ReactElement
+ TRenderFn extends (props: any, context: any) => React.ReactElement,
> = React.FC;