test(web): Switch (and add label back in)

This commit is contained in:
Paul Armstrong 2021-02-12 16:06:51 -08:00 committed by Blake Blackshear
parent f70fb12c3d
commit 5eaf8a5448
5 changed files with 138 additions and 71 deletions

View File

@ -1,7 +1,7 @@
import { h } from 'preact'; import { h } from 'preact';
import { useCallback, useState } from 'preact/hooks'; import { useCallback, useState } from 'preact/hooks';
export default function Switch({ checked, id, onChange }) { export default function Switch({ checked, id, onChange, label, labelPosition = 'before' }) {
const [isFocused, setFocused] = useState(false); const [isFocused, setFocused] = useState(false);
const handleChange = useCallback( const handleChange = useCallback(
@ -24,15 +24,21 @@ export default function Switch({ checked, id, onChange }) {
return ( return (
<label <label
htmlFor={id} htmlFor={id}
className={`flex items-center justify-center ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`} className={`flex items-center space-x-4 w-full ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`}
> >
{label && labelPosition === 'before' ? (
<div data-testid={`${id}-label`} className="inline-flex flex-grow">
{label}
</div>
) : null}
<div <div
onMouseOver={handleFocus} onMouseOver={handleFocus}
onMouseOut={handleBlur} onMouseOut={handleBlur}
className={`w-8 h-5 relative ${!onChange ? 'opacity-60' : ''}`} className={`self-end w-8 h-5 relative ${!onChange ? 'opacity-60' : ''}`}
> >
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
<input <input
data-testid={`${id}-input`}
className="absolute left-48" className="absolute left-48"
onBlur={handleBlur} onBlur={handleBlur}
onFocus={handleFocus} onFocus={handleFocus}
@ -55,6 +61,11 @@ export default function Switch({ checked, id, onChange }) {
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'} style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
/> />
</div> </div>
{label && labelPosition !== 'before' ? (
<div data-testid={`${id}-label`} class="inline-flex flex-grow">
{label}
</div>
) : null}
</label> </label>
); );
} }

View File

@ -0,0 +1,47 @@
import { h } from 'preact';
import Switch from '../Switch';
import { fireEvent, render, screen } from '@testing-library/preact';
describe('Switch', () => {
test('renders a hidden checkbox', async () => {
render(
<div>
<Switch id="unchecked-switch" />
<Switch id="checked-switch" checked={true} />
</div>
);
const unchecked = screen.queryByTestId('unchecked-switch-input');
expect(unchecked).toHaveAttribute('type', 'checkbox');
expect(unchecked).not.toBeChecked();
const checked = screen.queryByTestId('checked-switch-input');
expect(checked).toHaveAttribute('type', 'checkbox');
expect(checked).toBeChecked();
});
test('calls onChange callback when checked/unchecked', async () => {
const handleChange = jest.fn();
const { rerender } = render(<Switch id="check" onChange={handleChange} />);
fireEvent.change(screen.queryByTestId('check-input'), { checked: true });
expect(handleChange).toHaveBeenCalledWith('check', true);
rerender(<Switch id="check" onChange={handleChange} checked />);
fireEvent.change(screen.queryByTestId('check-input'), { checked: false });
expect(handleChange).toHaveBeenCalledWith('check', false);
});
test('renders a label before', async () => {
render(<Switch id="check" label="This is the label" />);
const items = screen.queryAllByTestId(/check-.+/);
expect(items[0]).toHaveTextContent('This is the label');
expect(items[1]).toHaveAttribute('data-testid', 'check-input');
});
test('renders a label after', async () => {
render(<Switch id="check" label="This is the label" labelPosition="after" />);
const items = screen.queryAllByTestId(/check-.+/);
expect(items[0]).toHaveAttribute('data-testid', 'check-input');
expect(items[1]).toHaveTextContent('This is the label');
});
});

View File

@ -45,30 +45,36 @@ export default function Camera({ camera }) {
const optionContent = showSettings ? ( const optionContent = showSettings ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="flex space-x-3"> <Switch
<Switch checked={options['bbox']} id="bbox" onChange={handleSetOption} /> checked={options['bbox']}
<span className="inline-flex">Bounding box</span> id="bbox"
</div> onChange={handleSetOption}
<div className="flex space-x-3"> label="Bounding box"
<Switch checked={options['timestamp']} id="timestamp" onChange={handleSetOption} /> labelPosition="after"
<span className="inline-flex">Timestamp</span> />
</div> <Switch
<div className="flex space-x-3"> checked={options['timestamp']}
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} /> id="timestamp"
<span className="inline-flex">Zones</span> onChange={handleSetOption}
</div> label="Timestamp"
<div className="flex space-x-3"> labelPosition="after"
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} /> />
<span className="inline-flex">Masks</span> <Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
</div> <Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Masks" labelPosition="after" />
<div className="flex space-x-3"> <Switch
<Switch checked={options['motion']} id="motion" onChange={handleSetOption} /> checked={options['motion']}
<span className="inline-flex">Motion boxes</span> id="motion"
</div> onChange={handleSetOption}
<div className="flex space-x-3"> label="Motion boxes"
<Switch checked={options['regions']} id="regions" onChange={handleSetOption} /> labelPosition="after"
<span className="inline-flex">Regions</span> />
</div> <Switch
checked={options['regions']}
id="regions"
onChange={handleSetOption}
label="Regions"
labelPosition="after"
/>
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</div> </div>
) : null; ) : null;

View File

@ -29,8 +29,8 @@ export default function CameraMasks({ camera, url }) {
Array.isArray(motionMask) Array.isArray(motionMask)
? motionMask.map((mask) => getPolylinePoints(mask)) ? motionMask.map((mask) => getPolylinePoints(mask))
: motionMask : motionMask
? [getPolylinePoints(motionMask)] ? [getPolylinePoints(motionMask)]
: [] : []
); );
const [zonePoints, setZonePoints] = useState( const [zonePoints, setZonePoints] = useState(
@ -44,8 +44,8 @@ export default function CameraMasks({ camera, url }) {
[name]: Array.isArray(objectFilters[name].mask) [name]: Array.isArray(objectFilters[name].mask)
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask)) ? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
: objectFilters[name].mask : objectFilters[name].mask
? [getPolylinePoints(objectFilters[name].mask)] ? [getPolylinePoints(objectFilters[name].mask)]
: [], : [],
}), }),
{} {}
) )
@ -128,11 +128,11 @@ ${motionMaskPoints.map((mask, i) => ` - ${polylinePointsToPolyline(mask)}`)
const handleCopyZones = useCallback(async () => { const handleCopyZones = useCallback(async () => {
await window.navigator.clipboard.writeText(` zones: await window.navigator.clipboard.writeText(` zones:
${Object.keys(zonePoints) ${Object.keys(zonePoints)
.map( .map(
(zoneName) => ` ${zoneName}: (zoneName) => ` ${zoneName}:
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}` coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
) )
.join('\n')}`); .join('\n')}`);
}, [zonePoints]); }, [zonePoints]);
// Object methods // Object methods
@ -164,14 +164,14 @@ ${Object.keys(zonePoints)
await window.navigator.clipboard.writeText(` objects: await window.navigator.clipboard.writeText(` objects:
filters: filters:
${Object.keys(objectMaskPoints) ${Object.keys(objectMaskPoints)
.map((objectName) => .map((objectName) =>
objectMaskPoints[objectName].length objectMaskPoints[objectName].length
? ` ${objectName}: ? ` ${objectName}:
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}` mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
: '' : ''
) )
.filter(Boolean) .filter(Boolean)
.join('\n')}`); .join('\n')}`);
}, [objectMaskPoints]); }, [objectMaskPoints]);
const handleAddToObjectMask = useCallback( const handleAddToObjectMask = useCallback(
@ -222,8 +222,8 @@ ${Object.keys(objectMaskPoints)
height={height} height={height}
/> />
</div> </div>
<div className="flex space-x-4"> <div className="max-w-xs">
<span>Snap to edges</span> <Switch checked={snap} onChange={handleChangeSnap} /> <Switch checked={snap} label="Snap to edges" labelPosition="after" onChange={handleChangeSnap} />
</div> </div>
</div> </div>
@ -360,15 +360,15 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
{!scaledPoints {!scaledPoints
? null ? null
: scaledPoints.map(([x, y], i) => ( : scaledPoints.map(([x, y], i) => (
<PolyPoint <PolyPoint
boundingRef={boundingRef} boundingRef={boundingRef}
index={i} index={i}
onMove={handleMovePoint} onMove={handleMovePoint}
onRemove={handleRemovePoint} onRemove={handleRemovePoint}
x={x + MaskInset} x={x + MaskInset}
y={y + MaskInset} y={y + MaskInset}
/> />
))} ))}
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} /> <div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
<svg <svg
width="100%" width="100%"

View File

@ -9,7 +9,7 @@ import TextField from '../components/TextField';
import { useCallback, useState } from 'preact/hooks'; import { useCallback, useState } from 'preact/hooks';
export default function StyleGuide() { export default function StyleGuide() {
const [switches, setSwitches] = useState({ 0: false, 1: true }); const [switches, setSwitches] = useState({ 0: false, 1: true, 2: false, 3: false });
const handleSwitch = useCallback( const handleSwitch = useCallback(
(id, checked) => { (id, checked) => {
@ -53,23 +53,26 @@ export default function StyleGuide() {
</div> </div>
<Heading size="md">Switch</Heading> <Heading size="md">Switch</Heading>
<div className="flex"> <div className="flex-col space-y-4 max-w-4xl">
<div> <Switch label="Disabled, off" labelPosition="after" />
<p>Disabled, off</p> <Switch label="Disabled, on" labelPosition="after" checked />
<Switch /> <Switch
</div> label="Enabled, (off initial)"
<div> labelPosition="after"
<p>Disabled, on</p> checked={switches[0]}
<Switch checked /> id={0}
</div> onChange={handleSwitch}
<div> />
<p>Enabled, (off initial)</p> <Switch
<Switch checked={switches[0]} id={0} onChange={handleSwitch} label="Default" /> label="Enabled, (on initial)"
</div> labelPosition="after"
<div> checked={switches[1]}
<p>Enabled, (on initial)</p> id={1}
<Switch checked={switches[1]} id={1} onChange={handleSwitch} label="Default" /> onChange={handleSwitch}
</div> />
<Switch checked={switches[2]} id={2} label="Label before" onChange={handleSwitch} />
<Switch checked={switches[3]} id={3} label="Label after" labelPosition="after" onChange={handleSwitch} />
</div> </div>
<Heading size="md">Select</Heading> <Heading size="md">Select</Heading>