Skip to content

Commit

Permalink
feat: check control
Browse files Browse the repository at this point in the history
  • Loading branch information
dannyhw committed Feb 10, 2024
1 parent 92d1598 commit 84cba78
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 167 deletions.
45 changes: 45 additions & 0 deletions examples/expo-example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,48 @@
// https://github.com/facebook/hermes/issues/817
// NOTE this fixes metro not logging objects/arrays for some reason
if (__DEV__) {
const primitiveTypes = ['string', 'number', 'boolean'];
const logLevels = ['log', 'debug', 'info', 'warn', 'error'];

const transformArgs = (args) => {
return args.map((arg) => {
if (arg === undefined) {
return 'undefined';
}
if (arg instanceof Error) {
if (arg.stack) {
return arg.stack;
}
return arg.toString();
}
if (arg instanceof Date) {
return arg.toString();
}
if (primitiveTypes.includes(typeof arg)) {
return arg.toString();
} else {
return JSON.stringify(arg);
}
});
};

const consoleProxy = new Proxy(console, {
get: (target, prop) => {
//@ts-ignore
if (logLevels.includes(prop)) {
return (...args) => {
// we proxy the call to itself, but we transform the arguments to strings before
// so that they are printed in the terminal
return target[prop].apply(this, transformArgs(args));
};
}
return target[prop];
},
});

console = consoleProxy;
}

import { Text, View } from 'react-native';
import Constants from 'expo-constants';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { StoryObj, Meta } from '@storybook/react';
import { CheckExample } from './Check';

const meta: Meta<typeof CheckExample> = {
title: 'ControlExamples/Check',
component: CheckExample,
argTypes: {
rotationAxis: {
control: 'check',
options: ['x', 'y', 'z'],
},
},
};

export default meta;

type Story = StoryObj<typeof meta>;

export const XAxis: Story = {
args: {
rotationAxis: ['x'],
},
};

export const YAxis: Story = {
args: {
rotationAxis: ['y'],
},
};

export const ZAxis: Story = {
args: {
rotationAxis: ['z'],
},
};

