Use @reach/slider to have styled enhanced sliders.pull/1/head
@@ -9,6 +9,7 @@ | |||
"lint": "next lint" | |||
}, | |||
"dependencies": { | |||
"@reach/slider": "^0.18.0", | |||
"@tesseract-design/web-action-react": "link:../../categories/action/react", | |||
"@tesseract-design/web-blob-react": "link:../../categories/blob/react", | |||
"@tesseract-design/web-formatted-react": "link:../../categories/formatted/react", | |||
@@ -17,6 +17,7 @@ import { | |||
import {getImageMetadata} from '../../../utils/image'; | |||
import {getAudioMetadata} from '../../../utils/audio'; | |||
import {getTextMetadata} from '../../../utils/text'; | |||
import {delegateTriggerChangeEvent} from '../../../utils/event'; | |||
interface FileWithPreview extends File { | |||
metadata?: Record<string, string | number | ArrayBuffer>; | |||
@@ -782,7 +783,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
}: FileButtonProps, | |||
forwardedRef, | |||
) => { | |||
const [isEnhanced, setIsEnhanced] = React.useState(false); | |||
const [renderEnhanced, setRenderEnhanced] = React.useState(false); | |||
const [fileList, setFileList] = React.useState<FileList>(); | |||
const defaultRef = React.useRef<HTMLInputElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
@@ -801,6 +802,9 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
if (typeof ref === 'object' && ref.current) { | |||
ref.current.value = ''; | |||
setFileList(undefined); | |||
setTimeout(() => { | |||
delegateTriggerChangeEvent(ref.current as HTMLInputElement); | |||
}) | |||
} | |||
}; | |||
@@ -815,12 +819,12 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
if (typeof ref === 'object' && ref.current) { | |||
const { files } = dataTransfer; | |||
setFileList(ref.current.files = files); | |||
ref.current.dispatchEvent(new Event('change')); | |||
delegateTriggerChangeEvent(ref.current); | |||
} | |||
} | |||
React.useEffect(() => { | |||
setIsEnhanced(enhanced); | |||
setRenderEnhanced(enhanced); | |||
}, [enhanced]); | |||
return ( | |||
@@ -843,7 +847,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
disabled={disabled} | |||
ref={ref} | |||
type="file" | |||
className={`${enhanced ? 'sr-only' : ''}`} | |||
className={`${renderEnhanced ? 'sr-only' : ''}`} | |||
onChange={addFile} | |||
multiple={multiple} | |||
data-testid="input" | |||
@@ -879,7 +883,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
} | |||
{ | |||
(fileList?.length ?? 0) < 1 | |||
&& isEnhanced | |||
&& renderEnhanced | |||
&& hint | |||
&& ( | |||
<div | |||
@@ -894,7 +898,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||
} | |||
{ | |||
(fileList?.length ?? 0) > 0 | |||
&& isEnhanced | |||
&& renderEnhanced | |||
&& ( | |||
<> | |||
<div className={`absolute top-0 left-0 w-full h-full pointer-events-none pb-12 box-border overflow-hidden pt-8`}> | |||
@@ -1,4 +1,185 @@ | |||
import * as React from 'react'; | |||
import { NextPage } from 'next'; | |||
import * as ReachSlider from '@reach/slider'; | |||
import '@reach/slider/styles.css'; | |||
import {delegateTriggerChangeEvent} from '../../../utils/event'; | |||
import * as SelectControlBase from '@tesseract-design/web-base-selectcontrol'; | |||
interface RenderOptionsProps { | |||
options: (number | SelectControlBase.SelectOption)[], | |||
optionComponent?: React.ElementType, | |||
optgroupComponent?: React.ElementType, | |||
level?: number, | |||
} | |||
export enum SliderOrientation { | |||
HORIZONTAL = 'horizontal', | |||
VERTICAL = 'vertical', | |||
} | |||
export interface SliderProps extends Omit<React.HTMLProps<HTMLInputElement>, 'type' | 'min' | 'max' | 'list'> { | |||
enhanced?: boolean; | |||
orient?: SliderOrientation; | |||
min?: number; | |||
max?: number; | |||
block?: boolean; | |||
tickMarks?: (number | SelectControlBase.SelectOption)[]; | |||
} | |||
const RenderOptions: React.FC<RenderOptionsProps> = ({ | |||
options, | |||
optionComponent: Option = 'option', | |||
optgroupComponent: Optgroup = 'optgroup', | |||
level = 0, | |||
}: RenderOptionsProps) => ( | |||
<> | |||
{ | |||
options.map((o) => { | |||
if (typeof o === 'number') { | |||
return ( | |||
<Option | |||
key={`${o}:${o}`} | |||
value={o} | |||
/> | |||
) | |||
} | |||
if (typeof o.value !== 'undefined') { | |||
return ( | |||
<Option | |||
key={`${o.label}:${o.value.toString()}`} | |||
value={o.value} | |||
label={o.label} | |||
> | |||
{o.label} | |||
</Option> | |||
); | |||
} | |||
if (typeof o.children !== 'undefined') { | |||
if (level === 0) { | |||
return ( | |||
<Optgroup | |||
key={o.label} | |||
label={o.label} | |||
> | |||
<RenderOptions | |||
options={o.children} | |||
optionComponent={Option} | |||
optgroupComponent={Optgroup} | |||
level={level + 1} | |||
/> | |||
</Optgroup> | |||
); | |||
} | |||
return ( | |||
<React.Fragment | |||
key={o.label} | |||
> | |||
<Option | |||
disabled | |||
> | |||
{o.label} | |||
</Option> | |||
<RenderOptions | |||
options={o.children} | |||
optionComponent={Option} | |||
optgroupComponent={Optgroup} | |||
level={level + 1} | |||
/> | |||
</React.Fragment> | |||
); | |||
} | |||
return null; | |||
}) | |||
} | |||
</> | |||
); | |||
export const Slider = React.forwardRef<HTMLInputElement, SliderProps>(({ | |||
enhanced = false, | |||
orient = SliderOrientation.HORIZONTAL, | |||
min = 0, | |||
max = 100, | |||
step = 'any', | |||
block = false, | |||
tickMarks = [], | |||
onChange, | |||
...etcProps | |||
}, forwardedRef) => { | |||
const [renderEnhanced, setRenderEnhanced] = React.useState(false); | |||
const defaultRef = React.useRef<HTMLInputElement>(null); | |||
const ref = forwardedRef ?? defaultRef; | |||
const nonStandardProps: any = { | |||
// Gecko (Firefox) fallback | |||
orient, | |||
} | |||
const tickMarkId = React.useId(); | |||
React.useEffect(() => { | |||
setRenderEnhanced(enhanced); | |||
}, [enhanced]); | |||
const handleEnhancedRangeChange = (newValue: number) => { | |||
if (typeof ref === 'object' && ref.current) { | |||
delegateTriggerChangeEvent(ref.current, newValue); | |||
} | |||
} | |||
return ( | |||
<> | |||
{ | |||
tickMarks.length > 0 | |||
&& ( | |||
<datalist | |||
id={tickMarkId} | |||
> | |||
<RenderOptions | |||
options={tickMarks} | |||
/> | |||
</datalist> | |||
) | |||
} | |||
<div> | |||
<input | |||
{...etcProps} | |||
{...nonStandardProps} | |||
min={min} | |||
max={max} | |||
step={step ?? 'any'} | |||
type="range" | |||
className={`${renderEnhanced ? 'sr-only' : ''} ${block ? 'w-full' : ''}`} | |||
ref={ref} | |||
list={tickMarks.length > 0 ? tickMarkId : undefined} | |||
onChange={onChange} | |||
/> | |||
{ | |||
renderEnhanced | |||
&& ( | |||
<> | |||
<ReachSlider.SliderInput | |||
min={min} | |||
max={max} | |||
step={typeof step === 'number' ? step : Number.EPSILON} | |||
orientation={orient === SliderOrientation.VERTICAL ? ReachSlider.SliderOrientation.Vertical : ReachSlider.SliderOrientation.Horizontal} | |||
onChange={handleEnhancedRangeChange} | |||
> | |||
<ReachSlider.SliderTrack> | |||
<ReachSlider.SliderRange /> | |||
<RenderOptions options={tickMarks} optionComponent={ReachSlider.SliderMarker} optgroupComponent={React.Fragment} /> | |||
<ReachSlider.SliderHandle /> | |||
</ReachSlider.SliderTrack> | |||
</ReachSlider.SliderInput> | |||
</> | |||
) | |||
} | |||
</div> | |||
</> | |||
); | |||
}); | |||
Slider.displayName = 'Slider'; | |||
const NumberPage: NextPage = () => { | |||
return ( | |||
@@ -24,9 +205,12 @@ const NumberPage: NextPage = () => { | |||
Slider | |||
</h2> | |||
<div> | |||
TODO | |||
<input type="range" /> | |||
<Slider | |||
min={-100} | |||
max={100} | |||
tickMarks={[{ label: 'low', value: 25, }, 50]} | |||
enhanced | |||
/> | |||
</div> | |||
</section> | |||
<section> | |||
@@ -35,9 +219,7 @@ const NumberPage: NextPage = () => { | |||
</h2> | |||
<div> | |||
TODO | |||
<input type="range" /> | |||
<input type="range" /> | |||
</div> | |||
</section> | |||
@@ -0,0 +1,35 @@ | |||
import { NextPage } from 'next'; | |||
import { DefaultLayout } from 'src/components/DefaultLayout'; | |||
const PresentationPage: NextPage = () => { | |||
return ( | |||
<DefaultLayout | |||
title="Code" | |||
> | |||
<main className="mt-8 mb-16 md:mt-16 md:mb-32"> | |||
<section> | |||
<div className="container mx-auto px-4"> | |||
<h2> | |||
Tabs | |||
</h2> | |||
<div> | |||
TODO | |||
</div> | |||
</div> | |||
</section> | |||
<section> | |||
<div className="container mx-auto px-4"> | |||
<h2> | |||
Accordion | |||
</h2> | |||
<div> | |||
TODO | |||
</div> | |||
</div> | |||
</section> | |||
</main> | |||
</DefaultLayout> | |||
) | |||
} | |||
export default PresentationPage; |
@@ -1 +1,5 @@ | |||
@tailwind utilities; | |||
input[type="range"][orient="vertical"] { | |||
appearance: slider-vertical; | |||
} |
@@ -0,0 +1,13 @@ | |||
export const delegateTriggerChangeEvent = <T extends HTMLElement>(target: T, value?: unknown) => { | |||
if (target.tagName === 'INPUT') { | |||
const inputTarget = target as unknown as HTMLInputElement; | |||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; | |||
if (nativeInputValueSetter) { | |||
if (inputTarget.type !== 'file') { | |||
nativeInputValueSetter.call(inputTarget, value); | |||
} | |||
const simulatedEvent = new Event('change', {bubbles: true}); | |||
inputTarget.dispatchEvent(simulatedEvent); | |||
} | |||
} | |||
} |
@@ -183,6 +183,32 @@ | |||
"@nodelib/fs.scandir" "2.1.5" | |||
fastq "^1.6.0" | |||
"@reach/auto-id@0.18.0": | |||
version "0.18.0" | |||
resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.18.0.tgz#4b97085cd1cf1360a9bedc6e9c78e97824014f0d" | |||
integrity sha512-XwY1IwhM7mkHZFghhjiqjQ6dstbOdpbFLdggeke75u8/8icT8uEHLbovFUgzKjy9qPvYwZIB87rLiR8WdtOXCg== | |||
dependencies: | |||
"@reach/utils" "0.18.0" | |||
"@reach/polymorphic@0.18.0": | |||
version "0.18.0" | |||
resolved "https://registry.yarnpkg.com/@reach/polymorphic/-/polymorphic-0.18.0.tgz#2fe42007a774e06cdbc8e13e0d46f2dc30f2f1ed" | |||
integrity sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA== | |||
"@reach/slider@^0.18.0": | |||
version "0.18.0" | |||
resolved "https://registry.yarnpkg.com/@reach/slider/-/slider-0.18.0.tgz#a2dbdad76611b0f12abc551849978e6237555331" | |||
integrity sha512-DLq8ziZn74P/puY0F2tO7fR67PPp7/MIiydfvUdB3kefmn8ewXMUrY7tO6sdhVXk2y2Jtncd5PO1NStPn1QFfw== | |||
dependencies: | |||
"@reach/auto-id" "0.18.0" | |||
"@reach/polymorphic" "0.18.0" | |||
"@reach/utils" "0.18.0" | |||
"@reach/utils@0.18.0": | |||
version "0.18.0" | |||
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" | |||
integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== | |||
"@rushstack/eslint-patch@^1.1.3": | |||
version "1.2.0" | |||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" | |||