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

[Implement] : FAB #67

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
127 changes: 127 additions & 0 deletions main/FAB/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {Animated, Easing, StyleProp, View, ViewStyle} from 'react-native';
import {ButtonSize, IconButton} from '../IconButton';
import {DoobooTheme, withTheme} from '../theme';
import {Icon, IconName} from '../Icon';
import React, {ReactElement, useLayoutEffect, useMemo, useRef} from 'react';

import styled from '@emotion/native';

export const StyledIcon = styled(Icon)`
color: ${({theme}) => theme.textContrast};
`;
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
export interface FABItem {
icon: IconName;
id: string;
}

export interface FABProps<Item extends FABItem> {
isActive: boolean;
fabItems: Item[];
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
onPressFAB: () => void;
onPressFabItem: (item?: Item) => void;
renderFAB?: () => ReactElement;
Copy link
Contributor

@yujonglee yujonglee Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderProps is not for providing ReactElement. It is for reusing render logic.

Like you did, Items - renderItem is proper use case here. Because it is providing render logic for each item. But If you are just providing ReactElement, children or slot pattern is for that use.

Additionally, it is better for performance too. Because ReactElement is always newly created for each render if you call () => ReactElement function. (Although It has almost no impact here)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to provide consistency on the methods on providing custom component.
Now in this component renderFABItem should be provided as renderProps so.. I think it is better to provide renderFAB as renderProps.

renderFabItem?: (item: Item, idx: number) => ReactElement;
size: ButtonSize;
style?: StyleProp<ViewStyle>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can have additional styles prop?
One for Animated.View for main icon and one for items.

}

function FloatingActionButtons<Item extends FABItem = FABItem>({
isActive,
fabItems,
onPressFAB,
onPressFabItem,
renderFAB,
renderFabItem,
size = 'large',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although they might have same size in general, I think FABItem is responsible for setting size, not FloatingActionButtons component.

style,
}: FABProps<Item> & {
theme: DoobooTheme;
}): ReactElement {
const spinValue = useRef(new Animated.Value(0));
const positionValue = useRef(new Animated.Value(0));

useLayoutEffect(() => {
Animated.timing(spinValue.current, {
toValue: isActive ? 1 : 0,
duration: 300,
easing: Easing.linear,
useNativeDriver: true,
}).start();
}, [isActive]);

useLayoutEffect(() => {
Animated.timing(positionValue.current, {
toValue: isActive ? 1 : 0,
duration: 300,
easing: Easing.linear,
useNativeDriver: true,
}).start();
}, [isActive]);
yujonglee marked this conversation as resolved.
Show resolved Hide resolved

const spin = spinValue.current.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '45deg'],
});
yujonglee marked this conversation as resolved.
Show resolved Hide resolved

const offsetAnimationList = useMemo(
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
() =>
fabItems?.map((_, idx) => {
return positionValue.current.interpolate({
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
inputRange: [0, 1],
outputRange: ['0%', `-${(idx + 1) * 80}%`],
});
}),
[fabItems],
);

return (
<View
style={[
{
position: 'absolute',
right: 10,
bottom: 10,
zIndex: 999,
flex: 1,
},
style,
]}>
{fabItems.map((item, idx) => {
return (
<Animated.View
key={item.id}
style={{
margin: 10,
position: 'absolute',
transform: [{translateY: offsetAnimationList[idx]}],
}}>
{renderFabItem ? (
renderFabItem(item, idx)
) : (
<IconButton
testID={item.id}
size={size}
icon={<StyledIcon size={24} name={item.icon} />}
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
onPress={() => onPressFabItem(item)}
/>
)}
</Animated.View>
);
})}
<Animated.View style={{transform: [{rotate: spin}], margin: 10}}>
{renderFAB ? (
renderFAB()
) : (
<IconButton
testID={'main_fab'}
size={size}
icon={<StyledIcon size={24} name="add-light" />}
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
onPress={onPressFAB}
/>
)}
</Animated.View>
</View>
);
}

export const FAB = withTheme(FloatingActionButtons);
3 changes: 2 additions & 1 deletion main/IconButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ type Styles = {
};

type ButtonType = 'primary' | 'secondary' | 'danger' | 'warning' | 'info';
type ButtonSize = 'small' | 'medium' | 'large';

export type ButtonSize = 'small' | 'medium' | 'large';

