mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
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