feat(react): orientation-aware buffer styling and slider improvements (#671)

This commit is contained in:
Sam Potts
2026-03-02 11:40:58 +11:00
committed by GitHub
parent e3d7f4b277
commit 86b2f4d3b4
8 changed files with 68 additions and 22 deletions
@@ -528,7 +528,16 @@
.media-minimal-skin .media-slider__buffer {
background-color: oklch(1 0 0 / 0.2);
width: var(--media-slider-buffer, 0%);
transition: width 0.25s ease-out;
transition-duration: 0.25s;
transition-timing-function: ease-out;
}
.media-default-skin .media-slider__buffer[data-orientation="horizontal"] {
width: var(--media-slider-buffer);
transition-property: width;
}
.media-default-skin .media-slider__buffer[data-orientation="vertical"] {
height: var(--media-slider-buffer);
transition-property: height;
}
/* Fill */
@@ -163,16 +163,33 @@ const SliderTrack = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function S
);
});
const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderFill({ className, ...props }, ref) {
const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: 'fill' | 'buffer' }>(function SliderFill(
{ type = 'fill', className, ...props },
ref
) {
return (
<div
ref={ref}
className={cn(
'absolute rounded-[inherit] pointer-events-none bg-white',
'absolute rounded-[inherit] pointer-events-none',
{
'bg-white': type === 'fill',
'bg-white/20 duration-250 ease-out': type === 'buffer',
},
// Horizontal
'data-[orientation=horizontal]:inset-y-0 data-[orientation=horizontal]:left-0 data-[orientation=horizontal]:w-(--media-slider-fill)',
`data-[orientation=horizontal]:inset-y-0 data-[orientation=horizontal]:left-0`,
{
'data-[orientation=horizontal]:w-(--media-slider-fill)': type === 'fill',
'data-[orientation=horizontal]:transition-[width] data-[orientation=horizontal]:w-(--media-slider-buffer)':
type === 'buffer',
},
// Vertical
'data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:bottom-0 data-[orientation=vertical]:h-(--media-slider-fill)',
`data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:bottom-0`,
{
'data-[orientation=vertical]:h-(--media-slider-fill)': type === 'fill',
'data-[orientation=vertical]:transition-[height] data-[orientation=vertical]:h-(--media-slider-buffer)':
type === 'buffer',
},
className
)}
{...props}
@@ -387,7 +404,7 @@ export function MinimalVideoSkinTailwind(props: MinimalVideoSkinProps): ReactNod
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white/20 w-(--media-slider-buffer) transition-[width] duration-250 ease-out" />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
</TimeSlider.Root>
@@ -189,7 +189,6 @@ export function MinimalVideoSkin(props: MinimalVideoSkinProps): ReactNode {
<MuteButtonIcon state={state} className="media-icon" />
</Button>
)}
{...props}
/>
}
/>
+4 -1
View File
@@ -543,13 +543,16 @@
/* Buffer */
.media-default-skin .media-slider__buffer {
background-color: oklch(1 0 0 / 0.2);
transition: width 0.25s ease-out;
transition-duration: 0.25s;
transition-timing-function: ease-out;
}
.media-default-skin .media-slider__buffer[data-orientation="horizontal"] {
width: var(--media-slider-buffer);
transition-property: width;
}
.media-default-skin .media-slider__buffer[data-orientation="vertical"] {
height: var(--media-slider-buffer);
transition-property: height;
}
/* Fill */
@@ -177,16 +177,33 @@ const SliderTrack = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function S
);
});
const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'>>(function SliderFill({ className, ...props }, ref) {
const SliderFill = forwardRef<HTMLDivElement, ComponentProps<'div'> & { type?: 'fill' | 'buffer' }>(function SliderFill(
{ type = 'fill', className, ...props },
ref
) {
return (
<div
ref={ref}
className={cn(
'absolute rounded-[inherit] pointer-events-none bg-white',
'absolute rounded-[inherit] pointer-events-none',
{
'bg-white': type === 'fill',
'bg-white/20 duration-250 ease-out': type === 'buffer',
},
// Horizontal
'data-[orientation=horizontal]:inset-y-0 data-[orientation=horizontal]:left-0 data-[orientation=horizontal]:w-(--media-slider-fill)',
`data-[orientation=horizontal]:inset-y-0 data-[orientation=horizontal]:left-0`,
{
'data-[orientation=horizontal]:w-(--media-slider-fill)': type === 'fill',
'data-[orientation=horizontal]:transition-[width] data-[orientation=horizontal]:w-(--media-slider-buffer)':
type === 'buffer',
},
// Vertical
'data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:bottom-0 data-[orientation=vertical]:h-(--media-slider-fill)',
`data-[orientation=vertical]:inset-x-0 data-[orientation=vertical]:bottom-0`,
{
'data-[orientation=vertical]:h-(--media-slider-fill)': type === 'fill',
'data-[orientation=vertical]:transition-[height] data-[orientation=vertical]:h-(--media-slider-buffer)':
type === 'buffer',
},
className
)}
{...props}
@@ -394,7 +411,7 @@ export function VideoSkinTailwind(props: VideoSkinProps): ReactNode {
<TimeSlider.Root render={(props) => <SliderRoot {...props} />}>
<TimeSlider.Track render={(props) => <SliderTrack {...props} />}>
<TimeSlider.Fill render={(props) => <SliderFill {...props} />} />
<TimeSlider.Buffer className="absolute inset-y-0 left-0 rounded-[inherit] pointer-events-none bg-white/20 w-(--media-slider-buffer) transition-[width] duration-250 ease-out" />
<TimeSlider.Buffer render={(props) => <SliderFill type="buffer" {...props} />} />
</TimeSlider.Track>
<TimeSlider.Thumb render={(props) => <SliderThumb {...props} />} />
</TimeSlider.Root>
@@ -184,7 +184,6 @@ export function VideoSkin(props: VideoSkinProps): ReactNode {
<MuteButtonIcon state={state} className="media-icon" />
</Button>
)}
{...props}
/>
}
/>
+8 -8
View File
@@ -84,20 +84,20 @@ export function useSlider<State extends SliderState = SliderState>(
// Cleanup on unmount.
useEffect(() => () => slider.destroy(), [slider]);
// Force a synchronous re-render after mount so edge thumb alignment
// can read DOM measurements from the now-populated element refs.
useLayoutEffect(() => {
if (rootElementRef.current && thumbElementRef.current) {
forceRender();
}
}, []);
// Subscribe to interaction state.
const interaction = useSnapshot(slider.interaction);
// Compute derived state from interaction + caller-provided projection.
const state = options.computeState(interaction);
// Force a synchronous re-render after mount so edge thumb alignment
// can read DOM measurements from the now-populated element refs.
useLayoutEffect(() => {
if (state.thumbAlignment === 'edge' && rootElementRef.current && thumbElementRef.current) {
forceRender();
}
}, [state.thumbAlignment]);
// Adjust CSS var percents for edge thumb alignment when DOM elements are available.
const rootEl = rootElementRef.current;
const thumbEl = thumbElementRef.current;
@@ -1,3 +1,5 @@
'use client';
import { useReducer } from 'react';
export function useForceRender() {