@@ -9,6 +9,7 @@ | |||||
"lint": "next lint" | "lint": "next lint" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@reach/slider": "^0.18.0", | |||||
"@tesseract-design/web-action-react": "link:../../categories/action/react", | "@tesseract-design/web-action-react": "link:../../categories/action/react", | ||||
"@tesseract-design/web-blob-react": "link:../../categories/blob/react", | "@tesseract-design/web-blob-react": "link:../../categories/blob/react", | ||||
"@tesseract-design/web-formatted-react": "link:../../categories/formatted/react", | "@tesseract-design/web-formatted-react": "link:../../categories/formatted/react", | ||||
@@ -17,6 +17,7 @@ import { | |||||
import {getImageMetadata} from '../../../utils/image'; | import {getImageMetadata} from '../../../utils/image'; | ||||
import {getAudioMetadata} from '../../../utils/audio'; | import {getAudioMetadata} from '../../../utils/audio'; | ||||
import {getTextMetadata} from '../../../utils/text'; | import {getTextMetadata} from '../../../utils/text'; | ||||
import {delegateTriggerChangeEvent} from '../../../utils/event'; | |||||
interface FileWithPreview extends File { | interface FileWithPreview extends File { | ||||
metadata?: Record<string, string | number | ArrayBuffer>; | metadata?: Record<string, string | number | ArrayBuffer>; | ||||
@@ -782,7 +783,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||||
}: FileButtonProps, | }: FileButtonProps, | ||||
forwardedRef, | forwardedRef, | ||||
) => { | ) => { | ||||
const [isEnhanced, setIsEnhanced] = React.useState(false); | |||||
const [renderEnhanced, setRenderEnhanced] = React.useState(false); | |||||
const [fileList, setFileList] = React.useState<FileList>(); | const [fileList, setFileList] = React.useState<FileList>(); | ||||
const defaultRef = React.useRef<HTMLInputElement>(null); | const defaultRef = React.useRef<HTMLInputElement>(null); | ||||
const ref = forwardedRef ?? defaultRef; | const ref = forwardedRef ?? defaultRef; | ||||
@@ -801,6 +802,9 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||||
if (typeof ref === 'object' && ref.current) { | if (typeof ref === 'object' && ref.current) { | ||||
ref.current.value = ''; | ref.current.value = ''; | ||||
setFileList(undefined); | 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) { | if (typeof ref === 'object' && ref.current) { | ||||
const { files } = dataTransfer; | const { files } = dataTransfer; | ||||
setFileList(ref.current.files = files); | setFileList(ref.current.files = files); | ||||
ref.current.dispatchEvent(new Event('change')); | |||||
delegateTriggerChangeEvent(ref.current); | |||||
} | } | ||||
} | } | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
setIsEnhanced(enhanced); | |||||
setRenderEnhanced(enhanced); | |||||
}, [enhanced]); | }, [enhanced]); | ||||
return ( | return ( | ||||
@@ -843,7 +847,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||||
disabled={disabled} | disabled={disabled} | ||||
ref={ref} | ref={ref} | ||||
type="file" | type="file" | ||||
className={`${enhanced ? 'sr-only' : ''}`} | |||||
className={`${renderEnhanced ? 'sr-only' : ''}`} | |||||
onChange={addFile} | onChange={addFile} | ||||
multiple={multiple} | multiple={multiple} | ||||
data-testid="input" | data-testid="input" | ||||
@@ -879,7 +883,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||||
} | } | ||||
{ | { | ||||
(fileList?.length ?? 0) < 1 | (fileList?.length ?? 0) < 1 | ||||
&& isEnhanced | |||||
&& renderEnhanced | |||||
&& hint | && hint | ||||
&& ( | && ( | ||||
<div | <div | ||||
@@ -894,7 +898,7 @@ export const FileSelectBox = React.forwardRef<HTMLInputElement, FileButtonProps> | |||||
} | } | ||||
{ | { | ||||
(fileList?.length ?? 0) > 0 | (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`}> | <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 { 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 = () => { | const NumberPage: NextPage = () => { | ||||
return ( | return ( | ||||
@@ -24,9 +205,12 @@ const NumberPage: NextPage = () => { | |||||
Slider | Slider | ||||
</h2> | </h2> | ||||
<div> | <div> | ||||
TODO | |||||
<input type="range" /> | |||||
<Slider | |||||
min={-100} | |||||
max={100} | |||||
tickMarks={[{ label: 'low', value: 25, }, 50]} | |||||
enhanced | |||||
/> | |||||
</div> | </div> | ||||
</section> | </section> | ||||
<section> | <section> | ||||
@@ -35,9 +219,7 @@ const NumberPage: NextPage = () => { | |||||
</h2> | </h2> | ||||
<div> | <div> | ||||
TODO | TODO | ||||
<input type="range" /> | <input type="range" /> | ||||
<input type="range" /> | <input type="range" /> | ||||
</div> | </div> | ||||
</section> | </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; | @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" | "@nodelib/fs.scandir" "2.1.5" | ||||
fastq "^1.6.0" | 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": | "@rushstack/eslint-patch@^1.1.3": | ||||
version "1.2.0" | version "1.2.0" | ||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" | resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" | ||||