Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/settings slider text input #107

Merged
2 changes: 1 addition & 1 deletion src/components/ui/SettingsInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default ({ settings, editing, value, setValue }: Props) => {
<input
type="text"
value={value()}
class="w-full mt-1 bg-transparent border border-base px-2 py-1 focus:border-base-100 transition-colors-200"
class="w-full mt-1 bg-transparent border border-base px-2 py-1 focus:border-base-100 transition-colors-200"
onChange={e => setValue(e.currentTarget.value)}
/>
)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/ui/SettingsSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export default ({ settings, editing, value, setValue }: Props) => {
step={sliderSettings.step}
/>
)}
{!editing() && value() && (
{!editing() && value() !== undefined && (
<div>{value()}</div>
)}
{!editing() && !value() && (
{!editing() && value() === undefined && (
<SettingsNotDefined />
)}
</div>
Expand Down
114 changes: 104 additions & 10 deletions src/components/ui/base/Slider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as slider from '@zag-js/slider'
import { normalizeProps, useMachine } from '@zag-js/solid'
import { createMemo, createUniqueId, mergeProps } from 'solid-js'
import { createMemo, createSignal, createUniqueId, mergeProps } from 'solid-js'
import type { Accessor } from 'solid-js'

interface Props {
Expand All @@ -9,22 +9,30 @@ interface Props {
max: number
step: number
disabled?: boolean
canEditSliderViaInput?: boolean
setValue: (v: number) => void
}

export const Slider = (selectProps: Props) => {
const props = mergeProps({
min: 0,
max: 10,
step: 1,
disabled: false,
}, selectProps)
const props = mergeProps(
{
min: 0,
max: 10,
step: 1,
disabled: false,
canEditSliderViaInput: true,
},
selectProps,
)

const formatSliderValue = (value: number) => {
if (!value) return 0

return Number.isInteger(value) ? value : parseFloat(value.toFixed(2))
}

const [input, setInput] = createSignal(props.value(), { equals: false })

const [state, send] = useMachine(slider.machine({
id: createUniqueId(),
value: props.value(),
Expand All @@ -33,15 +41,72 @@ export const Slider = (selectProps: Props) => {
step: props.step,
disabled: props.disabled,
onChange: (details) => {
details && details.value && props.setValue(formatSliderValue(details.value))
if (!details) return

const value = formatSliderValue(details.value)
props.setValue(value)
setInput(value)
},
}))

const api = createMemo(() => slider.connect(state, send, normalizeProps))

return (
<div {...api().rootProps}>
<div class="text-xs op-50 fb items-center">
<div class="text-xs op-50 focus-within:op-100 fb items-center">
<div />
<output {...api().outputProps}>{formatSliderValue(api().value)}</output>
{!props.canEditSliderViaInput && (
<output {...api().outputProps}>
{formatSliderValue(api().value)}
</output>
)}
{props.canEditSliderViaInput && (
<input
type="text"
spellcheck={false}
autocomplete="off"
autocorrect="off"
aria-valuemax={props.max}
aria-valuemin={props.min}
aria-valuenow={input()}
aria-controls={api().hiddenInputProps.id}
aria-live="off"
aria-label="Enter value to adjust slider"
data-scope="slider"
class="bg-transparent border border-transparent w-[80px] text-right px-2 py-1 hover:border-base focus:border-base-100 transition-colors-200"
value={input()}
onInput={(e) => {
const target = e.target
if (!target) return

let value = Number(target.value)

if (Number.isNaN(value)) value = props.value()

api().setValue(value)
}}
onBlur={(e) => {
const target = e.target
if (!target) return

let value = Number(target.value)

if (Number.isNaN(value)) value = props.value()

value = adjustValueToStep(
value,
props.step,
props.min,
props.max,
)

setInput(value)
}}
onKeyUp={(e) => {
if (e.key === 'Enter') e.currentTarget.blur()
}}
/>
)}
</div>
<div class="mt-2" {...api().controlProps}>
<div {...api().trackProps}>
Expand All @@ -54,3 +119,32 @@ export const Slider = (selectProps: Props) => {
</div>
)
}

/**
* Adjusts the given value to the nearest multiple of 'step'
* and ensures that the result lies within the range [min, max].
*
* @param value - The value to be adjusted.
* @param step - The step size to which the value should be adjusted.
* @param min - The minimum allowable value.
* @param max - The maximum allowable value.
*
* @returns The adjusted value.
*/
function adjustValueToStep(
value: number,
step: number,
min: number,
max: number,
) {
// Adjust the value to the nearest step
const adjustedValue = Math.round((value - min) / step) * step + min

// Clamp the value to the min and max
const boundedValue = Math.min(Math.max(adjustedValue, min), max)

// Round the value to the nearest decimal place
const decimalPlaces = (step.toString().split('.')[1] || []).length

return parseFloat(boundedValue.toFixed(decimalPlaces))
}