UNPKG

@pmndrs/uikit-horizon

Version:

Horizon kit for @pmndrs/uikit based on the Reality Labs Design System (RLDS)

230 lines (229 loc) 8.85 kB
import { abortableEffect, Container, Text, } from '@pmndrs/uikit'; import { computed, signal } from '@preact/signals-core'; import { theme } from '../theme.js'; import { Vector3 } from 'three'; const vectorHelper = new Vector3(); export function percentageFormatting(value) { return `${value.toFixed(0)}%`; } const sliderHeights = { sm: 12, md: 28, lg: 44, }; const sliderThumbHeights = { sm: 8, md: 20, lg: 36, }; const sliderProcessPaddingXs = { sm: 2, md: 4, lg: 4, }; export class Slider extends Container { uncontrolledSignal = signal(undefined); currentSignal = computed(() => Number(this.properties.value.value ?? this.uncontrolledSignal.value ?? this.properties.value.defaultValue ?? 0)); downPointerId; touchTarget; track; progress; thumb; thumbText; labels; leftLabel; rightLabel; icon; constructor(inputProperties, initialClasses, config) { super(inputProperties, initialClasses, { ...config, defaultOverrides: { width: '100%', flexDirection: 'column', ...config?.defaultOverrides, }, }); //TODO: why does it not work when putting the following listeners on the touch target? this.addEventListener('pointerdown', (e) => { if (this.downPointerId != null) { return; } this.downPointerId = e.pointerId; this.handleSetValue(e); if ('target' in e && e.target != null && typeof e.target === 'object' && 'setPointerCapture' in e.target && typeof e.target.setPointerCapture === 'function') { e.target.setPointerCapture(e.pointerId); } }); this.addEventListener('pointermove', (e) => { if (this.downPointerId != e.pointerId) { return; } this.handleSetValue(e); }); this.addEventListener('pointerup', (e) => { if (this.downPointerId == null) { return; } this.downPointerId = undefined; e.stopPropagation?.(); }); const format = computed(() => { let format = this.properties.value.valueFormat ?? 'percentage'; if (format == 'percentage') { format = percentageFormatting; } return format; }); this.touchTarget = new Container(undefined, undefined, { defaultOverrides: { width: '100%', height: computed(() => (this.properties.value.size === 'lg' ? 48 : 64)), flexDirection: 'row', alignItems: 'center', '*': { hover: { backgroundColor: theme.component.slider.handle.background.hover, }, active: { backgroundColor: theme.component.slider.handle.background.pressed, }, }, }, }); this.track = new Container(undefined, undefined, { defaultOverrides: { width: '100%', borderRadius: 1000, backgroundColor: theme.component.slider.background, height: computed(() => sliderHeights[this.properties.value.size ?? 'md']), }, }); this.touchTarget.add(this.track); const percentage = computed(() => { const min = Number(this.properties.value.min ?? 0); const max = Number(this.properties.value.max ?? 100); const range = max - min; return `${(100 * (this.currentSignal.value - min)) / range}%`; }); this.thumb = new Container(undefined, undefined, { defaultOverrides: { flexShrink: 0, borderRadius: 1000, height: computed(() => sliderThumbHeights[this.properties.value.size ?? 'md']), minWidth: computed(() => sliderThumbHeights[this.properties.value.size ?? 'md']), paddingX: computed(() => (this.properties.value.size === 'lg' ? 16 : undefined)), flexDirection: 'row', alignItems: 'center', positionType: 'relative', }, }); this.progress = new Container(undefined, undefined, { defaultOverrides: { borderRadius: 1000, flexShrink: 0, backgroundColor: theme.component.slider.foreground.default, minWidth: computed(() => Math.max(sliderHeights[this.properties.value.size ?? 'md'], 2 * sliderProcessPaddingXs[this.properties.value.size ?? 'md'] + (this.thumb.size.value?.[0] ?? 0))), width: percentage, height: '100%', flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', paddingX: computed(() => sliderProcessPaddingXs[this.properties.value.size ?? 'md']), }, }); this.progress.add(this.thumb); this.track.add(this.progress); this.fill = this.progress; this.thumbText = new Text(undefined, undefined, { defaultOverrides: { color: theme.component.slider.handle.label, fontWeight: 700, lineHeight: '20px', backgroundColor: 'initial', display: computed(() => this.properties.value.size == 'lg' && this.properties.value.icon == null ? 'flex' : 'none'), fontSize: 14, text: computed(() => format.value(this.currentSignal.value)), }, }); this.thumb.add(this.thumbText); //setting up the icon abortableEffect(() => { const Icon = this.properties.value.icon; if (Icon == null) { return; } const icon = new Icon(undefined, undefined, { defaultOverrides: { color: theme.component.slider.handle.icon, backgroundColor: 'initial', width: 24, height: 24, positionType: 'absolute', positionLeft: '50%', positionTop: '50%', transformTranslateX: '-50%', transformTranslateY: '-50%', }, }); this.thumb.add(icon); this.icon = icon; return () => { icon.dispose(); this.icon = icon; }; }, this.abortSignal); this.labels = new Container(undefined, undefined, { defaultOverrides: { flexDirection: 'row', justifyContent: 'space-between', fontSize: 12, lineHeight: '16px', color: theme.component.slider.label, fontWeight: 500, }, }); this.labels.add((this.leftLabel = new Text(undefined, undefined, { defaultOverrides: { text: computed(() => this.properties.signal.leftLabel.value ?? format.value(Number(this.properties.value.min ?? 0))), }, })), (this.rightLabel = new Text(undefined, undefined, { defaultOverrides: { text: computed(() => this.properties.signal.rightLabel.value ?? format.value(Number(this.properties.value.max ?? 100))), }, }))); super.add(this.touchTarget); super.add(this.labels); } handleSetValue(e) { vectorHelper.copy(e.point); this.worldToLocal(vectorHelper); const minValue = Number(this.properties.peek().min ?? 0); const maxValue = Number(this.properties.peek().max ?? 100); const stepValue = Number(this.properties.peek().step ?? 0.0001); const newValue = Math.min(Math.max(Math.round(((vectorHelper.x + 0.5) * (maxValue - minValue) + minValue) / stepValue) * stepValue, minValue), maxValue); if (this.properties.peek().value == null) { this.uncontrolledSignal.value = newValue; } this.properties.peek().onValueChange?.(newValue); e.stopPropagation?.(); } dispose() { this.leftLabel.dispose(); this.rightLabel.dispose(); this.thumbText.dispose(); this.thumb.dispose(); this.progress.dispose(); this.track.dispose(); this.touchTarget.dispose(); this.icon?.dispose(); this.labels.dispose(); super.dispose(); } add() { throw new Error(`the Slider component can not have any children`); } }