Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: enforce number data type for number input (#291) #315

Merged
merged 8 commits into from
Feb 19, 2022
Merged
23 changes: 20 additions & 3 deletions addons/ondevice-controls/src/ControlsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import styled from '@emotion/native';
import { API } from '@storybook/api';
import React from 'react';
import React, { useState, useCallback } from 'react';
import { Args } from '@storybook/addons';

import { useArgs } from './hooks';
import NoControlsWarning from './NoControlsWarning';
import PropForm from './PropForm';
Expand Down Expand Up @@ -46,8 +48,10 @@ export interface ArgTypes {
const ControlsPanel = ({ api }: { api: API }) => {
const store = api.store();
const storyId = store.getSelection().storyId;
const [isPristine, setIsPristine] = useState(true);
const [argsfromHook, updateArgs, resetArgs] = useArgs(storyId, store);
const { argTypes, parameters } = store.fromId(storyId);

const argsObject = Object.entries(argsfromHook).reduce((prev, [name, value]) => {
const isControl = Boolean(argTypes?.[name]?.control);

Expand All @@ -62,14 +66,27 @@ const ControlsPanel = ({ api }: { api: API }) => {
const isArgsStory = parameters.__isArgsStory;
const showWarning = !(hasControls && isArgsStory);

const updateArgsOnFieldChange = useCallback(
(args: Args) => {
updateArgs(args);
setIsPristine(false);
},
[updateArgs]
);

const handleReset = () => {
resetArgs();
setIsPristine(true);
};

if (showWarning) {
return <NoControlsWarning />;
}

return (
<>
<PropForm args={argsObject} onFieldChange={updateArgs} />
<Touchable onPress={() => resetArgs()}>
<PropForm args={argsObject} isPristine={isPristine} onFieldChange={updateArgsOnFieldChange} />
<Touchable onPress={handleReset}>
<ResetButton>RESET</ResetButton>
</Touchable>
</>
Expand Down
9 changes: 7 additions & 2 deletions addons/ondevice-controls/src/PropField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ const Label = styled.Text(({ theme }) => ({
interface PropFieldProps {
onChange: (value: any) => void;
arg: ArgType;
isPristine: boolean;
}

const PropField = ({ onChange, arg }: PropFieldProps) => {
const PropField = ({ onChange, arg, isPristine }: PropFieldProps) => {
const InputType: ComponentType<any> = TypeMap[arg.type];
return (
<View>
{!arg.hideLabel ? <Label>{`${arg.label || arg.name}`}</Label> : null}
{InputType ? <InputType arg={arg} onChange={onChange} /> : <InvalidType arg={arg} />}
{InputType ? (
<InputType arg={arg} isPristine={isPristine} onChange={onChange} />
) : (
<InvalidType arg={arg} />
)}
</View>
);
};
Expand Down
7 changes: 5 additions & 2 deletions addons/ondevice-controls/src/PropForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import PropField from './PropField';

interface FormProps {
args: ArgTypes;
isPristine: boolean;
onFieldChange: (value: any) => void;
}

const PropForm = ({ args, onFieldChange }: FormProps) => {
const PropForm = ({ args, isPristine, onFieldChange }: FormProps) => {
const makeChangeHandler = (name: string) => {
return (value) => {
onFieldChange({ [name]: value });
Expand All @@ -19,7 +20,9 @@ const PropForm = ({ args, onFieldChange }: FormProps) => {
<View>
{Object.values(args).map((arg) => {
const changeHandler = makeChangeHandler(arg.name);
return <PropField key={arg.name} arg={arg} onChange={changeHandler} />;
return (
<PropField key={arg.name} arg={arg} isPristine={isPristine} onChange={changeHandler} />
);
})}
</View>
);
Expand Down
35 changes: 27 additions & 8 deletions addons/ondevice-controls/src/types/Number.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import Slider from '@react-native-community/slider';
import styled from '@emotion/native';
Expand All @@ -15,29 +15,48 @@ const Input = styled.TextInput(({ theme }) => ({
export interface NumberProps {
arg: {
name: string;
value: any;
value: number;
step: number;
min: number;
max: number;
range: boolean;
defaultValue: number;
};
isPristine: boolean;

onChange: (value: any) => void;
onChange: (value: number) => void;
}

const NumberType = ({ arg, onChange = (value) => value }: NumberProps) => {
const allowComma = typeof arg.value === 'string' ? arg.value.trim().replace(/,/, '.') : arg.value;
const showError = Number.isNaN(Number(allowComma));
const NumberType = ({ arg, isPristine, onChange = (value) => value }: NumberProps) => {
const showError = Number.isNaN(arg.value);
const [numStr, setNumStr] = useState(arg.value.toString());

const handleNormalChangeText = (text: string) => {
const commaReplaced = text.trim().replace(/,/, '.');

setNumStr(commaReplaced);
if (commaReplaced === '-') {
onChange(-1);
} else {
onChange(Number(commaReplaced));
}
};

// handle arg.value and numStr out of sync issue on reset
useEffect(() => {
if (isPristine) {
setNumStr(arg.value.toString());
}
}, [isPristine, arg.value]);

const renderNormal = () => {
return (
<Input
autoCapitalize="none"
underlineColorAndroid="transparent"
value={arg.value.toString()}
value={numStr}
keyboardType="numeric"
onChangeText={onChange}
onChangeText={handleNormalChangeText}
style={showError && styles.errorBorder}
/>
);
Expand Down
4 changes: 2 additions & 2 deletions examples/native/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -531,11 +531,11 @@ SPEC CHECKSUMS:
Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541
FlipperKit: d8d346844eca5d9120c17d441a2f38596e8ed2b9
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 5337263514dd6f09803962437687240c5dc39aa4
glog: 85ecdd10ee8d8ec362ef519a6a45ff9aa27b2e85
hermes-engine: bf7577d12ac6ccf53ab8b5af3c6ccf0dd8458c5c
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
OpenSSL-Universal: 1aa4f6a6ee7256b83db99ec1ccdaa80d10f9af9b
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
RCT-Folly: 803a9cfd78114b2ec0f140cfa6fa2a6bafb2d685
RCTRequired: e5dc0c44cb366fc93383a2bffbc190fe821e7293
RCTTypeSafety: 6a4d0cfe070e7fd996e797f439b70878764a1ae0
React: e194f6b2f0a4f8d24065f3ca0a6abe859694df65
Expand Down