Skip to content

Commit

Permalink
useKnobKeyboardControl
Browse files Browse the repository at this point in the history
  • Loading branch information
satelllte committed Jan 13, 2024
1 parent 29ed968 commit 28f5105
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 7 deletions.
51 changes: 45 additions & 6 deletions apps/docs/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,6 @@ function IndexPage() {
"package.json" file by installing it via
"--save-exact" flag.
</Li>
<Li>
There is no keyboard interaction provided by default. This is
intentional, as knob behaviours might differ significantly between
use cases. To achieve keyboard interaction, you can just provide
your own &quot;onKeyDown&quot; listener.
</Li>
</Ul>
</Section>
<Section title='API'>
Expand Down Expand Up @@ -230,6 +224,51 @@ function IndexPage() {
},
]}
/>
<ComponentDocumentation
name='useKnobKeyboardControl'
about='A primitive for enabling keyboard controls.'
properties={[
{
name: 'valueRaw',
type: 'number',
description: 'Same as "valueRaw" prop of "KnobHeadless".',
},
{
name: 'valueMin',
type: 'number',
description: 'Same as "valueMin" prop of "KnobHeadless".',
},
{
name: 'valueMax',
type: 'number',
description: 'Same as "valueMax" prop of "KnobHeadless".',
},
{
name: 'step',
type: 'number',
description: "Step value. Typically it's 1% of the range.",
},
{
name: 'stepLarger',
type: 'number',
description:
"Larger step value. Typically it's 10% of the range.",
},
{
name: 'onValueRawChange',
type: 'function',
description:
'Same callback as "KnobHeadless" has, with "event" in 2nd argument.',
},
{
name: 'noDefaultPrevention',
type: 'boolean',
defaultValue: 'false',
description:
'To prevent scrolling, "event.preventDefault()" is called when the value changes, so for most cases you don\'t need to change this behaviour. However, if your application needs some more customized behaviour, you can set this prop to true and handle it on its own.',
},
]}
/>
</div>
</Section>
</div>
Expand Down
19 changes: 18 additions & 1 deletion apps/docs/src/components/knobs/KnobBase.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use client';
import clsx from 'clsx';
import {useId, useState} from 'react';
import {
KnobHeadless,
KnobHeadlessLabel,
KnobHeadlessOutput,
useKnobKeyboardControl,
} from 'react-knob-headless';
import {mapFrom01Linear, mapTo01Linear} from '@dsp-ts/math';
import {KnobBaseThumb} from './KnobBaseThumb';
Expand All @@ -24,6 +24,8 @@ type KnobBaseProps = Pick<
Pick<KnobBaseThumbProps, 'theme'> & {
readonly label: string;
readonly valueDefault: number;
readonly stepFn: (valueRaw: number) => number;
readonly stepLargerFn: (valueRaw: number) => number;
};

