Skip to content

Commit 6c57d87

Browse files
committed
feat: controlled deviceEmulator && deviceSelector
1 parent a01af8f commit 6c57d87

File tree

6 files changed

+96
-49
lines changed

6 files changed

+96
-49
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPh
6767
type DeviceEmulatorProps = {
6868
banDevices?: DeviceName[]
6969
children: (props: DeviceFramesetProps) => React.ReactNode,
70+
value?: DeviceName,
71+
onChange?: (deviceName: DeviceName) => void,
7072
}
7173
```
7274
@@ -92,6 +94,8 @@ type DeviceName = "iPhone X" | "iPhone 8" | "iPhone 8 Plus" | "iPhone 5s" | "iPh
9294
type DeviceEmulatorProps = {
9395
banDevices?: DeviceName[]
9496
children: (props: DeviceFramesetProps) => React.ReactNode,
97+
value?: DeviceName,
98+
onChange?: (deviceName: DeviceName) => void,
9599
}
96100
```
97101

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"typescript"
2323
],
2424
"repository": "https://github.com/zheeeng/react-device-frameset",
25-
"version": "1.0.9",
25+
"version": "1.1.0",
2626
"license": "MIT",
2727
"main": "lib/DeviceFrameset.js",
2828
"scripts": {

src/DeviceEmulator.tsx

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,58 @@ import { DeviceName, DeviceNames } from './DeviceOptions'
55
export type DeviceEmulatorProps = React.HTMLAttributes<HTMLDivElement> & {
66
banDevices?: DeviceName[]
77
children: (props: DeviceFramesetProps) => React.ReactNode,
8+
value?: DeviceName,
9+
onChange?: (deviceName: DeviceName) => void,
810
}
911

10-
export const DeviceEmulator = React.memo<DeviceEmulatorProps>(function DeviceEmulator ({ children, banDevices = [], ...divProps }) {
12+
export const DeviceEmulator = React.memo<DeviceEmulatorProps>(function DeviceEmulator ({ children, value, onChange, banDevices = [], ...divProps }) {
1113
const deviceNames = useMemo(() => DeviceNames.filter(devName => !banDevices.includes(devName)) as Array<keyof typeof DeviceOptions>, [])
1214
const [deviceName, setDeviceName] = useState<DeviceName>(deviceNames[0] ?? '')
15+
const selectedDeviceName = useMemo(() => value ?? deviceName, [value, deviceName])
1316

1417
const handleSelectChange = useCallback(
1518
(event: React.ChangeEvent<HTMLSelectElement>) => {
16-
setDeviceName(event.target.value as DeviceName)
19+
const newDeviceName = event.target.value as DeviceName
20+
if (!deviceNames.includes(newDeviceName)) throw new Error(`Invalid device name for ${newDeviceName}`)
21+
22+
onChange?.(newDeviceName)
23+
setDeviceName(newDeviceName)
1724
},
18-
[],
25+
[deviceNames, onChange],
1926
)
2027

21-
const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[deviceName], [deviceName])
28+
const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[selectedDeviceName], [selectedDeviceName])
29+
30+
const firstColor = useMemo(() => colors[0]!, [colors])
2231

23-
const [selectedColorIndex, setSelectedColorIndex] = useState(0)
2432
const [isLandscape, setIsLandscape] = useState<boolean | undefined>(undefined)
2533

26-
useEffect(
27-
() => { setSelectedColorIndex(0) },
28-
[colors],
29-
)
30-
useEffect(
31-
() => { setIsLandscape(hasLandscape ? false : undefined) },
32-
[hasLandscape],
33-
)
34+
const isLandscapeChecked = useMemo(() => hasLandscape ? isLandscape : undefined, [hasLandscape, isLandscape])
3435

35-
const selectedColor = useMemo(() => colors[selectedColorIndex], [colors, selectedColorIndex])
36+
const handleIsLandscapeChange = useCallback(
37+
() => {
38+
if (!hasLandscape) return
39+
40+
setIsLandscape(is => !is)
41+
},
42+
[hasLandscape]
43+
)
3644

3745
const deviceFramesetProps = useMemo(
3846
() => ({
39-
device: deviceName,
40-
color: selectedColor,
41-
landscape: isLandscape,
47+
device: selectedDeviceName,
48+
color: firstColor,
49+
landscape: isLandscapeChecked,
4250
width,
4351
height,
4452
}) as DeviceFramesetProps,
45-
[deviceName, selectedColor, isLandscape, width, height],
53+
[selectedDeviceName, firstColor, isLandscapeChecked, width, height],
4654
)
4755

4856
return (
4957
<div className="device-emulator" {...divProps}>
5058
<section>
51-
<select value={deviceName} onChange={handleSelectChange}>
59+
<select value={selectedDeviceName} onChange={handleSelectChange}>
5260
{deviceNames.map((devName) => (
5361
<option
5462
key={devName}
@@ -60,7 +68,7 @@ export const DeviceEmulator = React.memo<DeviceEmulatorProps>(function DeviceEmu
6068
<span>x</span>
6169
<input disabled value={height} />
6270
<label>Landscape:</label>
63-
<input type="checkbox" checked={isLandscape} onClick={() => setIsLandscape(is => !is)}/>
71+
<input type="checkbox" checked={!!isLandscapeChecked} disabled={!hasLandscape} onChange={handleIsLandscapeChange}/>
6472
</section>
6573

6674
<div className="device-emulator-container">

src/DeviceFrameset.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export { DeviceOptions, DeviceFramesetProps }
77

88
export const DeviceFrameset = React.memo<DeviceFramesetProps>(
99
function DeviceFrameset(props) {
10-
const { children, device, width, height, ...divProps } = props
10+
const { children, device, width, height, ...restProps } = props
11+
// @ts-expect-error
12+
const { landscape: _l, color: _c, ...divProps } = restProps
13+
1114
const color = 'color' in props ? props.color : undefined
1215
const landscape = 'landscape' in props ? props.landscape : undefined
1316

src/DeviceSelector.tsx

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,93 @@
1-
import React, { useEffect, useMemo, useState } from 'react'
1+
import React, { useEffect, useMemo, useState, useCallback } from 'react'
22
import { DeviceOptions, DeviceFramesetProps } from './DeviceFrameset'
33
import { DeviceName, DeviceNames } from './DeviceOptions'
44

55
export type DeviceSelectorProps = React.HTMLAttributes<HTMLDivElement> & {
66
banDevices?: DeviceName[],
77
children: (props: DeviceFramesetProps) => React.ReactNode,
8+
value?: DeviceName,
9+
onChange?: (deviceName: DeviceName) => void,
810
}
911

10-
export const DeviceSelector = React.memo<DeviceSelectorProps>(function DeviceSelector ({ children, banDevices = [], ...divProps }) {
12+
export const DeviceSelector = React.memo<DeviceSelectorProps>(function DeviceSelector ({ children, value, onChange, banDevices = [], ...divProps }) {
1113
const deviceNames = useMemo(() => DeviceNames.filter(devName => !banDevices.includes(devName)) as Array<keyof typeof DeviceOptions>, [])
12-
const [selectedDeviceIndex, setSelectedDeviceIndex] = useState(0)
14+
const [deviceName, setDeviceName] = useState<DeviceName>(deviceNames[0] ?? '')
15+
const selectedDeviceName = useMemo(() => value ?? deviceName, [value, deviceName])
16+
17+
const handleSelectChange = useCallback(
18+
(event: React.MouseEvent<HTMLElement>) => {
19+
20+
const newDeviceName = event.currentTarget.dataset['deviceName'] as DeviceName
21+
if (!deviceNames.includes(newDeviceName)) throw new Error(`Invalid device name for ${newDeviceName}`)
22+
23+
onChange?.(newDeviceName)
24+
setDeviceName(newDeviceName)
25+
},
26+
[deviceNames, onChange],
27+
)
28+
1329
const [showMenu, setShowMenu] = useState(true)
1430

15-
const deviceName = useMemo(() => deviceNames[selectedDeviceIndex], [selectedDeviceIndex])
31+
const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[selectedDeviceName], [selectedDeviceName])
1632

17-
const { colors, hasLandscape, width, height } = useMemo(() => DeviceOptions[deviceName], [deviceName])
33+
const firstColor = useMemo(() => colors[0]!, [colors])
1834

19-
const [selectedColorIndex, setSelectedColorIndex] = useState(0)
20-
const [isLandscape, setIsLandscape] = useState<boolean | undefined>(undefined)
35+
const [selectedColor, setSelectedColor] = useState<typeof colors[number]>(firstColor)
2136

22-
useEffect(
23-
() => { setSelectedColorIndex(0) },
24-
[colors],
25-
)
26-
useEffect(
27-
() => { setIsLandscape(hasLandscape ? false : undefined) },
28-
[hasLandscape],
37+
const handleColorChange = useCallback(
38+
(event: React.MouseEvent<HTMLLIElement>) => {
39+
40+
const newDeviceColor = event.currentTarget.dataset['deviceColor'] as typeof colors[number]
41+
42+
setSelectedColor(newDeviceColor)
43+
},
44+
[],
2945
)
3046

31-
const selectedColor = useMemo(() => colors[selectedColorIndex], [colors, selectedColorIndex])
47+
useEffect(() => { setSelectedColor(firstColor) }, [firstColor])
48+
49+
const [isLandscape, setIsLandscape] = useState<boolean | undefined>(undefined)
50+
51+
const isLandscapeChecked = useMemo(() => hasLandscape ? isLandscape : undefined, [hasLandscape, isLandscape])
52+
53+
const handleIsLandscapeChange = useCallback(
54+
() => {
55+
if (!hasLandscape) return
56+
57+
setIsLandscape(is => !is)
58+
},
59+
[hasLandscape]
60+
)
3261

3362
const deviceFramesetProps = useMemo(
3463
() => ({
35-
device: deviceName,
64+
device: selectedDeviceName,
3665
color: selectedColor,
37-
landscape: isLandscape,
66+
landscape: isLandscapeChecked,
3867
width,
3968
height,
4069
}) as DeviceFramesetProps,
41-
[deviceName, selectedColor, isLandscape, width, height],
70+
[selectedDeviceName, selectedColor, isLandscapeChecked, width, height],
4271
)
4372

4473
return (
4574
<div className="device-selector" {...divProps}>
4675
<dl>
4776
<dt>
48-
<p>The Chosen: {deviceName}</p>
77+
<p>The Chosen: {selectedDeviceName}</p>
4978
<span
5079
className={(showMenu ? 'active' : '')}
5180
onClick={() => setShowMenu(is => !is)}
5281
>
5382
show all devices
5483
</span>
5584
</dt>
56-
{showMenu && deviceNames.map((devName, index) => (
85+
{showMenu && deviceNames.map((devName) => (
5786
<dd
5887
key={devName}
59-
onClick={() => setSelectedDeviceIndex(index)}
60-
className={devName === deviceName ? 'active' : ''}
88+
data-device-name={devName}
89+
onClick={handleSelectChange}
90+
className={devName === selectedDeviceName ? 'active' : ''}
6191
>
6292
<input type="radio" id={devName} />
6393
<label htmlFor={devName}>
@@ -66,19 +96,20 @@ export const DeviceSelector = React.memo<DeviceSelectorProps>(function DeviceSel
6696
{DeviceOptions[devName].hasLandscape && (
6797
<span
6898
className={(devName === deviceName && isLandscape) ? 'active' : ''}
69-
onClick={() => setIsLandscape(is => !is)}
99+
onClick={handleIsLandscapeChange}
70100
>
71101
landscape
72102
</span>
73103
)}
74104
</div>
75105
<ul>
76106
{DeviceOptions[devName].colors.map(
77-
(color: string, index: number) => (
107+
(color: string) => (
78108
<li
79109
key={color}
80110
title={color}
81-
onClick={() => setSelectedColorIndex(index)}
111+
data-device-color={color}
112+
onClick={handleColorChange}
82113
className={[((devName === deviceName && color === selectedColor) ? 'active' : ''), color].join(' ')}
83114
/>
84115
)

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export * from './DeviceFrameset'
2-
export * from './DeviceSelector'
1+
export * from './DeviceFrameset';
2+
export * from './DeviceSelector';
3+
export * from './DeviceEmulator';

0 commit comments

Comments
 (0)