Skip to content

Commit

Permalink
Add switch component
Browse files Browse the repository at this point in the history
  • Loading branch information
maltenzo authored and maurobender committed Oct 19, 2022
1 parent 65421b2 commit 886e97f
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as Radio } from './main/Radio';
export { default as FormControl } from './main/FormControl';
export { default as TextInput } from './main/TextInput';
export { default as Chip } from './main/Chip';
export { default as Switch } from './main/Switch';
2 changes: 2 additions & 0 deletions src/components/main/Switch/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as useSwitchPropsResolver } from './useSwitchPropsResolver';
export { default as useSwitchAnimation } from './useSwitchAnimation';
28 changes: 28 additions & 0 deletions src/components/main/Switch/hooks/useSwitchAccessibilityProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useMemo } from 'react';
import { Platform } from 'react-native';

interface IUseSwitchAccessibilityProps {
label?: string,
selected: boolean,
disabled: boolean
}

const useSwitchAccessibilityProps = ( {
label,
selected,
disabled
}: IUseSwitchAccessibilityProps ) => useMemo(
() => ( {
accessible: true,
accessibilityRole: 'switch',
accessibilityLabel: label,
accessibilityState: { checked: selected, disabled },
...( Platform.OS === 'web'
? { accessibilityChecked: selected }
: {} )

} ),
[ label, selected, disabled ]
);

export default useSwitchAccessibilityProps;
23 changes: 23 additions & 0 deletions src/components/main/Switch/hooks/useSwitchIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';

import UIKitIcon from '../../../../icons/UIKitIcon';
import Icon from '../../Icon';

const defaultOnIcon = <Icon as={UIKitIcon} name="checkmark-outline" />;
const defaultOffIcon = <Icon as={UIKitIcon} name="close-outline" />;

interface IUseSwitchIconProps {
on: boolean,
onIcon?: JSX.Element,
offIcon?: JSX.Element
}

const useSwitchIcon = ( {
on,
onIcon = defaultOnIcon,
offIcon = defaultOffIcon
}: IUseSwitchIconProps ) => (
on ? onIcon : offIcon
);

export default useSwitchIcon;
56 changes: 56 additions & 0 deletions src/components/main/Switch/hooks/useSwitchPropsResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useComponentPropsResolver } from '../../../../hooks';
import type { IBoxProps } from '../../Box/types';
import type { IIconProps } from '../../Icon/types';
import type { ISwitchProps } from '../types';
import useSwitchStateProps from '../useSwitchStateProps';
import useSwitchAccessibilityProps from './useSwitchAccessibilityProps';
import useSwitchIcon from './useSwitchIcon';

interface IUseSwitchPropsResolverProps extends Omit<ISwitchProps, 'initialValue' >{
selected: boolean;
disabled?: boolean;
}

interface IUseSwitchPropsResolverReturnType {
icon: JSX.Element,
iconProps?: Omit<IIconProps, 'name'>,
iconContainerProps?: Omit<IBoxProps, 'children'>,
switchContainerProps?: Omit<IBoxProps, 'children'>,
containerProps: Omit<IBoxProps, 'children'>
}

const useSwitchPropsResolver = (
props: IUseSwitchPropsResolverProps
): IUseSwitchPropsResolverReturnType => {
const { onIcon, offIcon } = props;
const { state, stateProps } = useSwitchStateProps( props );
const {
__switchContainer: switchContainerProps,
__iconContainer: iconContainerProps,
__icon: iconProps,
...containerProps
} = useComponentPropsResolver( 'Switch', props, state );

const icon = useSwitchIcon( {
on: state.isSelected,
onIcon,
offIcon
} );

const accessibilityProps = useSwitchAccessibilityProps( {
disabled: state.isDisabled,
selected: state.isSelected
} );

Object.assign( containerProps, stateProps, accessibilityProps );

return {
icon,
containerProps,
iconContainerProps,
iconProps,
switchContainerProps
};
};

export default useSwitchPropsResolver;
60 changes: 60 additions & 0 deletions src/components/main/Switch/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, {
cloneElement, useCallback, useState
} from 'react';
import Pressable from '../Pressable';
import { useSwitchPropsResolver, useSwitchAnimation } from './hooks';
import type { ISwitchProps } from './types';
import Box from '../Box';

const Switch = ( {
initialValue = false,
withIcon = true,
animationDuration,
onChange,
testID,
...props
}: ISwitchProps ) => {
const [ isOn, setIsOn ] = useState( initialValue );

const {
icon, iconProps, iconContainerProps, containerProps, switchContainerProps
} = useSwitchPropsResolver( {
selected: isOn,
...props
} );

const { position, startAnimation, reverseAnimation } = useSwitchAnimation( {
containerProps,
iconContainerProps: iconContainerProps || {},
animationDuration,
animatedValue: initialValue ? 1 : 0
} );

const toggle = useCallback( () => {
setIsOn( ( wasOn ) => {
if ( wasOn ) {
reverseAnimation();
} else {
startAnimation();
}
onChange?.( !wasOn );
return !wasOn;
} );
}, [ startAnimation, reverseAnimation, onChange ] );

return (
<Box {...switchContainerProps}>
<Pressable {...containerProps} onPress={toggle} testID={testID}>
<Box.Animated {...iconContainerProps} style={{ marginLeft: position }}>
{withIcon && cloneElement( icon, {
...iconProps,
testID: testID && `${testID}-icon`
} )}
</Box.Animated>
</Pressable>
</Box>

);
};

export default Switch;
16 changes: 16 additions & 0 deletions src/components/main/Switch/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { IPressableProps } from '../Pressable/types';