const ButtonContainer = styled(ButtonWrapper)<{
type: ButtonType;
Expand Down
54 changes: 54 additions & 0 deletions main/__tests__/FAB.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {FAB, FABItem, FABProps} from '../../main';
import React, {ReactElement} from 'react';
import {RenderAPI, fireEvent, render} from '@testing-library/react-native';

import {IconButton} from '../IconButton';
import {View} from 'react-native';
import {createComponent} from '../../test/testUtils';

const Component = (props: FABProps<FABItem>): ReactElement =>
createComponent(<FAB {...props} />);

describe('[FAB]', () => {
it('should render', async () => {
let count = 0;
let item: FABItem = {icon: 'bell-solid', id: 'item1'};
let resItem: FABItem;

const {getByTestId} = render(
Component({
fabItems: [item],
isActive: true,
size: 'large',
onPressFabItem: (item) => {
count += 1;
resItem = item;
},
onPressFAB: () => {},
}),
);

expect(count).toBe(0);
fireEvent.press(getByTestId('item1'));
expect(count).toBe(1);
expect(resItem.id).toBe(item.id);
});

it('should render customFAB', async () => {
const testingLib = render(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pattern from enzyme, which is not good practice for testing library.

I recommend writing like this.

const {getByTestId} = render(...);

fireEvent.press(getByTestId('item1'));

or

render(...);

fireEvent.press(screen.getByTestId('item1'));

Although later one is new feature and recommended by author, I rarely use it. We can talk about it if you want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See this for detailed guide for using testing-library.

Component({
fabItems: [{icon: 'bell-solid', id: 'item1'}],
isActive: true,
size: 'large',
onPressFabItem: (item1) => {},
onPressFAB: () => {},
renderFAB: () => <View />,
renderFabItem: (item, idx) => <View />,
}),
);

const json = testingLib.toJSON();

expect(json).toBeTruthy();
});
});
1 change: 1 addition & 0 deletions main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './StatusBarBrightness';
export * from './SwitchToggle';
export * from './theme';
export * from './SelectBox';
export * from './FAB';
44 changes: 44 additions & 0 deletions stories/dooboo-ui/FloatingActionButton/FABStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, {FC, useState} from 'react';
import {SafeAreaView, View} from 'react-native';

import {FAB} from '../../../main';
import styled from '@emotion/native';
import {useFonts} from 'expo-font';
import {withTheme} from '../../../main/theme/ThemeProvider';

const StoryContainer = styled.View`
flex: 1;
align-self: stretch;
background-color: ${({theme}) => theme.background};
`;

const FABContainer: FC = () => {
const [active, setActive] = useState<boolean>(false);

const [fontsLoaded] = useFonts({
IcoMoon: require('../../../main/Icon/doobooui.ttf'),
});

if (!fontsLoaded) return <View />;

return (
<StoryContainer>
yujonglee marked this conversation as resolved.
Show resolved Hide resolved
<SafeAreaView style={{display: 'flex', width: '100%', height: '100%'}}>
<FAB
size="medium"
isActive={active}
onPressFAB={() => setActive((prev) => !prev)}
fabItems={[
{id: 'search', icon: 'home-light'},
{id: 'like', icon: 'like-light'},
]}
onPressFabItem={(item) => {
console.log(item);
}}
/>
</SafeAreaView>
</StoryContainer>
);
};

export const FABStory = withTheme(FABContainer);
35 changes: 35 additions & 0 deletions stories/dooboo-ui/FloatingActionButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, {ReactElement} from 'react';

import {ContainerDeco} from '../../../storybook/decorators';
import {FABStory} from './FABStory';
import {ThemeProvider} from '../../../main/theme';
import {storiesOf} from '@storybook/react-native';

/**
* Below are stories for web
*/
export default {
title: 'FAB',
};

export const toStorybook = (): ReactElement => <FABStory />;

toStorybook.story = {
name: 'FAB',
};

/**
* Below are stories for app
*/
storiesOf('FAB', module)
.addDecorator(ContainerDeco)
.add('FAB - light', () => (
<ThemeProvider initialThemeType="light">
<FABStory />
</ThemeProvider>
))
.add('FAB - dark', () => (
<ThemeProvider initialThemeType="dark">
<FABStory />
</ThemeProvider>
));
1 change: 1 addition & 0 deletions stories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ import './dooboo-ui/SwitchToggleStories';
import './dooboo-ui/AccordionStories';
import './packages/CalendarCarouselStories';
import './packages/PinzoomStories';
import './dooboo-ui/FloatingActionButton';