diff --git a/apps/docs/pages/docs/v2/Components/icon.en-US.mdx b/apps/docs/pages/docs/v2/Components/icon.en-US.mdx new file mode 100644 index 00000000..67bb922b --- /dev/null +++ b/apps/docs/pages/docs/v2/Components/icon.en-US.mdx @@ -0,0 +1,78 @@ +--- +searchable: true +--- + +import { CodeEditor } from '@components/code-editor'; +import PropsTable from "@components/docs/props-table"; + +# Icon + +Component to display icon, it's a wrapper around the `Icon` component from `react-native-vector-icons` library. + +## Import + +```js +import { Icon } from "@ficus-ui/native"; +``` + +## Usage + +### Examples + + + + + + + + + + +`} /> + +### Custom style + +`} /> + +## Props + +Extends every `Box` + +### `name` + + +### `color` + + +### `size` + + +### `iconSet` + diff --git a/apps/examples/app/components-v2/Icon.tsx b/apps/examples/app/components-v2/Icon.tsx new file mode 100644 index 00000000..458ed5a6 --- /dev/null +++ b/apps/examples/app/components-v2/Icon.tsx @@ -0,0 +1,49 @@ +import ExampleSection from "@/src/ExampleSection"; +import { HStack, Icon, SafeAreaBox, ScrollBox, Text } from "@ficus-ui/native"; + + +const IconComponent = () => { + return ( + + + Icon component + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default IconComponent; diff --git a/apps/examples/app/items-v2.ts b/apps/examples/app/items-v2.ts index 5b95bd0f..6d4eae6d 100644 --- a/apps/examples/app/items-v2.ts +++ b/apps/examples/app/items-v2.ts @@ -15,6 +15,7 @@ import PressableComponent from './components-v2/Pressable'; import DividerComponent from '@/app/components-v2/Divider'; import SpinnerComponent from '@/app/components-v2/Spinner'; import SliderComponent from '@/app/components-v2/Slider'; +import IconComponent from '@/app/components-v2/Icon'; type ExampleComponentType = { onScreenName: string; @@ -39,4 +40,5 @@ export const components: ExampleComponentType[] = [ { navigationPath: 'Spinner', onScreenName: 'Spinner', component: SpinnerComponent }, { navigationPath: 'Pressable', onScreenName: 'Pressable', component: PressableComponent }, { navigationPath: 'Slider', onScreenName: 'Slider', component: SliderComponent }, + { navigationPath: 'Icon', onScreenName: 'Icon', component: IconComponent }, ]; diff --git a/packages/components/src/icon/icon.service.tsx b/packages/components/src/icon/icon.service.tsx new file mode 100644 index 00000000..429dddb8 --- /dev/null +++ b/packages/components/src/icon/icon.service.tsx @@ -0,0 +1,37 @@ +import IconAntDesign from 'react-native-vector-icons/AntDesign'; +import IconEntypo from 'react-native-vector-icons/Entypo'; +import IconEvilIcons from 'react-native-vector-icons/EvilIcons'; +import IconFeather from 'react-native-vector-icons/Feather'; +import IconFontAwesome from 'react-native-vector-icons/FontAwesome'; +import IconFontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import IconFontisto from 'react-native-vector-icons/Fontisto'; +import IconFoundation from 'react-native-vector-icons/Foundation'; +import IconIonicons from 'react-native-vector-icons/Ionicons'; +import IconMaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import IconMaterialIcons from 'react-native-vector-icons/MaterialIcons'; +import IconOcticons from 'react-native-vector-icons/Octicons'; +import IconSimpleLineIcons from 'react-native-vector-icons/SimpleLineIcons'; +import IconZocial from 'react-native-vector-icons/Zocial'; + +const iconSetMap = { + AntDesign: IconAntDesign, + Entypo: IconEntypo, + EvilIcons: IconEvilIcons, + Feather: IconFeather, + FontAwesome: IconFontAwesome, + FontAwesome5: IconFontAwesome5, + Foundation: IconFoundation, + Ionicons: IconIonicons, + MaterialIcons: IconMaterialIcons, + MaterialCommunityIcons: IconMaterialCommunityIcons, + Octicons: IconOcticons, + Zocial: IconZocial, + Fontisto: IconFontisto, + SimpleLineIcons: IconSimpleLineIcons, +} as const; + +export type IconSet = keyof typeof iconSetMap; + +export const getIconSet = (iconSet: IconSet = 'Ionicons') => { + return iconSetMap[iconSet]; +}; diff --git a/packages/components/src/icon/icon.spec.tsx b/packages/components/src/icon/icon.spec.tsx new file mode 100644 index 00000000..d203870e --- /dev/null +++ b/packages/components/src/icon/icon.spec.tsx @@ -0,0 +1,63 @@ +import { theme } from '@ficus-ui/theme'; +import { renderWithTheme as render } from '@test-utils'; + +import { Icon } from '.'; +import { getIconSet } from './icon.service'; + +jest.mock('./icon.service', () => ({ + getIconSet: jest.fn(() => jest.fn(() => null)), // Mock the icon component +})); + +describe('Icon component', () => { + it('should render default icon with no props', () => { + const { getByTestId } = render(); + + expect(getIconSet).toHaveBeenCalled(); + expect(getByTestId('icon-no-props')).toBeTruthy(); + }); + + it('should apply color from theme', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId('icon-color').props.children.props.color).toBe( + theme.colors.blue[500] + ); + }); + + it('should apply font size from theme', () => { + const { getByTestId } = render( + + ); + expect(getByTestId('icon-size').props.children.props.size).toBe( + theme.fontSizes['6xl'] + ); + }); + + it('should use the correct icon set based on fontFamily', () => { + render(); + + expect(getIconSet).toHaveBeenCalledWith('Feather'); + }); + + it('should pass additional props correctly', () => { + const { getByTestId } = render( + + ); + + expect(getByTestId('icon-custom').props.accessibilityLabel).toBe( + 'Settings Icon' + ); + }); +}); diff --git a/packages/components/src/icon/index.tsx b/packages/components/src/icon/index.tsx new file mode 100644 index 00000000..576f1f97 --- /dev/null +++ b/packages/components/src/icon/index.tsx @@ -0,0 +1,27 @@ +import { getColor, getProperty, theme } from '@ficus-ui/theme'; + +import { type NativeFicusProps, ficus, forwardRef } from '../system'; +import { type IconSet, getIconSet } from './icon.service'; + +interface IconOptions { + name: string; + color?: string; + size?: string; + iconSet?: IconSet; +} + +export interface IconProps extends NativeFicusProps<'View'>, IconOptions {} + +export const Icon = forwardRef(function Badge(props, ref) { + const { name, iconSet, color = 'gray.800', size, ...rest } = props; + const IconComponent = getIconSet(iconSet); + return ( + + + + ); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 1dc98090..c9a1a19e 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -12,5 +12,6 @@ export * from './pressable'; export * from './divider'; export * from './spinner'; export * from './slider'; +export * from './icon'; export { ThemeProvider } from '@ficus-ui/theme'; diff --git a/packages/components/src/slider/slider.spec.tsx b/packages/components/src/slider/slider.spec.tsx index da692f51..6a6b6a32 100644 --- a/packages/components/src/slider/slider.spec.tsx +++ b/packages/components/src/slider/slider.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { fireEvent, render } from '@testing-library/react-native'; +import { renderWithTheme as render } from '@test-utils'; +import { fireEvent } from '@testing-library/react-native'; import { Slider } from '.'; diff --git a/packages/theme/src/utilities/index.ts b/packages/theme/src/utilities/index.ts deleted file mode 100644 index 83c49128..00000000 --- a/packages/theme/src/utilities/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react'; -import { useEffect, useRef, useState } from 'react'; - -import { Dimensions } from 'react-native'; -import { - validateHTMLColorHex, - validateHTMLColorHsl, - validateHTMLColorName, - validateHTMLColorRgb, - validateHTMLColorSpecialName, -} from 'validate-color'; - -const WINDOW = Dimensions.get('window'); - -export const WINDOW_WIDTH = WINDOW.width; -export const WINDOW_HEIGHT = WINDOW.height; - -//is the value an empty array? -export const isEmptyArray = (value?: any) => - Array.isArray(value) && value.length === 0; - -// is the given object a Function? -export const isFunction = (obj: any): obj is Function => - typeof obj === 'function'; - -// is the given object an Object? -export const isObject = (obj: any): obj is Object => - obj !== null && typeof obj === 'object'; - -// is the given object an integer? -export const isInteger = (obj: any): boolean => - String(Math.floor(Number(obj))) === obj; - -// is the given object a string? -export const isString = (obj: any): obj is string => - Object.prototype.toString.call(obj) === '[object String]'; - -// is the given object a NaN? -// eslint-disable-next-line no-self-compare -export const isNaN = (obj: any): boolean => obj !== obj; - -// Does a React component have exactly 0 children? -export const isEmptyChildren = (children: any): boolean => - React.Children.count(children) === 0; - -// is the given object/value a promise? -export const isPromise = (value: any): value is PromiseLike => - isObject(value) && isFunction(value.then); - -// is the given object/value a type of synthetic event? -export const isInputEvent = (value: any): value is React.SyntheticEvent => - value && isObject(value) && isObject(value.target); - -/** - * useState with callback - * - * @param initialState - */ -export const useStateCallback = (initialState: any) => { - const [state, setState] = useState(initialState); - const cbRef = useRef(null); // mutable ref to store current callback - - const setStateCallback = (newState: any, cb: any) => { - cbRef.current = cb; // store passed callback to ref - setState(newState); - }; - - useEffect(() => { - // cb.current is `null` on initial render, so we only execute cb on state *updates* - if (cbRef.current) { - //@ts-ignore - cbRef.current(state); - cbRef.current = null; // reset callback after execution - } - }, [state]); - - return [state, setStateCallback]; -}; - -export const isValidColor = (color: string): boolean => { - return ( - validateHTMLColorRgb(color) || - validateHTMLColorSpecialName(color) || - validateHTMLColorHex(color) || - validateHTMLColorHsl(color) || - validateHTMLColorName(color) - ); -}; - -export const getSpecificProps = (obj: T, ...keys: string[]) => - //@ts-ignore - keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {}); - -export const removeSpecificProps = ( - obj: T, - ...keys: string[] -): T => - keys.reduce((a, c) => { - //@ts-ignore - delete a[c]; - return a; - }, obj); diff --git a/packages/theme/src/utils/index.ts b/packages/theme/src/utils/index.ts index 64d25856..bbbb4202 100644 --- a/packages/theme/src/utils/index.ts +++ b/packages/theme/src/utils/index.ts @@ -1 +1,12 @@ +export function getProperty( + value: any, + themeScope: Record | undefined +) { + if (themeScope?.[value]) { + return themeScope[value]; + } + + return value; +} + export * from './color';