export interface ISwitchProps extends Omit<IPressableProps, 'children'>{
initialValue?: boolean,
disabled?: boolean,

onIcon?: JSX.Element,
offIcon?: JSX.Element,

animationDuration?: number,

withIcon?: boolean,

onChange?: ( value: boolean ) => void,

}
62 changes: 62 additions & 0 deletions src/components/main/Switch/useSwitchStateProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useMemo } from 'react';
import type { PressableProps } from 'react-native';
import { useIsFocused, useIsHovered, useIsPressed } from '../../hooks';
import type { IHoverableComponent } from '../../../core/components/types';

interface IUseSwitchStateProps {
value?: string,
disabled?: boolean,
selected?: boolean,
onPress?: PressableProps[ 'onPress' ],
onPressIn?: PressableProps[ 'onPressIn' ],
onPressOut?: PressableProps[ 'onPressOut' ],
onHoverIn?: IHoverableComponent[ 'onHoverIn' ],
onHoverOut?: IHoverableComponent[ 'onHoverOut' ],
onFocus?: PressableProps[ 'onFocus' ],
onBlur?: PressableProps[ 'onBlur' ]
}

const useSwitchStateProps = ( {
disabled: isDisabled,
selected: isSelected,
onPress: onPressProp,
...props
}: IUseSwitchStateProps ) => {
const { isPressed, onPressIn, onPressOut } = useIsPressed( props );
const { isHovered, onHoverIn, onHoverOut } = useIsHovered( props );
const { isFocused, onFocus, onBlur } = useIsFocused( props );

return useMemo(
() => {
const onPress = onPressProp;

const state = {
isSelected: !!isSelected,
isDisabled: !!isDisabled,
isPressed,
isHovered,
isFocused
};

const stateProps = {
selected: !!isSelected,
disabled: !!isDisabled,
onPress,
onPressIn,
onPressOut,
onHoverIn,
onHoverOut,
onFocus,
onBlur
};

return { state, stateProps };
},
[
isSelected, isDisabled, isPressed, isHovered, isFocused,
onPressProp, onPressIn, onPressOut, onHoverIn, onHoverOut, onFocus, onBlur
]
);
};

export default useSwitchStateProps;
2 changes: 1 addition & 1 deletion src/core/components/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StyledProps } from '../../theme/types';

export type ComponentName = 'Text' | 'Box' | 'Stack' | 'Button' | 'Pressable' | 'Icon' | 'Checkbox'
| 'IconButton' | 'Radio' | 'FormControl' | 'TextInput' | 'Chip';
| 'IconButton' | 'Radio' | 'FormControl' | 'TextInput' | 'Chip' | 'Switch';

export type VariantName = string;

Expand Down
13 changes: 10 additions & 3 deletions src/core/components/types/pseudoProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ interface ITextInputPseudoProps {
__icon: ComponentBaseStyledProps<'Icon'>
__textInput: ComponentBaseStyledProps<'Box'>
}
// Switch pseudoprops
interface ISwitchPseudoProps {
__icon: ComponentBaseStyledProps<'Icon'>,
__iconContainer: ComponentBaseStyledProps<'Box'>,
__switchContainer: ComponentBaseStyledProps<'Box'>,
}

// CHIP pseudoprops
interface IChipPseudoProps {
Expand All @@ -71,13 +77,14 @@ interface ComponentsPseudoPropsConfig {
Radio: IRadioPseudoProps
FormControl: IFormControlPseudoProps,
TextInput: ITextInputPseudoProps,
Chip: IChipPseudoProps
Chip: IChipPseudoProps,
Switch: ISwitchPseudoProps
}

// Template type to get pseudoprops for a given component
/* eslint-disable @typescript-eslint/ban-types */
export type ComponentPseudoProps<C extends ComponentName> =
C extends keyof ComponentsPseudoPropsConfig
? ComponentsPseudoPropsConfig[C]
: {};
? ComponentsPseudoPropsConfig[C]
: {};
/* eslint-enable @typescript-eslint/ban-types */
63 changes: 63 additions & 0 deletions src/core/theme/defaultTheme/components/Switch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export default {
defaultProps: {
width: 52,
height: 32,
padding: '0.5',
borderWidth: 'sm',
borderColor: 'neutral.600',
backgroundColor: 'secondary.50',
rounded: '4xl',
justifyContent: 'center',
alignItems: 'flex-start',
__switchContainer: {
width: 60,
height: 40,
rounded: '4xl',
justifyContent: 'center',
alignItems: 'center'
},
__iconContainer: {
width: 24,
height: 24,
rounded: 'full',
backgroundColor: 'neutral.600',
justifyContent: 'center',
alignItems: 'center'
},
__icon: {
size: '3xs',
color: 'white'
},
__selected: {
backgroundColor: 'secondary.900',
borderColor: 'transparent',
__iconContainer: {
backgroundColor: 'white'
},
__icon: {
color: 'secondary.900'
}
},
__disabled: {
__icon: {
color: 'white'
},
backgroundColor: 'neutral.200',
borderColor: 'neutral.600',
opacity: 0.5,
__iconContainer: {
backgroundColor: 'neutral.600'
}
},
__hovered: {
__switchContainer: {
backgroundColor: 'secondary.50'
}
},
__pressed: {
__switchContainer: {
backgroundColor: 'secondary.50'
}
}
}
};
4 changes: 3 additions & 1 deletion src/core/theme/defaultTheme/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Radio from './Radio';
import FormControl from './FormControl';
import TextInput from './TextInput';
import Chip from './Chip';
import Switch from './Switch';

export default {
Text,
Expand All @@ -19,5 +20,6 @@ export default {
Radio,
FormControl,
TextInput,
Chip
Chip,
Switch
};
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export {
Radio,
FormControl,
TextInput,
Chip
Chip,
Switch
} from './components';
export { UIKitIcon };
Loading

0 comments on commit 886e97f

Please sign in to comment.