export const XYZAxis: Story = {
args: {
rotationAxis: ['x', 'y', 'z'],
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { screen, render } from '@testing-library/react-native';
import { composeStory } from '@storybook/react';

import Meta, { XAxis, XYZAxis, YAxis, ZAxis } from './Check.stories';

const XAxisStory = composeStory(XAxis, Meta);
const YAxisStory = composeStory(YAxis, Meta);
const ZAxisStory = composeStory(ZAxis, Meta);
const XYZAxisStory = composeStory(XYZAxis, Meta);

test('x Axis story renders', () => {
render(<XAxisStory />);

screen.getByText('axis: x');
});

test('y Axis story renders', () => {
render(<YAxisStory />);

screen.getByText('axis: y');
});

test('z Axis story renders', () => {
render(<ZAxisStory />);

screen.getByText('axis: z');
});
test('xyz Axis story renders', () => {
render(<XYZAxisStory />);

screen.getByText('axis: x, y, z');
});
19 changes: 19 additions & 0 deletions examples/expo-example/components/ControlExamples/Check/Check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useEffect } from 'react';
import { Text } from 'react-native';

interface Props {
rotationAxis: Array<'x' | 'y' | 'z'>;
}

export const CheckExample = ({ rotationAxis }: Props) => {
useEffect(() => {
console.log(
'rotationAxis',
JSON.stringify(rotationAxis),
typeof rotationAxis,
Array.isArray(rotationAxis)
);
});

return <Text>axis: {rotationAxis.join(', ')}</Text>;
};
12 changes: 6 additions & 6 deletions examples/expo-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"test:ci": "jest --runInBand"
},
"dependencies": {
"@expo/metro-runtime": "^3.1.1",
"@expo/metro-runtime": "~3.1.3",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-native-community/datetimepicker": "7.6.1",
"@react-native-community/slider": "4.4.2",
Expand All @@ -34,8 +34,8 @@
"@storybook/addon-ondevice-controls": "^7.6.15",
"@storybook/addon-ondevice-knobs": "^7.6.15",
"@storybook/addon-ondevice-notes": "^7.6.15",
"@storybook/addon-react-native-server": "0.0.5--canary.3.18ef8d7.0",
"@storybook/addon-react-native-web": "^0.0.22",
"@storybook/addon-react-native-server": "^0.0.5",
"@storybook/addon-react-native-web": "^0.0.23",
"@storybook/blocks": "^7.6.13",
"@storybook/builder-webpack5": "^7.6.13",
"@storybook/core-common": "^7.6.13",
Expand All @@ -45,11 +45,11 @@
"@storybook/react-native": "^7.6.15",
"@storybook/react-webpack5": "^7.6.13",
"@storybook/test": "^7.6.13",
"expo": "^50.0.2",
"expo": "^50.0.6",
"querystring": "^0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.73.2",
"react-native": "0.73.4",
"react-native-safe-area-context": "4.8.2",
"react-native-web": "~0.19.6",
"storybook": "^7.6.13",
Expand All @@ -65,7 +65,7 @@
"@types/ws": "^8.5.10",
"babel-plugin-react-docgen-typescript": "^1.5.1",
"jest": "29.7.0",
"jest-expo": "50.0.1",
"jest-expo": "~50.0.2",
"metro-react-native-babel-preset": "^0.77.0",
"ts-node": "^10.9.1",
"typescript": "^5.3.3"
Expand Down
4 changes: 4 additions & 0 deletions packages/ondevice-controls/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
"react-native-modal-selector": "^2.1.1",
"tinycolor2": "^1.4.1"
},
"devDependencies": {
"cross-env": "^7.0.3",
"typescript": "^5.3.3"
},
"peerDependencies": {
"@react-native-community/datetimepicker": "*",
"@react-native-community/slider": "*",
Expand Down
136 changes: 136 additions & 0 deletions packages/ondevice-controls/src/types/Check.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { FC } from 'react';
import { useState, useEffect } from 'react';
import { styled } from '@storybook/react-native-theming';
import { logger } from '@storybook/client-logger';
import type { Conditional } from '@storybook/types';

export type OptionsObject = Record<string, any>;

export interface NormalizedOptionsConfig {
options: OptionsObject;
}

export type OptionsMultiSelection = any[];

export interface ArgType {
name?: string;
description?: string;
defaultValue?: any;
if?: Conditional;
[key: string]: any;
}

export interface ControlProps<T> {
name: string;
value?: T;
defaultValue?: T;
argType?: ArgType;
onChange: (value: T) => T | void;
onFocus?: (evt: any) => void;
onBlur?: (evt: any) => void;
}

const Wrapper = styled.View(({}) => ({}));

const Text = styled.Text({});

const Container = styled.TouchableOpacity<{ isLast: boolean }>(({ isLast, theme }) => ({
marginBottom: isLast ? 0 : theme.tokens.spacing2,
flexDirection: 'row',
alignItems: 'center',
}));

const Tick = styled.Image({});

const Box = styled.View<{ selected: boolean }>(({ theme, selected }) => ({
width: 20,
height: 20,
marginRight: theme.inputs.radio.labelSpacing,
alignItems: 'center',
justifyContent: 'center',
borderColor: theme.inputs.text.borderColor,
borderWidth: 1,
borderRadius: theme.tokens.borderRadius.small,
backgroundColor: selected ? theme.inputs.radio.activeBackgroundColor : 'transparent',
}));

export const selectedKeys = (value: any[], options: OptionsObject) =>
value && options
? Object.entries(options)
.filter((entry) => value?.includes?.(entry[1]))
.map((entry) => entry[0])
: [];

export const selectedValues = (keys: string[], options: OptionsObject) =>
keys && options && keys.map((key) => options[key]);

export const getControlId = (value: string) => `control-${value?.replace(/\s+/g, '-')}`;

export const CheckboxControl: FC<{
arg: {
name: string;
value: any;
options: Array<any> | Record<string, any>;
control: {
labels?: Record<string, string>;
};
};
onChange: (value: any) => void;
}> = ({ onChange, arg: { name, value, options } }) => {
const initial = selectedKeys(value, options);
const [selected, setSelected] = useState(initial);

const handleChange = (text) => {
const option = text;
const updated = [...selected];
if (updated.includes(option)) {
updated.splice(updated.indexOf(option), 1);
} else {
updated.push(option);
}
onChange(selectedValues(updated, options));
setSelected(updated);
};

useEffect(() => {
setSelected(selectedKeys(value, options));
}, [options, value]);

const controlId = getControlId(name);

if (!options) {
logger.warn(`Checkbox with no options: ${name}`);
return null;
}

return (
<Wrapper>
{Object.keys(options).map((key, index) => {
return (
<Container
key={`${controlId}-${index}`}
isLast={index === options.length - 1}
onPress={() => {
handleChange(key);
}}
>
<Box selected={selected?.includes(key)}>
<Tick
accessibilityLabel={key}
source={{
width: 14,
height: 10,
uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAUCAYAAACeXl35AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAB2SURBVHgB7dRLDoAgEAPQ3pTenCPMERSiC0NQYZyyskkTF5IXvoA+qdRK8/ktx7ZLDcK0WG2GKD2slhDkx9ZjX+/J9Mxy8+MM6lpG6wwYQd17xpuBSYF50LDTyAE0DBtBw7E3VILNoERwuBJ7QglxiOM1MgRgO5xnlTXeMlnGAAAAAElFTkSuQmCC',
}}
tintColor={selected?.includes(key) ? 'white' : 'transparent'}
/>
</Box>
<Text>{options[key]}</Text>
</Container>
);
})}
</Wrapper>
);
};

export default CheckboxControl;
2 changes: 2 additions & 0 deletions packages/ondevice-controls/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import SelectType from './Select';
import DateType from './Date';
import ArrayType from './Array';
import RadioType from './Radio';
import CheckboxType from './Check';

export default {
text: TextType,
Expand All @@ -18,4 +19,5 @@ export default {
date: DateType,
array: ArrayType,
radio: RadioType,
check: CheckboxType,
};
Loading

0 comments on commit 84cba78

Please sign in to comment.