mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add HTML vertical orientation to time and volume (#32)
* feat: add vertical orientation to time and volume range components * fix: improve resetting of styles
This commit is contained in:
@@ -13,6 +13,8 @@ type TimeRangeRootState = {
|
||||
};
|
||||
|
||||
export class TimeRangeRootBase extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[] = ['orientation'];
|
||||
|
||||
_state: TimeRangeRootState | undefined;
|
||||
_core: CoreTimeRange | null = null;
|
||||
|
||||
@@ -24,6 +26,16 @@ export class TimeRangeRootBase extends HTMLElement {
|
||||
return this._state?.duration ?? 0;
|
||||
}
|
||||
|
||||
get orientation(): 'horizontal' | 'vertical' {
|
||||
return this.getAttribute('orientation') as 'horizontal' | 'vertical' || 'horizontal';
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
|
||||
if (name === 'orientation' && this._state) {
|
||||
this._render(useTimeRangeRootProps(this._state, this), this._state);
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, state: any): void {
|
||||
this._state = state;
|
||||
|
||||
@@ -50,9 +62,11 @@ export class TimeRangeRootBase extends HTMLElement {
|
||||
this.setAttribute('aria-valuemax', '100');
|
||||
this.setAttribute('aria-valuenow', coreState._fillWidth.toString());
|
||||
this.setAttribute('aria-valuetext', props['aria-valuetext'] || '');
|
||||
this.setAttribute('aria-orientation', props['aria-orientation']);
|
||||
|
||||
this.setAttribute('data-current-time', state.currentTime.toString());
|
||||
this.setAttribute('data-duration', state.duration.toString());
|
||||
this.setAttribute('data-orientation', props['data-orientation']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +83,18 @@ export class TimeRangeTrackBase extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, _state: any): void {
|
||||
// Track doesn't need much state management
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate dimensions based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.width = '100%';
|
||||
this.style.removeProperty('height');
|
||||
} else {
|
||||
this.style.height = '100%';
|
||||
this.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +106,22 @@ export class TimeRangeProgressBase extends HTMLElement {
|
||||
this.style.height = '100%';
|
||||
}
|
||||
|
||||
_update(_props: any, _state: any): void {
|
||||
// Progress updates are handled by CSS custom properties
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate dimensions based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.width = 'var(--slider-fill, 0%)';
|
||||
this.style.height = '100%';
|
||||
this.style.top = '0';
|
||||
this.style.removeProperty('bottom');
|
||||
} else {
|
||||
this.style.height = 'var(--slider-fill, 0%)';
|
||||
this.style.width = '100%';
|
||||
this.style.bottom = '0';
|
||||
this.style.removeProperty('top');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,8 +133,22 @@ export class TimeRangePointerBase extends HTMLElement {
|
||||
this.style.height = '100%';
|
||||
}
|
||||
|
||||
_update(_props: any, _state: any): void {
|
||||
// Pointer updates are handled by CSS custom properties
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate dimensions based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.width = 'var(--slider-pointer, 0%)';
|
||||
this.style.height = '100%';
|
||||
this.style.top = '0';
|
||||
this.style.removeProperty('bottom');
|
||||
} else {
|
||||
this.style.height = 'var(--slider-pointer, 0%)';
|
||||
this.style.width = '100%';
|
||||
this.style.bottom = '0';
|
||||
this.style.removeProperty('top');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,13 +156,22 @@ 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): void {
|
||||
// Thumb updates are handled by CSS custom properties
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate positioning based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.left = 'var(--slider-fill, 0%)';
|
||||
this.style.top = '50%';
|
||||
this.style.transform = 'translate(-50%, -50%)';
|
||||
} else {
|
||||
this.style.bottom = 'var(--slider-fill, 0%)';
|
||||
this.style.left = '50%';
|
||||
this.style.transform = 'translate(-50%, 50%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +194,7 @@ export const useTimeRangeRootProps: PropsHook<{
|
||||
duration: number;
|
||||
requestSeek: (time: number) => void;
|
||||
core: CoreTimeRange | null;
|
||||
}> = (state, _element) => {
|
||||
}> = (state, element) => {
|
||||
const formatTime = (time: number) => {
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
@@ -147,28 +208,46 @@ export const useTimeRangeRootProps: PropsHook<{
|
||||
/** data attributes/props */
|
||||
['data-current-time']: state.currentTime.toString(),
|
||||
['data-duration']: state.duration.toString(),
|
||||
['data-orientation']: (element as any).orientation || 'horizontal',
|
||||
/** aria attributes/props */
|
||||
['aria-label']: 'Seek',
|
||||
['aria-valuetext']: `${currentTimeText} of ${durationText}`,
|
||||
['aria-orientation']: (element as any).orientation || 'horizontal',
|
||||
};
|
||||
|
||||
return baseProps;
|
||||
};
|
||||
|
||||
export const useTimeRangeTrackProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useTimeRangeTrackProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-time-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
export const useTimeRangeProgressProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useTimeRangeProgressProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-time-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
export const useTimeRangePointerProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useTimeRangePointerProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-time-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
export const useTimeRangeThumbProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useTimeRangeThumbProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-time-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
export const TimeRangeRoot: ConnectedComponentConstructor<{
|
||||
|
||||
@@ -17,6 +17,8 @@ type VolumeRangeRootState = {
|
||||
};
|
||||
|
||||
export class VolumeRangeRootBase extends HTMLElement {
|
||||
static readonly observedAttributes: readonly string[] = ['orientation'];
|
||||
|
||||
_state: VolumeRangeRootState | undefined;
|
||||
_core: CoreVolumeRange | null = null;
|
||||
|
||||
@@ -32,7 +34,17 @@ export class VolumeRangeRootBase extends HTMLElement {
|
||||
return this._state?.volumeLevel ?? 'high';
|
||||
}
|
||||
|
||||
_update(_props: any, state: any): void {
|
||||
get orientation(): 'horizontal' | 'vertical' {
|
||||
return this.getAttribute('orientation') as 'horizontal' | 'vertical' || 'horizontal';
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
|
||||
if (name === 'orientation' && this._state) {
|
||||
this._render(useVolumeRangeRootProps(this._state, this), this._state);
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, state: any): void {
|
||||
this._state = state;
|
||||
|
||||
if (state && !this._core) {
|
||||
@@ -58,9 +70,11 @@ export class VolumeRangeRootBase extends HTMLElement {
|
||||
this.setAttribute('aria-valuemax', '100');
|
||||
this.setAttribute('aria-valuenow', coreState._fillWidth.toString());
|
||||
this.setAttribute('aria-valuetext', props['aria-valuetext'] || '');
|
||||
this.setAttribute('aria-orientation', props['aria-orientation']);
|
||||
|
||||
this.setAttribute('data-muted', state.muted.toString());
|
||||
this.setAttribute('data-volume-level', state.volumeLevel);
|
||||
this.setAttribute('data-orientation', props['data-orientation']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,8 +94,18 @@ export class VolumeRangeTrackBase extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
_update(_props: any, _state: any): void {
|
||||
// Track doesn't need much state management
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate dimensions based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.width = '100%';
|
||||
this.style.removeProperty('height');
|
||||
} else {
|
||||
this.style.height = '100%';
|
||||
this.style.removeProperty('width');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +120,22 @@ export class VolumeRangeProgressBase extends HTMLElement {
|
||||
this.style.height = '100%';
|
||||
}
|
||||
|
||||
_update(_props: any, _state: any): void {
|
||||
// Progress updates are handled by CSS custom properties
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate dimensions based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.width = 'var(--slider-fill, 0%)';
|
||||
this.style.height = '100%';
|
||||
this.style.top = '0';
|
||||
this.style.removeProperty('bottom');
|
||||
} else {
|
||||
this.style.height = 'var(--slider-fill, 0%)';
|
||||
this.style.width = '100%';
|
||||
this.style.bottom = '0';
|
||||
this.style.removeProperty('top');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,13 +146,22 @@ export class VolumeRangeThumbBase 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): void {
|
||||
// Thumb updates are handled by CSS custom properties
|
||||
_update(props: any, _state: any): void {
|
||||
const orientation = props['data-orientation'] || 'horizontal';
|
||||
this.setAttribute('data-orientation', orientation);
|
||||
|
||||
// Set appropriate positioning based on orientation
|
||||
if (orientation === 'horizontal') {
|
||||
this.style.left = 'var(--slider-fill, 0%)';
|
||||
this.style.top = '50%';
|
||||
this.style.transform = 'translate(-50%, -50%)';
|
||||
} else {
|
||||
this.style.bottom = 'var(--slider-fill, 0%)';
|
||||
this.style.left = '50%';
|
||||
this.style.transform = 'translate(-50%, 50%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,16 +194,18 @@ export const useVolumeRangeRootProps: PropsHook<{
|
||||
volumeLevel: string;
|
||||
requestVolumeChange: (volume: number) => void;
|
||||
core: CoreVolumeRange | null;
|
||||
}> = (state, _element) => {
|
||||
}> = (state, element) => {
|
||||
const volumeText = `${Math.round(state.muted ? 0 : state.volume * 100)}%`;
|
||||
|
||||
const baseProps: Record<string, any> = {
|
||||
/** data attributes/props */
|
||||
['data-muted']: state.muted.toString(),
|
||||
['data-volume-level']: state.volumeLevel,
|
||||
['data-orientation']: (element as any).orientation || 'horizontal',
|
||||
/** aria attributes/props */
|
||||
['aria-label']: 'Volume',
|
||||
['aria-valuetext']: volumeText,
|
||||
['aria-orientation']: (element as any).orientation || 'horizontal',
|
||||
};
|
||||
|
||||
return baseProps;
|
||||
@@ -165,22 +214,34 @@ export const useVolumeRangeRootProps: PropsHook<{
|
||||
/**
|
||||
* VolumeRange Track props hook
|
||||
*/
|
||||
export const useVolumeRangeTrackProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useVolumeRangeTrackProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-volume-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* VolumeRange Progress props hook
|
||||
*/
|
||||
export const useVolumeRangeProgressProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useVolumeRangeProgressProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-volume-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* VolumeRange Thumb props hook
|
||||
*/
|
||||
export const useVolumeRangeThumbProps: PropsHook<{}> = (_state, _element) => {
|
||||
return {};
|
||||
export const useVolumeRangeThumbProps: PropsHook<{}> = (_state, element) => {
|
||||
// Get orientation from parent root element if not provided in state
|
||||
const rootElement = element.closest('media-volume-range-root') as any;
|
||||
return {
|
||||
['data-orientation']: rootElement?.orientation || 'horizontal',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,13 +108,28 @@ export function getTemplateHTML() {
|
||||
media-time-range-root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
width: 100%;
|
||||
padding-block: .75rem;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
media-time-range-root[data-orientation="horizontal"] {
|
||||
min-width: 100px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
media-time-range-root[data-orientation="vertical"] {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 100px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
media-time-range-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -125,6 +140,18 @@ export function getTemplateHTML() {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
media-time-range-track[data-orientation="horizontal"] {
|
||||
width: 100%;
|
||||
height: .375rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
media-time-range-track[data-orientation="vertical"] {
|
||||
width: .375rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
media-time-range-thumb {
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
@@ -147,13 +174,28 @@ export function getTemplateHTML() {
|
||||
media-volume-range-root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
min-width: 80px;
|
||||
width: 80px;
|
||||
padding-block: .75rem;
|
||||
margin: 0 .5rem;
|
||||
}
|
||||
|
||||
/* Horizontal orientation styles */
|
||||
media-volume-range-root[data-orientation="horizontal"] {
|
||||
min-width: 80px;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Vertical orientation styles */
|
||||
media-volume-range-root[data-orientation="vertical"] {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 80px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
media-volume-range-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -164,6 +206,18 @@ export function getTemplateHTML() {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Horizontal track styles */
|
||||
media-volume-range-track[data-orientation="horizontal"] {
|
||||
width: 100%;
|
||||
height: .375rem;
|
||||
}
|
||||
|
||||
/* Vertical track styles */
|
||||
media-volume-range-track[data-orientation="vertical"] {
|
||||
width: .375rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
media-volume-range-thumb {
|
||||
width: .75rem;
|
||||
height: .75rem;
|
||||
@@ -202,7 +256,7 @@ export function getTemplateHTML() {
|
||||
<media-volume-low-icon class="icon volume-low-icon"></media-volume-low-icon>
|
||||
<media-volume-off-icon class="icon volume-off-icon"></media-volume-off-icon>
|
||||
</media-mute-button>
|
||||
<media-volume-range-root>
|
||||
<media-volume-range-root orientation="vertical">
|
||||
<media-volume-range-track>
|
||||
<media-volume-range-progress></media-volume-range-progress>
|
||||
</media-volume-range-track>
|
||||
|
||||
Reference in New Issue
Block a user