export function KnobBase({
Expand All @@ -35,14 +37,28 @@ export function KnobBase({
valueRawRoundFn,
valueRawDisplayFn,
orientation,
stepFn,
stepLargerFn,
mapTo01 = mapTo01Linear,
mapFrom01 = mapFrom01Linear,
}: KnobBaseProps) {
const knobId = useId();
const labelId = useId();
const [valueRaw, setValueRaw] = useState<number>(valueDefault);
const value01 = mapTo01(valueRaw, valueMin, valueMax);
const step = stepFn(valueRaw);
const stepLarger = stepLargerFn(valueRaw);
const dragSensitivity = 0.006;

const onKeyDown = useKnobKeyboardControl({
valueRaw,
valueMin,
valueMax,
step,
stepLarger,
onValueRawChange: setValueRaw,
});

return (
<div
className={clsx(
Expand All @@ -65,6 +81,7 @@ export function KnobBase({
mapTo01={mapTo01}
mapFrom01={mapFrom01}
onValueRawChange={setValueRaw}
onKeyDown={onKeyDown}
>
<KnobBaseThumb theme={theme} value01={value01} />
</KnobHeadless>
Expand Down
15 changes: 15 additions & 0 deletions apps/docs/src/components/knobs/KnobFrequency.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export function KnobFrequency(props: KnobFrequencyProps) {
valueDefault={valueDefault}
valueMin={valueMin}
valueMax={valueMax}
stepFn={stepFn}
stepLargerFn={stepLargerFn}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
mapTo01={mapTo01}
Expand All @@ -26,6 +28,19 @@ export function KnobFrequency(props: KnobFrequencyProps) {
const valueMin = 20;
const valueMax = 20000;
const valueDefault = 440;
const stepFn = (valueRaw: number): number => {
if (valueRaw < 100) {
return 1;
}

if (valueRaw < 1000) {
return 10;
}

return 100;
};

const stepLargerFn = (valueRaw: number): number => stepFn(valueRaw) * 10;
const valueRawRoundFn = (x: number): number => x;
const valueRawDisplayFn = (hz: number): string => {
if (hz < 100) {
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/src/components/knobs/KnobPercentage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export function KnobPercentage(props: KnobPercentageProps) {
valueDefault={valueDefault}
valueMin={valueMin}
valueMax={valueMax}
stepFn={stepFn}
stepLargerFn={stepLargerFn}
valueRawRoundFn={valueRawRoundFn}
valueRawDisplayFn={valueRawDisplayFn}
{...props}
Expand All @@ -23,6 +25,8 @@ export function KnobPercentage(props: KnobPercentageProps) {
const valueMin = 0;
const valueMax = 100;
const valueDefault = 50;
const stepFn = (valueRaw: number): number => 1;
const stepLargerFn = (valueRaw: number): number => 10;
const valueRawRoundFn = Math.round;
const valueRawDisplayFn = (valueRaw: number): string =>
`${valueRawRoundFn(valueRaw)}%`;
1 change: 1 addition & 0 deletions packages/react-knob-headless/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export {KnobHeadless} from './KnobHeadless';
export {KnobHeadlessLabel} from './KnobHeadlessLabel';
export {KnobHeadlessOutput} from './KnobHeadlessOutput';
export {useKnobKeyboardControl} from './useKnobKeyboardControl';
98 changes: 98 additions & 0 deletions packages/react-knob-headless/src/useKnobKeyboardControl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {clamp} from '@dsp-ts/math';

type UseKnobKeyboardControlProps = {
/**
* Same as `valueRaw` prop of `KnobHeadless`.
*/
readonly valueRaw: number;
/**
* Same as `valueMin` prop of `KnobHeadless`.
*/
readonly valueMin: number;
/**
* Same as `valueMax` prop of `KnobHeadless`.
*/
readonly valueMax: number;
/**
* Step value. Typically it's 1% of the range.
*/
readonly step: number;
/**
* Larger step value. Typically it's 10% of the range.
*/
readonly stepLarger: number;
/**
* Same callback as `KnobHeadless` has, with "event" in 2nd argument.
*/
readonly onValueRawChange: (
newValueRaw: number,
event: React.KeyboardEvent,
) => void;
/**
* To prevent scrolling, "event.preventDefault()" is called when the value changes,
* so for most cases you don't need to change this behaviour.
* However, if your application needs some more customized behaviour, you can set this prop to true and handle it on its own.
*/
readonly noDefaultPrevention?: boolean;
};

export const useKnobKeyboardControl =
({
valueRaw,
valueMin,
valueMax,
step,
stepLarger,
onValueRawChange,
noDefaultPrevention = false,
}: UseKnobKeyboardControlProps): React.KeyboardEventHandler =>
(event) => {
const {code} = event;
switch (code) {
case 'ArrowUp':
case 'ArrowRight':
onValueRawChange(clamp(valueRaw + step, valueMin, valueMax), event);
maybePreventDefault({event, noDefaultPrevention});
break;
case 'ArrowDown':
case 'ArrowLeft':
onValueRawChange(clamp(valueRaw - step, valueMin, valueMax), event);
maybePreventDefault({event, noDefaultPrevention});
break;
case 'PageUp':
onValueRawChange(
clamp(valueRaw + stepLarger, valueMin, valueMax),
event,
);
maybePreventDefault({event, noDefaultPrevention});
break;
case 'PageDown':
onValueRawChange(
clamp(valueRaw - stepLarger, valueMin, valueMax),
event,
);
maybePreventDefault({event, noDefaultPrevention});
break;
case 'Home':
onValueRawChange(valueMin, event);
maybePreventDefault({event, noDefaultPrevention});
break;
case 'End':
onValueRawChange(valueMax, event);
maybePreventDefault({event, noDefaultPrevention});
break;
default:
break;
}
};

const maybePreventDefault = ({
event,
noDefaultPrevention,
}: {
event: React.KeyboardEvent;
noDefaultPrevention: boolean;
}): void => {
if (noDefaultPrevention) return;
event.preventDefault();
};

0 comments on commit 28f5105

Please sign in to comment.