Adds a range selector component (#38191)
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
Inspired by:
|
||||||
|
https://danielstern.ca/range.css
|
||||||
|
https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/
|
||||||
|
*/
|
||||||
|
|
||||||
|
.input {
|
||||||
|
--color-bg-thumb: var(--color-bg-brand-base);
|
||||||
|
--color-bg-thumb-hover: var(--color-bg-brand-base-hover);
|
||||||
|
--color-bg-track: var(--color-bg-secondary);
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 0;
|
||||||
|
background-color: transparent;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumb
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
margin-top: -6px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--color-bg-thumb);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--color-bg-thumb);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--color-bg-thumb);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 0; // Needed to keep the Edge thumb centred
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
background: var(--color-bg-thumb-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
background: var(--color-bg-thumb-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-thumb {
|
||||||
|
background: var(--color-bg-thumb-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-thumb {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
background: var(--color-bg-track);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 1.3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
background: var(--color-bg-track);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 1.3px;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-ms-track {
|
||||||
|
background: var(--color-bg-track);
|
||||||
|
border: 0;
|
||||||
|
color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
option {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
|
||||||
|
import { RangeInputField } from './range_input_field';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Form Fields/RangeInputField',
|
||||||
|
component: RangeInputField,
|
||||||
|
args: {
|
||||||
|
label: 'Label',
|
||||||
|
hint: 'This is a description of this form field',
|
||||||
|
checked: false,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof RangeInputField>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Simple: Story = {};
|
||||||
|
|
||||||
|
export const Markers: Story = {
|
||||||
|
args: {
|
||||||
|
markers: [
|
||||||
|
{ value: 0, label: 'None' },
|
||||||
|
{ value: 25, label: 'Some' },
|
||||||
|
{ value: 50, label: 'Half' },
|
||||||
|
{ value: 75, label: 'Most' },
|
||||||
|
{ value: 100, label: 'All' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { ComponentPropsWithoutRef } from 'react';
|
||||||
|
import { forwardRef, useId } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { FormFieldWrapper } from './form_field_wrapper';
|
||||||
|
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||||
|
import classes from './range_input.module.scss';
|
||||||
|
|
||||||
|
export type RangeInputProps = Omit<
|
||||||
|
ComponentPropsWithoutRef<'input'>,
|
||||||
|
'type' | 'list'
|
||||||
|
> & {
|
||||||
|
markers?: { value: number; label: string }[] | number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props extends RangeInputProps, CommonFieldWrapperProps {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple form field for single-line text.
|
||||||
|
*
|
||||||
|
* Accepts an optional `hint` and can be marked as required
|
||||||
|
* or optional (by explicitly setting `required={false}`)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const RangeInputField = forwardRef<HTMLInputElement, Props>(
|
||||||
|
(
|
||||||
|
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
|
<FormFieldWrapper
|
||||||
|
label={label}
|
||||||
|
hint={hint}
|
||||||
|
required={required}
|
||||||
|
status={status}
|
||||||
|
inputId={id}
|
||||||
|
className={wrapperClassName}
|
||||||
|
>
|
||||||
|
{(inputProps) => <RangeInput {...otherProps} {...inputProps} ref={ref} />}
|
||||||
|
</FormFieldWrapper>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
RangeInputField.displayName = 'RangeInputField';
|
||||||
|
|
||||||
|
export const RangeInput = forwardRef<HTMLInputElement, RangeInputProps>(
|
||||||
|
({ className, markers, id, ...otherProps }, ref) => {
|
||||||
|
const markersId = useId();
|
||||||
|
|
||||||
|
if (!markers) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
{...otherProps}
|
||||||
|
type='range'
|
||||||
|
className={classNames(className, classes.input)}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
{...otherProps}
|
||||||
|
type='range'
|
||||||
|
className={classNames(className, classes.input)}
|
||||||
|
ref={ref}
|
||||||
|
list={markersId}
|
||||||
|
/>
|
||||||
|
<datalist id={markersId} className={classes.markers}>
|
||||||
|
{markers.map((marker) => {
|
||||||
|
const value = typeof marker === 'number' ? marker : marker.value;
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
label={typeof marker !== 'number' ? marker.label : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</datalist>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
RangeInput.displayName = 'RangeInput';
|
||||||
Reference in New Issue
Block a user