From 66ffe8e63cc97743ef2a3ece1b598ce6c26ab1e7 Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Fri, 23 Jun 2023 11:35:59 +0300 Subject: [PATCH 1/7] chore: remove advanced, siplify intro --- docs/docs/introduction.mdx | 207 ++++-- docs/docs/tutorials/advanced/app.md | 295 -------- docs/docs/tutorials/advanced/bootstrap.mdx | 78 --- docs/docs/tutorials/advanced/navigation.md | 431 ------------ docs/docs/tutorials/advanced/screens.md | 457 ------------ docs/docs/tutorials/advanced/ui.md | 655 ------------------ .../basics/{quickstart.mdx => example.mdx} | 4 +- docs/sidebars.js | 16 +- docs/src/components/Features.tsx | 7 - 9 files changed, 164 insertions(+), 1986 deletions(-) delete mode 100644 docs/docs/tutorials/advanced/app.md delete mode 100644 docs/docs/tutorials/advanced/bootstrap.mdx delete mode 100644 docs/docs/tutorials/advanced/navigation.md delete mode 100644 docs/docs/tutorials/advanced/screens.md delete mode 100644 docs/docs/tutorials/advanced/ui.md rename docs/docs/tutorials/basics/{quickstart.mdx => example.mdx} (87%) diff --git a/docs/docs/introduction.mdx b/docs/docs/introduction.mdx index 25b286772..5d147ce63 100644 --- a/docs/docs/introduction.mdx +++ b/docs/docs/introduction.mdx @@ -11,98 +11,211 @@ import TabItem from '@theme/TabItem'; 📺 Built-in focus control system allows for better **control** of your application behavior on the big screen. -## Getting Started - -Welcome to the Flexn Create documentation! - -If you're new to Flexn we recommend starting with [the learning course](tutorials/advanced/bootstrap). - -The interactive course will guide you through everything you need to know to use Flexn Create. - ### System requirements - [Node.js 12.22.0](https://nodejs.org) or later - [Xcode](https://apps.apple.com/us/app/xcode) on MacOS (not available for download on Windows or Linux) for running iOS and tvOS applications - [Android Studio](https://developer.android.com/studio) for Android applications -## Setup +## Installation -To begin with, it's recommended to first install [ReNative](https://renative.org/): +Install Flexn Create package and it's peer dependencies ```shell -yarn global add rnv +yarn add @flexn/create +yarn add @flexn/shopify-flash-list ``` ```shell -npm install rnv -g +npm install @flexn/create +npm install @flexn/shopify-flash-list ``` -Before you get into building complex applications, we suggest starting with a template as it already prepares some things for you. Based on your skill, requirements, you can choose one of the three: +## Setup -- Flexn template (you can build it yourself following the tutorial) -- Hello World template from ReNative - a simple application with some basic navigation structure -- Blank template from ReNative (recommended for first-timers) + + -During `rnv new` you will be given an option to choose between these templates (or you could type the name of your custom one, but we will not do that this time around). +Add this snippet to your MainActivity.java or MainActivity.kt -> For tutorial purposes starting off with the most basic - Blank template is recommended. + + -```shell -rnv new +```javascript +import io.flexn.create.TvRemoteHandlerModule +import android.view.KeyEvent + +@Override +public boolean onKeyUp(int keyCode, KeyEvent event) { + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "up"); + + return super.onKeyUp(keyCode, event); +} + +@Override +public boolean onKeyLongPress(int keyCode, KeyEvent event) { + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "longPress"); + + return super.onKeyLongPress(keyCode, event); +} + +@Override +public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + event.startTracking(); + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "down"); + + return true; + } + + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "down"); + + return super.onKeyDown(keyCode, event); +} ``` -If you chose Hello World or Blank templates, you also need to install Flexn Create. Add the code below in `renative.json`, which is located in the root of the project folder, under plugins: + + + +```javascript +import io.flexn.create.TvRemoteHandlerModule +import android.view.KeyEvent + +override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "up"); + + return super.onKeyUp(keyCode, event) +} + +override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean { + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "longPress"); + + return super.onKeyLongPress(keyCode, event) +} + +override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { + if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + event?.startTracking(); + TvRemoteHandlerModule.getInstance().onKeyEvent(event,"down"); + + return true; + } + + TvRemoteHandlerModule.getInstance().onKeyEvent(event, "down"); + + return super.onKeyDown(keyCode, event) +} +``` + + + + + + -```json -"plugins": { +Add this snippet to your renative.json plugins section + +```shell "@flexn/create": { - "version": "0.11.0", - "webpack": { - "modulePaths": true, - "moduleAliases": true - }, - "tvos": { - "podName": "FlexnCreate" - }, "androidtv": { - "package": "io.flexn.create.FlexnCreatePackage", - "projectName": "flexn-io-create", "activityImports": [ "io.flexn.create.TvRemoteHandlerModule", "android.view.KeyEvent;" ], "activityMethods": [ - "override fun dispatchKeyEvent(event: KeyEvent): Boolean {", - " if (event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) {", - " TvRemoteHandlerModule.getInstance().onKeyEvent(event);", - " }", - " return super.dispatchKeyEvent(event);", + "override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"up\");", + "return super.onKeyUp(keyCode, event)", + "}", + "override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {", + " TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"longPress\");", + " return super.onKeyLongPress(keyCode, event)", + "}", + "override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {", + "if(keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {", + "event?.startTracking();", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event,\"down\");", + "return true;", + "}", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"down\");", + "return super.onKeyDown(keyCode, event)", "}" - ] + ], + "package": "io.flexn.create.FlexnCreatePackage", + "projectName": "flexn-io-create" }, "firetv": { - "package": "io.flexn.create.FlexnCreatePackage", - "projectName": "flexn-io-create", "activityImports": [ "io.flexn.create.TvRemoteHandlerModule", "android.view.KeyEvent;" ], "activityMethods": [ - "override fun dispatchKeyEvent(event: KeyEvent): Boolean {", - " if (event.action == KeyEvent.ACTION_DOWN || event.action == KeyEvent.ACTION_UP) {", - " TvRemoteHandlerModule.getInstance().onKeyEvent(event);", - " }", - " return super.dispatchKeyEvent(event);", + "override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"up\");", + "return super.onKeyUp(keyCode, event)", + "}", + "override fun onKeyLongPress(keyCode: Int, event: KeyEvent?): Boolean {", + " TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"longPress\");", + " return super.onKeyLongPress(keyCode, event)", + "}", + "override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {", + "if(keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {", + "event?.startTracking();", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event,\"down\");", + "return true;", + "}", + "TvRemoteHandlerModule.getInstance().onKeyEvent(event, \"down\");", + "return super.onKeyDown(keyCode, event)", "}" - ] + ], + "package": "io.flexn.create.FlexnCreatePackage", + "projectName": "flexn-io-create" + }, + "tvos": { + "podName": "FlexnCreate" + }, + "version": "0.21.0-alpha.0", + "webpack": { + "moduleAliases": true, + "modulePaths": true } +}, +"@flexn/shopify-flash-list": { + "android": { + "package": "com.shopify.reactnative.flash_list.ReactNativeFlashListPackage" + }, + "androidtv": { + "package": "com.shopify.reactnative.flash_list.ReactNativeFlashListPackage" + }, + "firetv": { + "package": "com.shopify.reactnative.flash_list.ReactNativeFlashListPackage" + }, + "ios": { + "podName": "RNFlashList" + }, + "tvos": { + "podName": "RNFlashList" + }, + "macos": { + "podName": "RNFlashList" + }, + "webpack": { + "modulePaths": true, + "moduleAliases": true, + "nextTranspileModules": ["@flexn/shopify-flash-list"] + }, + "version": "1.4.8" +} ``` + + + diff --git a/docs/docs/tutorials/advanced/app.md b/docs/docs/tutorials/advanced/app.md deleted file mode 100644 index a281b55f3..000000000 --- a/docs/docs/tutorials/advanced/app.md +++ /dev/null @@ -1,295 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Application - -In this chapter we will quickly cover the state management of your application and also add a few utility hooks, that help unify different navigation structures. - -## State management - -You might be familiar with a lot of tools for that already like Redux, Recoil, Rematch or anything else, however this application is meant to be a simple template without too much of external dependencies, which users may or may not be familiar with, therefore we will stick with the most simple solution for state management, which already comes built in with React - Context API. - -We will be managing theme and the thing that depends upon it - the styles, therefore lets define a simple implementation of Context API at the bottom of `src/config.tsx` (you need to create this file if you haven't already): - -```typescript -const lightStyleSheet = createStyleSheet(staticThemes.light); -const darkStyleSheet = createStyleSheet(staticThemes.dark); - -const themes = { - light: { - static: { ...staticThemes.light }, - styles: lightStyleSheet.styles, - ids: lightStyleSheet.ids, - }, - dark: { - static: { ...staticThemes.dark }, - styles: darkStyleSheet.styles, - ids: darkStyleSheet.ids, - }, -}; - -type ThemeContextType = { - theme: Theme; - dark: boolean; - toggle?: () => void; -}; - -export const ThemeContext = createContext({ theme: themes.dark, dark: true }); - -export function ThemeProvider({ children }) { - const [dark, setDark] = useState(false); - - const toggle = () => { - const isDark = !dark; - setDark(isDark); - }; - - const theme = dark ? themes.dark : themes.light; - - return {children}; -} -``` - -With this snippet above we're missing the type definition, therefore in a separate `config.types.ts` file lets defined the types we need and import them: - -```typescript -import { ImageStyle, StatusBarStyle, TextStyle, ViewStyle } from 'react-native'; - -export type StaticTheme = { - primaryFontFamily?: string; - iconSize: number; - buttonSize: number; - menuWidth: number; - menuHeight: number; - colorLight?: string; - colorBrand: string; - colorBgPrimary: string; - colorTextPrimary: string; - colorTextSecondary: string; - colorBorder: string; - statusBar: StatusBarStyle; -}; - -export type ApplicationStyles = { - app: ViewStyle; - appContainer: ViewStyle; - container: ViewStyle; - modalContainer: ViewStyle; - textH1: TextStyle; - textH2: TextStyle; - textH3: TextStyle; - text: TextStyle; - icon: ViewStyle; - button: ViewStyle; - buttonText: TextStyle; - screen: ViewStyle; - screenModal: ViewStyle; - headerTitle: TextStyle; - header: ViewStyle; - modalHeader: ViewStyle; - image: ImageStyle; - menuContainer: ViewStyle; - menuButton: ViewStyle; - recycler: ViewStyle; - recyclerItem: ViewStyle; - sideMenuContainerAnimation: ViewStyle; - menuButtonText: TextStyle; - recyclerContent: ViewStyle; - recyclerContainer: ViewStyle; - burgerMenuBtn: ViewStyle; - menuContainerBurgerOpen: ViewStyle; - menuItemsBurgerOpen: ViewStyle; - detailsInfoContainer: ViewStyle; - menuItems: ViewStyle; - center: ViewStyle; - detailsTitle: TextStyle; - recyclerItemText: TextStyle; -}; - -export type RNMQIDS = { - menuContainer: string; - burgerMenuBtn: string; - menuContainerBurgerOpen: string; - menuItemsBurgerOpen: string; - menuItems: string; -}; - -export type Theme = { - static: StaticTheme; - styles: ApplicationStyles; - ids: RNMQIDS; -}; -``` - -Great! Now we have our context, all that's left is to wrap our application with it and then we can use it in any component: - -```typescript -import React from 'react'; -import { ThemeProvider } from '../config'; -import Navigation from '../navigation'; - -const App = () => ( - - - - -); - -export default App; -``` - -Only one final step remains before our application file is ready - TV Focus (you can read about it more [here](../../guides/focus-manager)). Briefly, we will need a HOC (higher order component), which handles all the press, swipe events coming from a remote controller and initializes tracking of focusable and currently focused items. To do that, just import the App from SDK and wrap your application with it: - -```typescript -import React from 'react'; -import { App, Debugger } from '@flexn/create'; -import { ThemeProvider } from '../config'; -import Navigation from '../navigation'; - -const MyApp = () => ( - - - - - - -); - -export default MyApp; -``` - -## Hooks - -There are a few navigation hooks needed for a better multiplatform experience as web and native platforms use different libraries for it. Lets start with defining what we need - hooks for the most common navigation methods, which should be familiar if you have used React Navigation before: - -- `useNavigate`, which pushes another screen in stack if it's not already in it, otherwise goes back to it; -- `usePop`, pops the last entry of the stack; -- `useReplace`, replaces the last entry of the stack with a new screen; -- `useOpenDrawer`, dispatches a navigation event, which triggers drawer to open (mocked on web); -- `useOpenUrl`, opens a URL link in a browser (except for TV platforms); - -Most platforms will call the general `hooks/navigation/index.ts` file as we use React Navigation for most of the platforms: - -```typescript -import { useFocusEffect, useNavigation } from '@react-navigation/native'; -import { Linking } from 'react-native'; -import { isPlatformIos, isPlatformAndroid, isPlatformMacos } from '@rnv/renative'; - -export function useNavigate({ navigation }) { - function navigate(route: string, params?: any) { - navigation.navigate(route, params); - } - return navigate; -} - -export function usePop({ navigation }) { - function pop() { - navigation.pop(); - } - return pop; -} - -export function useReplace({ navigation }) { - function replace(route: string) { - if (isPlatformIos || isPlatformAndroid) { - navigation.reset({ - index: 0, - routes: [{ name: route }], - }); - } else { - navigation.navigate(route); - } - } - return replace; -} - -export function useOpenDrawer({ navigation }) { - function openDrawer() { - navigation.dispatch({ type: 'OPEN_DRAWER' }); - } - return openDrawer; -} - -export function useOpenURL() { - async function openURL(url: string) { - if (isPlatformIos || isPlatformAndroid || isPlatformMacos) { - await Linking.openURL(url); - } else { - // error happened - } - } - return openURL; -} - -export { useFocusEffect, useNavigation }; -``` - -As a second step, we need to create a file for web - `hooks/navigation/index.web.ts`, which exports the same hooks, but with different logic: - -```typescript -import { Linking } from 'react-native'; -import Router, { useRouter } from 'next/router'; - -export function useNavigate() { - function navigate(route: string, params?: any) { - if (params) { - Router.push({ - pathname: route, - query: params, - }); - } else { - Router.push(route, params); - } - } - return navigate; -} - -export function usePop() { - function pop() { - Router.back(); - } - return pop; -} - -export function useReplace() { - function replace(route: string) { - Router.replace(route); - } - return replace; -} - -export function useOpenDrawer() { - function openDrawer() { - return; - } - return openDrawer; -} - -export function useOpenURL() { - async function openURL(url: string) { - await Linking.openURL(url); - } - return openURL; -} - -export function useFocusEffect() { - return; -} - -export { useRouter as useNavigation }; -``` - -Finally, lets create our general hooks export file - `hooks/index.ts`, where you would be exporting not just the navigation hooks, but other, that you may need to customize to match the needs of your application: - -```typescript -export { - useFocusEffect, - useNavigate, - usePop, - useOpenDrawer, - useOpenURL, - useReplace, - useNavigation, -} from './navigation'; -``` diff --git a/docs/docs/tutorials/advanced/bootstrap.mdx b/docs/docs/tutorials/advanced/bootstrap.mdx deleted file mode 100644 index 7484c4123..000000000 --- a/docs/docs/tutorials/advanced/bootstrap.mdx +++ /dev/null @@ -1,78 +0,0 @@ ---- -sidebar_position: 1 ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Bootstrap - -In this tutorial we will not start completely from scratch as the aim is to explain our process of creating Flexn template and also showcase the befits of using Flexn Create. - -## Bootstrap Flexn template - -To avoid having to configure plugins, engines and various other settings we will start with `@flexn/create-template-starter`, however unlike in quick start we will delete all the src files, meaning we get a fresh start and start focusing on what matters - the multiplatform code. - -> If you want to learn more about the configurations and plugin management or bootstrapping process, head over to [`rnv` website](https://www.renative.org). - -Let's start by creating a new RNV project: - -```shell -rnv new --projectTemplate @flexn/create-template-starter -``` - -- Follow the instructions on your cli: -- Enter you project's name, title, app ID (or skip this step by clicking Enter), version; -- If you have multiple workspaces available, select rnv: - -
- -![HelloFlexn](/img/tutorial/step1.png) - -
- -- Select the template version you want to use - Latest is recommended; -- Follow the rest of instructions on your cli; -- Navigate to your project's folder; -- Run command below and select your preferable platform to run: - -```shell -rnv run -``` - -- Follow the rest of instructions on your cli; -- Now you should see the orginal template (you can see Apple TV example bellow); - -
- -![HelloFlexn](/img/tutorial/step5.png) - -
- -## The advanced part - -In order to understand our process of creating this template and learn more about multiplatform development lets handicap ourselves first by deleting the `src` folder. You can do that using command below or do it manually via IDE/file manager. - - - - -```shell -rm -R -f src -``` - - - - -```cmd -rmdir src /s /q -``` - - - - -```shell -rm -R -f src -``` - - - diff --git a/docs/docs/tutorials/advanced/navigation.md b/docs/docs/tutorials/advanced/navigation.md deleted file mode 100644 index 2acf5e130..000000000 --- a/docs/docs/tutorials/advanced/navigation.md +++ /dev/null @@ -1,431 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Navigation - -Once we have screens ready it's time to construct proper navigation which can behave on any OS and form factor. -In this chapter we're going to cover how to build simple navigation which works on: web, TV and mobile. - -For Flexn Template we choose to use [react-navigation](https://reactnavigation.org/). Navigation is one of the most non platform agnostic parts in the whole application. -Each platform has it's own navigation structure, navigational paradigms, components, UI. Because of that we're going differentiate different platform/form factor per separate files as following: - -- `index.chromecast.tsx` - defines navigation which is only for chromecast -- `index.desktop.tsx` - defines navigation which is only for macos -- `index.tv.native.tsx` - defines navigation which is only for Android TV, Fire TV and Apple TV -- `index.tsx` - defines navigation which would be applied for all platforms which is not mentioned above, but in our app it defined mobile(ios, android) navigation - -There is one exception. As you can see there is no specific file for web. According to order described above `index.tsx` should handle it, but that's not the case. -Since in our template as an web engine we're using [next.js](https://nextjs.org/) we're also have to apply rules which works for next.js. More about that [Web navigation](#web-navigation) - -## Mobile navigation - -First let's start from most common one - mobile. Let's create first file `src/navigation/index.tsx`. For mobile we choose to have [Drawer navigation](https://reactnavigation.org/docs/drawer-based-navigation). It has very common use case of ReactNavigation without any specificness. We're utilizing multiple different stacks to achieve following functionality: - -We're putting `Modal` out of our `DrawerNavigator` because regardless in which page we are we would like to be able to render Modal always on the top independently. Menu component can be found [here](./ui#menu-component). - -```javascript -import React, { useContext, useEffect } from 'react'; -import { StatusBar } from 'react-native'; -import { createStackNavigator, CardStyleInterpolators } from '@react-navigation/stack'; -import { NavigationContainer } from '@react-navigation/native'; -import { createDrawerNavigator } from '@react-navigation/drawer'; -// import { CastButton } from 'react-native-google-cast'; -import ScreenHome from '../screens/home'; -import ScreenCarousels from '../screens/carousels'; -import ScreenDetails from '../screens/details'; -import ScreenModal from '../screens/modal'; -import Menu, { DrawerButton } from '../components/menu'; -import { ROUTES, ThemeContext } from '../config'; - -const ModalStack = createStackNavigator(); -const Stack = createStackNavigator(); -const Drawer = createDrawerNavigator(); - -const CarouselsStack = () => ( - // implementation in next example -); - -const DrawerNavigator = ({ navigation }) => { - // implementation in next example -}; - -const App = () => { - const { theme } = useContext(ThemeContext); - - useEffect(() => { - StatusBar.setBarStyle(theme.static.statusBar); - StatusBar.setBackgroundColor(theme.static.colorBgPrimary); - }, [theme?.static]); - - return ( - - - - - - - ); -}; - -export default App; -``` - -DrawerNavigator contains rest of our navigational screens. But as you can see instead of putting `ScreenCarousels` and `ScreenDetails` details directly into `Drawer.Navigator` we're creating separate stack for it. The reason for it to create a proper stack history so in this case when we're opened `ScreenDetails` we can navigate back to `ScreenCarousels` as we expect to. - -```javascript -const CarouselsStack = () => ( - - - - -); - -const DrawerNavigator = ({ navigation }) => { - const { theme } = useContext(ThemeContext); - - return ( - } - screenOptions={{ - headerLeft: () => , - headerTitleStyle: theme.styles.headerTitle, - headerStyle: theme.styles.header, - headerShown: true, - }} - > - ( - // - // ), - // }} - /> - - - ); -}; -``` - -## Desktop navigation - -The next is desktop navigation create a file called `src/navigation/index.desktop.tsx`. Desktop navigation is even more simpler, but instead of Drawer and separate stacks we're using custom [menu](./ui#menu-component) and single stack to hold all navigational pages: - -```javascript -import React, { useContext, useEffect } from 'react'; -import { StatusBar } from 'react-native'; -import { View } from '@flexn/create'; -import { createStackNavigator, CardStyleInterpolators } from '@react-navigation/stack'; -import { NavigationContainer } from '@react-navigation/native'; -import ScreenHome from '../screens/home'; -import ScreenCarousels from '../screens/carousels'; -import ScreenDetails from '../screens/details'; -import ScreenModal from '../screens/modal'; -import Menu from '../components/menu'; -import { ThemeContext, ROUTES } from '../config'; - -const Stack = createStackNavigator(); -const RootStack = createStackNavigator(); - -const StackNavigator = ({ navigation }) => { - const { theme } = useContext(ThemeContext); - - return ( - - - - - - - - - ); -}; - -const App = () => { - const { theme } = useContext(ThemeContext); - - useEffect(() => { - StatusBar.setBarStyle(theme.static.statusBar); - }, []); - - return ( - - - - - - - - - ); -}; - -export default App; -``` - -## Native TV navigation - -The next is TV `src/navigation/index.tv.native.tsx`. Native TV navigation is exceptional because for that we have created our custom `SideNavigator`. The reason of this choice is because all react navigation defaults like Drawer doesn't work well on TV and are very flaky. There are few important parts which we need to know here. - -As you can see we're using `createNativeStackNavigator` instead if `createStackNavigator`. Native Stack Navigator offers native performance and are better designed to work with react-native-screens which is required for TV navigation to work. Menu component can be found [here](./ui#menu-component). - -```javascript -import React, { useEffect, useCallback } from 'react'; -import { View } from '@flexn/create'; -import { TVMenuControl, StyleSheet } from 'react-native'; -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { - NavigationContainer, - useNavigationBuilder, - StackActions, - StackRouter, - createNavigatorFactory, -} from '@react-navigation/native'; -import { screensEnabled } from 'react-native-screens'; -import { isPlatformTvos } from '@rnv/renative'; -import { ScreenContainer } from 'react-native-screens'; //eslint-disable-line -import ResourceSavingScene from '@react-navigation/drawer/src/views/ResourceSavingScene'; - -import ScreenHome from '../screens/home'; -import ScreenCarousels from '../screens/carousels'; -import ScreenDetails from '../screens/details'; -import ScreenModal from '../screens/modal'; -import Menu from '../components/menu'; -import { ROUTES } from '../config'; - -const createTVSideNavigator = createNavigatorFactory(Navigator); - -function Navigator({ initialRouteName, children, screenOptions, drawerContent, ...rest }) { - // implementation in next example -} - -const RootStack = createNativeStackNavigator(); -const SideNavigatorStack = createTVSideNavigator(); - -const SideNavigator = () => ( - } - > - - - - -); - -const App = () => ( - - - - - - -); - -const styles = StyleSheet.create({ - container: { - top: 0, - bottom: 0, - left: 0, - right: 0, - zIndex: 2, - opacity: 1, - position: 'absolute', - }, - content: { flex: 1 }, - main: { flex: 1 }, -}); - -export default App; -``` - -For SideNavigator we have created our own navigator which is simple enough, but at the same time are optimized to work on Native TV. What SideNavigator does essentially is very similar to Drawer. It keeps SideMenu always visible on the left side and at the same time rendering the content. The main difference is that in this case we're using react-native-screens and also we're able to define our own menu container which has proper focus handling. - -```javascript -function Navigator({ initialRouteName, children, screenOptions, drawerContent, ...rest }) { - if (!screensEnabled()) { - throw new Error('Native stack is only available if React Native Screens is enabled.'); - } - - const { state, navigation, descriptors } = useNavigationBuilder(StackRouter, { - initialRouteName, - children, - screenOptions, - }); - - const tabPressEventHandler = useCallback(() => { - const isFocused = navigation.isFocused(); - requestAnimationFrame(() => { - if (state.index > 0 && isFocused) { - navigation.dispatch({ - ...StackActions.popToTop(), - target: state.key, - }); - } - }); - }, [navigation, state.index, state.key]); - - useEffect(() => { - if (isPlatformTvos) { - TVMenuControl.enableTVMenuKey(); - if (state.index === 0) { - TVMenuControl.disableTVMenuKey(); - } - } - - navigation.addListener('tabPress', tabPressEventHandler); - return () => navigation.removeListener('tabPress', tabPressEventHandler); - }, [navigation, state.index, tabPressEventHandler]); - - const renderContent = () => ( - - {state.routes.map((route, index) => { - const descriptor = descriptors[route.key]; - const { unmountOnBlur } = descriptor.options; - const isFocused = state.index === index; - - if (unmountOnBlur && !isFocused) { - return null; - } - - return ( - - {descriptor.render()} - - ); - })} - - ); - - const renderDrawerView = () => - drawerContent({ - state, - navigation, - descriptors, - ...rest, - }); - - return ( - - {renderDrawerView()} - {renderContent()} - - ); -} -``` - -## Web navigation - -Web navigation is very different than others. Since it does not utilize react-navigation but instead of it's based on next.js navigational paradigms. As you can see there is no single file which would define how navigation structure for the web looks like. Instead of that we're folders structure itself which in next.js is definition of the page. - -Let's create a files as following. - -First `src/pages/index.tsx`. As you can guess it holds our home page. - -```javascript -import React from 'react'; -import ScreenHome from '../screens/home'; - -const Page = () => ; -export default Page; -``` - -Next is `src/pages/[slug]/index.tsx`. By having `[slug]` as folder name we can capture rest of our urls and map them as following: - -```javascript -import React from 'react'; -import { useRouter } from 'next/router'; -import Error from 'next/error'; -import ScreenHome from '../../screens/home'; -import ScreenCarousels from '../../screens/carousels'; -import ScreenDetails from '../../screens/details'; -import ScreenModal from '../../screens/modal'; -import { ROUTES } from '../../config'; - -type NavigationScreenKey = '/' | 'modal' | 'my-page'; - -const pages = { - [ROUTES.HOME]: ScreenHome, - [ROUTES.CAROUSELS]: ScreenCarousels, - [ROUTES.DETAILS]: ScreenDetails, - [ROUTES.MODAL]: ScreenModal, -}; - -const App = () => { - const router = useRouter(); - - const Page = pages[router.query?.slug as NavigationScreenKey]; - - if (!Page) { - return ; - } - - return ; -}; - -export default App; -``` - -And finally create `src/pages/_app.tsx` define our pages wrapper and add top [menu](./ui#menu-component) there: - -```javascript -import React from 'react'; -import { View } from '@flexn/create'; -import Menu from '../components/menu'; -import { themeStyles, ThemeProvider } from '../config'; - -export default function MyApp({ Component, pageProps }) { - return ( - - - - - - - ); -} -``` - -## Chromecast navigation - -And finally most simplistic is chromecast navigation which is holding only on page to render text in casting device. It can be defined as simple as `src/navigation/index.chromecast.tsx`: - -```javascript -import React from 'react'; -import ScreenCast from '../screens/cast'; - -const App = () => ; - -export default App; -``` diff --git a/docs/docs/tutorials/advanced/screens.md b/docs/docs/tutorials/advanced/screens.md deleted file mode 100644 index f4e164291..000000000 --- a/docs/docs/tutorials/advanced/screens.md +++ /dev/null @@ -1,457 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Screens - -Let's start filling content of our application. Flexn Template contains several platform agnostics screens which means the same file is rendered on all the platforms. - -## Abstracted screen - -First let's create an abstracted screen wrapper which will hold logic repeated over each screen. In `src/screens` folder create file called `screens.tsx` and fill with the following content: - -```javascript -import { Screen as FMScreen, ScreenProps, ScreenStates } from '@flexn/create'; -import React, { useState, useCallback } from 'react'; -import { useFocusEffect } from '../hooks'; - -const Screen = ({ children, stealFocus, focusOptions, style, ...rest }: ScreenProps) => { - const [screenState, setScreenState] = useState < ScreenStates > 'foreground'; - - useFocusEffect( - useCallback(() => { - setScreenState('foreground'); - - return () => { - setScreenState('background'); - }; - }, []) - ); - - return ( - - {children} - - ); -}; - -export default Screen; -``` - -We are using [Screen](../../components/screen.mdx) component to wrap every template screen and by utilizing `useFocusEffect` hook setting state of the screen whatever screen is in background or foreground. It's worth to mention that [Screen](../../components/screen.mdx) functionality is applied only for TV platforms for the rest behind the scenes it's only simple React Native View. - -## Home screen - -It's a good practice to start from Home screen. Create a new file called `src/screens/home.tsx` and copy over the code below: - -```javascript -import React, { useContext, useRef } from 'react'; -import { Text, View, ScrollView, TouchableOpacity, Image } from '@flexn/create'; -import { Api } from '@rnv/renative'; -import Icon from 'react-native-vector-icons/FontAwesome'; -import { ROUTES, ICON_LOGO, ThemeContext } from '../config'; -import { useNavigate, useOpenURL } from '../hooks'; -import { testProps } from '../utils'; -import Screen from './screen'; -import packageJson from '../../package.json'; - -const ScreenHome = ({ navigation }: { navigation?: any }) => { - const swRef = useRef() as React.MutableRefObject; - const navigate = useNavigate({ navigation }); - const openURL = useOpenURL(); - - const { theme, toggle } = useContext(ThemeContext); - - const focusAnimation = { - type: 'background_color', - colorFocus: theme.static.colorBrand, - colorBlur: theme.static.colorBgPrimary, - }; - - return ( - - - - {'Flexn Create Example'} - v {packageJson.version} - {`platform: ${Api.platform}`} - {`factor: ${Api.formFactor}`} - {`engine: ${Api.engine}`} - { - if (swRef.current) swRef.current.scrollTo({ y: 0 }); - }} - style={theme.styles.button} - focusOptions={{ - animatorOptions: focusAnimation, - forbiddenFocusDirections: ['up'], - }} - {...testProps('template-screen-home-try-me-button')} - > - Try Me! - - navigate(ROUTES.CAROUSELS)} - style={theme.styles.button} - focusOptions={{ - animatorOptions: focusAnimation, - }} - {...testProps('template-screen-home-now-try-me-button')} - > - Now Try Me! - - Explore more - - openURL('https://github.com/flexn-io/create')} - style={theme.styles.icon} - focusOptions={{ - forbiddenFocusDirections: ['left'], - }} - {...testProps('template-screen-home-navigate-to-github')} - > - - - openURL('https://create.flexn.org')} - style={theme.styles.icon} - {...testProps('template-screen-home-navigate-to-renative')} - > - - - openURL('https://twitter.com/flexn_io')} - style={theme.styles.icon} - focusOptions={{ - forbiddenFocusDirections: ['right'], - }} - {...testProps('template-screen-home-navigate-to-twitter')} - > - - - - - - ); -}; - -export default ScreenHome; -``` - -## Carousels screen - -One of the most dynamic screens in whole template. Let's add several rows with some nice images inside. First let's create a file `src/utils/index.ts` and write a function which generates a random data for us: - -```javascript -import { isFactorMobile } from '@rnv/renative'; - -const kittyNames = ['Abby', 'Angel', 'Annie', 'Baby', 'Bailey', 'Bandit']; - -function interval(min = 0, max = kittyNames.length - 1) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -const data = {}; -export function getRandomData(row: number, idx?: number, items = 50) { - const width = isFactorMobile ? 400 : 650; - const height = 200; - - if (data[row] && idx !== undefined) { - return data[row][idx]; - } - - const temp: { backgroundImage: string, title: string, index: number }[] = []; - for (let index = 0; index < items; index++) { - temp.push({ - index, - backgroundImage: `https://placekitten.com/${width + row}/${height + index}`, - title: `${kittyNames[interval()]} ${kittyNames[interval()]} ${kittyNames[interval()]}`, - }); - } - - data[row] = temp; - - return temp; -} -``` - -Next create a file `src/screens/carousels.tsx`. There define screen layout define sizes of ours rows based on the platform and start filling array with data: - -```javascript -import { - Image, - TouchableOpacity, - RecyclableList, - RecyclableListDataProvider, - RecyclableListLayoutProvider, - View, - ScrollView, - Text, -} from '@flexn/create'; -import { testProps } from '../utils'; -import React, { useContext, useEffect, useRef, useState } from 'react'; -import { Dimensions } from 'react-native'; -import { isFactorMobile } from '@rnv/renative'; -import { Ratio, ThemeContext, ROUTES } from '../config'; -import { useNavigate } from '../hooks'; -import { getRandomData } from '../utils'; -import Screen from './screen'; - -const { width } = Dimensions.get('window'); -const MARGIN_GUTTER = Ratio(20); - -const itemsInRows = [ - [1, 3], - [2, 4], - [3, 5], - [4, 6], - [2, 4], - [3, 5], -]; - -function getRecyclerDimensions(itemsInViewport: number) { - return { - layout: { width: width / itemsInViewport, height: Ratio(270) }, - item: { width: width / itemsInViewport - MARGIN_GUTTER, height: Ratio(250) }, - }; -} - -const RecyclerExample = ({ items, rowNumber, dimensions: { layout, item }, parentContext, navigation }: any) => { - // implementation in next example -}; - -const ScreenCarousels = ({ navigation }: { navigation?: any }) => { - const { theme } = useContext(ThemeContext); - const [recyclers, setRecyclers] = useState< - { - items: any; - dimensions: { - layout: { - width: number; - height: number; - }; - item: { - width: number; - height: number; - }; - }; - }[] - >([]); - - useEffect(() => { - setRecyclers( - itemsInRows.map(([smallScreenItems, bigScreenItems], rowNumber) => ({ - dimensions: getRecyclerDimensions(isFactorMobile ? smallScreenItems : bigScreenItems), - items: getRandomData(rowNumber), - })) - ); - }, []); - - const renderRecyclers = () => - recyclers.map((recyclerInfo, i) => ( - - )); - - return ( - - {renderRecyclers()} - - ); -}; - -export default ScreenCarousels; -``` - -Finally add function which is rendering our carousels: - -```javascript -const RecyclerExample = ({ items, rowNumber, dimensions: { layout, item }, parentContext, navigation }: any) => { - const navigate = useNavigate({ navigation }); - const { theme } = useContext(ThemeContext); - - const [dataProvider] = useState( - new RecyclableListDataProvider((r1: number, r2: number) => r1 !== r2).cloneWithRows(items) - ); - - const layoutProvider = useRef( - new RecyclableListLayoutProvider( - () => '_', - (_: string | number, dim: { width: number, height: number }) => { - dim.width = layout.width; - dim.height = layout.height; - } - ) - ).current; - - return ( - - { - return ( - { - navigate(ROUTES.DETAILS, { row: rowNumber, index: data.index }); - }} - {...testProps(`template-my-page-image-pressable-${index}`)} - > - - - {data.title} - - - ); - }} - isHorizontal - style={theme.styles.recycler} - contentContainerStyle={theme.styles.recyclerContent} - scrollViewProps={{ - showsHorizontalScrollIndicator: false, - }} - focusOptions={{ - forbiddenFocusDirections: ['right'], - }} - /> - - ); -}; -``` - -## Details screen - -Next is Details screen. That's the target page when we click on any of carousel items. Let's add it at `src/screens/details.tsx` and copy code below: - -```javascript -import { TouchableOpacity, ImageBackground, View, Text, ScrollView, ActivityIndicator } from '@flexn/create'; -import React, { useContext, useState, useEffect } from 'react'; -import { isPlatformWeb } from '@rnv/renative'; -import { ThemeContext, ROUTES } from '../config'; -import { usePop, useReplace } from '../hooks'; -import { getRandomData } from '../utils'; -import Screen from './screen'; - -const ScreenDetails = ({ route, navigation, router }: { navigation?: any; router?: any; route?: any }) => { - const replace = useReplace({ navigation }); - const pop = usePop({ navigation }); - const [item, setItem] = useState<{ backgroundImage: string; title: string }>(); - const { theme } = useContext(ThemeContext); - - const focusAnimation = { - type: 'border', - colorFocus: theme.static.colorBrand, - colorBlur: '#EEEEEE', - borderWidth: 3, - }; - - useEffect(() => { - const params = isPlatformWeb ? router.query : route?.params; - setItem(getRandomData(params.row, params.index)); - }, []); - - if (!item) { - return ( - - - - ); - } - - return ( - - - - - {item.title} - - pop()} - focusOptions={{ - forbiddenFocusDirections: ['up'], - animatorOptions: focusAnimation, - }} - > - Go back - - replace(ROUTES.HOME)} - focusOptions={{ - forbiddenFocusDirections: ['down'], - animatorOptions: focusAnimation, - }} - > - Go to home - - - - - ); -}; - -export default ScreenDetails; -``` - -## Modal screen - -Modal screen is the one which is rendered on the top of everything. Create a new file called `src/screens/modal.tsx` and copy this code there: - -```javascript -import React, { useContext } from 'react'; -import { Text, View, ScrollView, TouchableOpacity } from '@flexn/create'; -import Icon from 'react-native-vector-icons/FontAwesome'; -import { ThemeContext } from '../config'; -import { usePop } from '../hooks'; -import Screen from './screen'; - -const ScreenModal = ({ navigation }: { navigation?: any }) => { - const pop = usePop({ navigation }); - const { theme } = useContext(ThemeContext); - - return ( - - - pop()} style={theme.styles.icon}> - - - - - This is my Modal! - - - ); -}; - -export default ScreenModal; -``` - -## Screen Cast - -Cast screen is super simplistic page which is purpose is only represent a simple text on your casting device. Create a new file called `src/screens/cast.tsx` and copy following code there: - -```javascript -import React from 'react'; -import { Text } from '@flexn/create'; -import { themeStyles } from '../config'; -import Screen from './screen'; - -const ScreenCast = () => ( - - This is cast Page! - -); - -export default ScreenCast; -``` diff --git a/docs/docs/tutorials/advanced/ui.md b/docs/docs/tutorials/advanced/ui.md deleted file mode 100644 index 9541a642e..000000000 --- a/docs/docs/tutorials/advanced/ui.md +++ /dev/null @@ -1,655 +0,0 @@ ---- -sidebar_position: 5 ---- - -# UI - -In this section we will cover the styling of the application and we will also add a custom menu component, which will improve the user experience. - -## Styles - -Styling for the applications is done in React Native like fashion, meaning camel case is used and not all browser specific css features are supported. More on the key differences in [React Native documentation](https://reactnative.dev/docs/style). We also are using `react-native-media-query` library to have responsive styles on web, you can read more on that [here](https://github.com/kasinskas/react-native-media-query). - -Lets define our styles in the single file - `./src/config.tsx`, just above the ThemeContext definition as they are dependant on the changes in theme. - -> This is only meant as an example. If you plan to scale your application, it's recommended to have separate styles file for each component or screen as the list of styles can become huge and cumbersome to maintain. - -```typescript -import React, { createContext, useState } from 'react'; -import { Dimensions, PixelRatio, StatusBarStyle } from 'react-native'; -import StyleSheet from 'react-native-media-query'; -import { - getScaledValue, - isEngineNative, - isEngineRnWeb, - isFactorBrowser, - isFactorDesktop, - isFactorMobile, - isFactorTv, - isPlatformAndroidtv, - isPlatformFiretv, - isPlatformTizen, - isPlatformWeb, - isPlatformWebos, - isPlatformWindows, - isPlatformMacos, - registerServiceWorker, -} from '@rnv/renative'; -import '../platformAssets/runtime/fontManager'; -import { StaticTheme, Theme } from './configTypes'; -//@ts-ignore -import ICON_LOGO from '../platformAssets/runtime/logo.png'; - -if (isFactorBrowser) registerServiceWorker(); - -export const hasMobileWebUI = isFactorMobile && isEngineWeb; -export const hasHorizontalMenu = !isFactorMobile && !isFactorDesktop && !hasMobileWebUI; -export const isWebBased = isPlatformWeb || isPlatformTizen || isPlatformWebos; - -const staticTheme = { - primaryFontFamily: 'Inter-Light', - iconSize: getScaledValue(30), - buttonSize: getScaledValue(30), - menuWidth: hasHorizontalMenu ? '100%' : getScaledValue(280), - menuHeight: hasHorizontalMenu ? getScaledValue(80) : '100%', - colorBrand: '#0A74E6', -}; - -const staticThemes = { - dark: { - colorBgPrimary: '#000000', - colorTextPrimary: '#FFFFFF', - colorTextSecondary: '#AAAAAA', - colorBorder: '#111111', - statusBar: 'light-content' as StatusBarStyle, - ...staticTheme, - }, - light: { - colorBgPrimary: '#FFFFFF', - colorTextPrimary: '#000000', - colorTextSecondary: '#333333', - colorBorder: '#EEEEEE', - statusBar: 'dark-content' as StatusBarStyle, - ...staticTheme, - }, -}; - -export function Ratio(pixels: number): number { - if (!(isPlatformAndroidtv || isPlatformFiretv)) return pixels; - const resolution = Dimensions.get('screen').height * PixelRatio.get(); - - return Math.round(pixels / (resolution < 2160 ? 2 : 1)); -} - -export const createStyleSheet = (currentTheme: StaticTheme) => - StyleSheet.create({ - app: { - flexDirection: isFactorDesktop ? 'row' : 'column', - }, - appContainer: { - position: 'absolute', - left: !hasHorizontalMenu ? getScaledValue(280) : 0, - right: 0, - top: hasHorizontalMenu ? getScaledValue(80) : 0, - bottom: 0, - }, - container: { - justifyContent: 'center', - alignItems: 'center', - paddingVertical: getScaledValue(50), - minHeight: getScaledValue(300), - alignSelf: 'stretch', - width: '100%', - }, - modalContainer: isEngineWeb - ? { - position: 'absolute', - backgroundColor: currentTheme.colorBgPrimary, - zIndex: 100, - top: 0, - left: 0, - height: '100vh', - width: '100%', - } - : { - flex: 1, - backgroundColor: currentTheme.colorBgPrimary, - }, - textH1: { - fontFamily: currentTheme.primaryFontFamily, - fontSize: getScaledValue(28), - marginHorizontal: getScaledValue(20), - color: currentTheme.colorTextPrimary, - textAlign: 'center', - fontWeight: '600', - }, - textH2: { - fontFamily: currentTheme.primaryFontFamily, - fontSize: getScaledValue(20), - marginHorizontal: getScaledValue(20), - color: currentTheme.colorTextPrimary, - textAlign: 'center', - }, - textH3: { - fontFamily: currentTheme.primaryFontFamily, - fontSize: getScaledValue(15), - marginHorizontal: getScaledValue(20), - marginTop: getScaledValue(5), - color: currentTheme.colorTextPrimary, - textAlign: 'center', - }, - text: { - fontFamily: currentTheme.primaryFontFamily, - color: currentTheme.colorTextSecondary, - fontSize: getScaledValue(20), - marginTop: getScaledValue(10), - textAlign: 'left', - }, - icon: { - width: getScaledValue(40), - height: getScaledValue(40), - margin: getScaledValue(10), - }, - button: { - marginHorizontal: getScaledValue(20), - borderWidth: getScaledValue(2), - borderRadius: getScaledValue(25), - borderColor: currentTheme.colorBorder, - height: getScaledValue(50), - width: '80%', - marginTop: getScaledValue(20), - justifyContent: 'center', - alignItems: 'center', - }, - buttonText: { - fontFamily: currentTheme.primaryFontFamily, - color: currentTheme.colorTextPrimary, - fontSize: getScaledValue(20), - }, - screen: { - backgroundColor: currentTheme.colorBgPrimary, - flex: 1, - }, - screenModal: { - position: 'absolute', - backgroundColor: currentTheme.colorBgPrimary, - top: hasHorizontalMenu && isEngineWeb ? -currentTheme.menuHeight : 0, - left: hasHorizontalMenu || isEngineNative || isPlatformMacos ? 0 : -currentTheme.menuWidth, - right: 0, - bottom: 0, - zIndex: 3, - }, - headerTitle: { - color: currentTheme.colorTextSecondary, - fontFamily: currentTheme.primaryFontFamily, - fontSize: getScaledValue(18), - }, - header: { - backgroundColor: currentTheme.colorBgPrimary, - borderBottomWidth: 1, - height: getScaledValue(70), - }, - modalHeader: { - width: '100%', - height: getScaledValue(80), - alignItems: 'flex-end', - paddingTop: getScaledValue(20), - }, - image: { - marginBottom: getScaledValue(30), - width: getScaledValue(100), - height: getScaledValue(100), - }, - menuContainer: { - ...(isFactorTv - ? { - height: '100%', - alignItems: 'center', - justifyContent: 'center', - } - : { - paddingTop: getScaledValue(hasHorizontalMenu ? 20 : 40), - backgroundColor: currentTheme.colorBgPrimary, - alignItems: 'flex-start', - borderBottomWidth: getScaledValue(hasHorizontalMenu ? 1 : 0), - borderColor: currentTheme.colorBorder, - flexDirection: hasHorizontalMenu ? 'row' : 'column', - borderRightWidth: getScaledValue(hasHorizontalMenu ? 0 : 1), - width: isPlatformMacos ? currentTheme.menuWidth : '100%', - height: currentTheme.menuHeight, - }), - }, - menuContainerBurgerOpen: { - height: '100vh', - width: isPlatformWindows ? '100%' : '100%', - zIndex: 5, - justifyContent: 'flex-start', - alignItems: 'flex-start', - flex: 1, - }, - burgerMenuBtn: { - flex: 1, - display: 'none', - textAlign: 'end', - right: 10, - '@media (max-width: 768px)': { - display: 'flex !important;', - }, - }, - menuItems: { - display: 'flex', - flexDirection: 'row', - '@media (max-width: 768px)': { - display: 'none', - }, - }, - menuItemsBurgerOpen: { - display: 'flex', - flexDirection: 'column', - position: 'absolute', - top: 50, - }, - sideMenuContainerAnimation: { - backgroundColor: currentTheme.colorBgPrimary, - width: Ratio(400), - borderColor: currentTheme.colorBorder, - position: 'absolute', - left: 0, - top: 0, - right: 0, - bottom: 0, - borderWidth: 1, - }, - menuButton: { - alignSelf: 'flex-start', - alignItems: 'center', - maxWidth: getScaledValue(400), - minWidth: getScaledValue(50), - flexDirection: 'row', - padding: getScaledValue(10), - ...(isFactorTv && { - marginRight: Ratio(20), - }), - }, - menuButtonText: { - marginLeft: isFactorTv ? Ratio(16) : 8, - }, - recyclerContainer: { - flex: 1, - ...(isFactorTv && { - left: Ratio(80), - marginVertical: Ratio(20), - }), - }, - recyclerContent: { - ...(isFactorTv && { - paddingLeft: Ratio(40), - paddingRight: Ratio(100), - }), - }, - recycler: { width: '100%', height: Ratio(270) }, - recyclerItem: { - width: getScaledValue(90), - height: getScaledValue(50), - margin: getScaledValue(5), - justifyContent: 'flex-end', - alignItems: 'center', - }, - recyclerItemText: { - color: currentTheme.colorTextPrimary, - fontSize: isFactorMobile ? 12 : Ratio(28), - }, - center: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - detailsInfoContainer: { - backgroundColor: 'rgba(0,0,0,0.2)', - width: isFactorTv ? '50%' : '90%', - padding: Ratio(30), - }, - detailsTitle: { - fontSize: isFactorMobile ? 22 : Ratio(42), - color: '#FFFFFF', - marginBottom: Ratio(20), - textAlign: 'center', - }, - }); - -export const ROUTES = { - HOME: isWebBased ? '/' : 'home', - MODAL: 'modal', - CAROUSELS: 'carousels', - DETAILS: 'details', -}; - -const lightStyleSheet = createStyleSheet(staticThemes.light); -const darkStyleSheet = createStyleSheet(staticThemes.dark); - -const themes = { - light: { - static: { ...staticThemes.light }, - styles: lightStyleSheet.styles, - ids: lightStyleSheet.ids, - }, - dark: { - static: { ...staticThemes.dark }, - styles: darkStyleSheet.styles, - ids: darkStyleSheet.ids, - }, -}; - -type ThemeContextType = { - theme: Theme; - dark: boolean; - toggle?: () => void; -}; - -export const ThemeContext = createContext({ theme: themes.dark, dark: true }); - -export function ThemeProvider({ children }) { - const [dark, setDark] = useState(false); - - const toggle = () => { - const isDark = !dark; - setDark(isDark); - }; - - const theme = dark ? themes.dark : themes.light; - - return {children}; -} - -export const themeStyles = themes.dark.styles; - -export { ICON_LOGO }; - -export default staticThemes.dark; -``` - -## `Menu` component - -On web and mobile our menu needs to be just a View, which wraps all the menu items, because we can utilize the default animation for drawer on mobile and on web we render the menu without any animation at all. Create `components/menu.tsx` file and paste the code below into it. - -```typescript -import React, { useContext, useState } from 'react'; -import { View, TouchableOpacity, Text } from '@flexn/create'; -import { testProps } from '../utils'; -import { isFactorBrowser } from '@rnv/renative'; -import Icon from 'react-native-vector-icons/Ionicons'; -import { ThemeContext, ROUTES } from '../config'; -import { useNavigate } from '../hooks'; - -export const DrawerButton = ({ navigation }: { navigation?: any }) => { - const { theme } = useContext(ThemeContext); - return ( - { - if (navigation && navigation.dispatch) navigation.dispatch({ type: 'OPEN_DRAWER' }); - }} - {...testProps('template-menu-open-drawer-button')} - > - - - ); -}; - -const Menu = ({ navigation }: { navigation?: any }) => { - const navigate = useNavigate({ navigation }); - const { theme } = useContext(ThemeContext); - const [burgerMenuOpen, setBurgerMenuOpen] = useState(false); - - const onPress = (route: string) => { - navigate(route); - setBurgerMenuOpen(false); - }; - - const renderMenuItems = () => ( - <> - onPress(ROUTES.HOME)} - style={theme.styles.menuButton} - {...testProps('template-menu-home-button')} - > - - Home - - onPress(ROUTES.CAROUSELS)} - style={theme.styles.menuButton} - {...testProps('template-menu-my-page-button')} - > - - Carousels - - onPress(ROUTES.MODAL)} - style={theme.styles.menuButton} - {...testProps('template-menu-my-modal-button')} - > - - My Modal - - - ); - - const renderMenu = () => { - if (isFactorBrowser) { - return ( - - {renderMenuItems()} - - ); - } - - return renderMenuItems(); - }; - - const renderBurgerButton = () => { - if (isFactorBrowser) { - return ( - setBurgerMenuOpen(!burgerMenuOpen)} - > - - - ); - } - - return null; - }; - - return ( - - {renderBurgerButton()} - {renderMenu()} - - ); -}; - -export default Menu; -``` - -We also make sure to render a burger button to open the drawer on mobile as this is usually the expected behavior for the end user. - -However, default animations and layout from react navigation don't work that well when it comes to handling focus events, therefore on TV platforms we will need to customize the menu even further. Create a `components/menu.tv.native.tsx` file and paste the code below into it. - -```typescript -import React, { useContext, useRef } from 'react'; -import { Animated } from 'react-native'; -import { TouchableOpacity, Text, Screen } from '@flexn/create'; -import { testProps } from '../utils'; -import Icon from 'react-native-vector-icons/Ionicons'; -import { ThemeContext, ROUTES, Ratio } from '../config'; -import { useNavigate } from '../hooks'; - -const AnimatedText = Animated.createAnimatedComponent(Text); - -const TRANSLATE_VAL_HIDDEN = Ratio(-300); - -const Menu = ({ navigation }) => { - const navigate = useNavigate({ navigation }); - const { theme } = useContext(ThemeContext); - - const translateBgAnim = useRef(new Animated.Value(TRANSLATE_VAL_HIDDEN)).current; - const opacityAnim = [ - useRef(new Animated.Value(0)).current, - useRef(new Animated.Value(0)).current, - useRef(new Animated.Value(0)).current, - ]; - const translateTextAnim = [ - useRef(new Animated.Value(TRANSLATE_VAL_HIDDEN)).current, - useRef(new Animated.Value(TRANSLATE_VAL_HIDDEN)).current, - useRef(new Animated.Value(TRANSLATE_VAL_HIDDEN)).current, - ]; - - const timing = (object: Animated.AnimatedValue, toValue: number, duration = 200): Animated.CompositeAnimation => { - return Animated.timing(object, { - toValue, - duration, - useNativeDriver: true, - }); - }; - - const onFocus = () => { - Animated.parallel([ - timing(translateBgAnim, 0), - timing(opacityAnim[0], 1, 800), - timing(opacityAnim[1], 1, 800), - timing(opacityAnim[2], 1, 800), - timing(translateTextAnim[0], 0), - timing(translateTextAnim[1], 0), - timing(translateTextAnim[2], 0), - ]).start(); - }; - - const onBlur = () => { - Animated.parallel([ - timing(translateBgAnim, TRANSLATE_VAL_HIDDEN), - timing(opacityAnim[0], 0, 100), - timing(opacityAnim[1], 0, 100), - timing(opacityAnim[2], 0, 100), - timing(translateTextAnim[0], TRANSLATE_VAL_HIDDEN), - timing(translateTextAnim[1], TRANSLATE_VAL_HIDDEN), - timing(translateTextAnim[2], TRANSLATE_VAL_HIDDEN), - ]).start(); - }; - - return ( - - - navigate(ROUTES.HOME)} - style={theme.styles.menuButton} - focusOptions={{ - forbiddenFocusDirections: ['up'], - }} - {...testProps('template-menu-home-button')} - > - - - Home - - - navigate(ROUTES.CAROUSELS)} - style={theme.styles.menuButton} - {...testProps('template-menu-my-page-button')} - > - - - Carousels - - - navigate(ROUTES.MODAL)} - style={theme.styles.menuButton} - focusOptions={{ - forbiddenFocusDirections: ['down'], - }} - {...testProps('template-menu-my-modal-button')} - > - - - My Modal - - - - ); -}; - -export default Menu; -``` diff --git a/docs/docs/tutorials/basics/quickstart.mdx b/docs/docs/tutorials/basics/example.mdx similarity index 87% rename from docs/docs/tutorials/basics/quickstart.mdx rename to docs/docs/tutorials/basics/example.mdx index f558f1f78..854161ed8 100644 --- a/docs/docs/tutorials/basics/quickstart.mdx +++ b/docs/docs/tutorials/basics/example.mdx @@ -1,9 +1,9 @@ import step1 from '/img/tutorial/step1.png'; import step5 from '/img/tutorial/step5.png'; -# Quickstart +# Example App -For quickstart, we will run the Flexn template (or you can build it yourself following the advanced tutorial) +It's optional step, but if you want to see how Flexn Create looks in action follow steps below. > Let's start by creating a new RNV project: diff --git a/docs/sidebars.js b/docs/sidebars.js index 7e657ecc7..3e5ae73e8 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -25,20 +25,8 @@ const sidebars = { }, { type: 'doc', - label: 'Quickstart', - id: 'tutorials/basics/quickstart', - }, - { - type: 'category', - label: 'Advanced Tutorial', - collapsed: false, - items: [ - 'tutorials/advanced/bootstrap', - 'tutorials/advanced/app', - 'tutorials/advanced/screens', - 'tutorials/advanced/navigation', - 'tutorials/advanced/ui', - ], + label: 'Example app', + id: 'tutorials/basics/example', }, { label: 'Components', diff --git a/docs/src/components/Features.tsx b/docs/src/components/Features.tsx index 74078ccdb..ed4ad6945 100644 --- a/docs/src/components/Features.tsx +++ b/docs/src/components/Features.tsx @@ -10,13 +10,6 @@ export default function HomepageFeatures(): JSX.Element { separator >
- - -Type: *({ index, item, separators, parentContext }) => JSX.Element | JSX.Element[] | null* - -Method which returns component to be rendered. It's important to note that `parentContext` always must be passed down to Pressable or TouchableOpacity component in your return. - -```javascript - { - return ; - }}} - /> -``` \ No newline at end of file diff --git a/docs/docs/components/pressable.mdx b/docs/docs/components/pressable.mdx index 86f5b714d..1cea72078 100644 --- a/docs/docs/components/pressable.mdx +++ b/docs/docs/components/pressable.mdx @@ -3,42 +3,43 @@ import Prop from '@site/src/components/Prop'; # Pressable -Pressable is one of [React Native core components](https://reactnative.dev/docs/pressable) but optimized to work on multiplatform environment. +Pressable is one of [React Native core components](https://reactnative.dev/docs/pressable) but optimized to work on multiplatform environment and has awareness of Focus Manager with set of optimized animations. ## Usage + ```javascript import * as React from 'react'; import { App, Screen, Text, Pressable, StyleSheet } from '@flexn/create'; const MyComponent = () => { - return ( - - - console.log('Pressed')}> - Press me - - - - ); + return ( + + + console.log('Pressed')}> + Press me + + + + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#FFFFFF', - }, - button: { - marginHorizontal: 20, - borderWidth: 2, - borderRadius: 25, - borderColor: '#111111', - height: 50, - width: 200, - justifyContent: 'center', - alignItems: 'center', - }, + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#FFFFFF', + }, + button: { + marginHorizontal: 20, + borderWidth: 2, + borderRadius: 25, + borderColor: '#111111', + height: 50, + width: 200, + justifyContent: 'center', + alignItems: 'center', + }, }); export default MyComponent; @@ -46,189 +47,173 @@ export default MyComponent; ## Props - +It inherits all the properties from [React Native Pressable](https://reactnative.dev/docs/pressable) and adds everything what's described below. + + +{' '} -Type: *boolean* +Type: _boolean_ Default value: `true` Property which tells Focus Manager if component should be included in focus engine finding next focusable element. ------------------------------------------------------------- +--- - + + +{' '} -Type: *ParentContext* +Type: _FocusContext_ This property allows Focus Manager to understand what's the structure of the screen. Usually Focus Manager iterates all the components -and passes down `parentContext` of the parent component to it's children. But if you have to created custom component you must pass it down by +and passes down `focusContext` of the parent component to it's children. But if you have to created custom component you must pass it down by yourself. ```javascript import * as React from 'react'; -import { App, Screen, Text, Pressable, StyleSheet } from '@flexn/create'; +import { App, Screen, Text, Pressable, StyleSheet, FocusContext } from '@flexn/create'; -const MyCustomComponent = ({ parentContext }: { parentContext?: any }) => { - return ( - console.log('Pressed')}> - Press me - - ); +const MyCustomComponent = ({ focusContext }: { focusContext?: FocusContext }) => { + return ( + console.log('Pressed')}> + Press me + + ); }; const MyComponent = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#FFFFFF', - }, - button: { - marginHorizontal: 20, - borderWidth: 2, - borderRadius: 25, - borderColor: '#111111', - height: 50, - width: 200, - justifyContent: 'center', - alignItems: 'center', - }, + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#FFFFFF', + }, + button: { + marginHorizontal: 20, + borderWidth: 2, + borderRadius: 25, + borderColor: '#111111', + height: 50, + width: 200, + justifyContent: 'center', + alignItems: 'center', + }, }); export default MyComponent; ``` ------------------------------------------------------------- - - +--- -Type: *() => void* + +{' '} -Event fired when Pressable was pressed. +Type: _PressableFocusOptions_ ------------------------------------------------------------- +Property which holds following related properties: - + -Type: *() => void* +Type: _ForbiddenFocusDirections[]_ -Event fired when Pressable gains focus. +Can contain one or more directions. When component is focused and direction is set an example to `right` then pressing right button on your remote will do nothing just keep focus as it is. ------------------------------------------------------------- + - +Type: _string_ -Type: *() => void* +An unique string which can be used to force focus on specific element by `focusKey`. -Event fired when Pressable loose focus. + ------------------------------------------------------------- +Type: _string_ | _string[]_ - +Forces next focus direction for component when user navigates up. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. -Type: *ViewFocusOptions* + -Property which holds following related properties: +Type: _string_ | _string[]_ - +Forces next focus direction for component when user navigates down. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + -Type: *ForbiddenFocusDirections[]* +Type: _string_ | _string[]_ -Can contain one or more directions. When component is focused and direction is set an example to `right` then pressing right button on your remote will do nothing just keep focus as it is. +Forces next focus direction for component when user navigates left. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. - + -Type: *string* +Type: _string_ | _string[]_ -An unique string which can be used to force focus on specific element by `focusKey`. +Forces next focus direction for component when user navigates right. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. - + -Type: *boolean* +Type: _boolean_ Forces element to gain focus once screen is loaded. - + -Type: *any* +Type: _Animator_ -Default value: `Scale` +Default value: `Scale animator` -Animator Options can define how your component will behave when it gains focus. It has multiple animation variations which can be controlled by following configurations: +Animator can define how your component will behave when it gains focus. It has multiple animation variations which can be controlled by following configurations: -- Scale +Type: _AnimatorScale_ ```javascript - +type: 'scale; +focus: { + scale?: number; + duration?: number; +}; ``` -- Scale with border +Type: _AnimatorScaleWithBorder_ ```javascript - - +type: 'scale_with_border'; +focus: { + scale?: number; + duration?: number; + borderWidth: number; + borderColor: ColorValue; + borderRadius?: number; +}; ``` -- Border + +Type: _AnimatorBorder_ ```javascript - +type: 'border'; +focus: { + borderWidth: number; + borderColor: string; + borderRadius?: number; + duration?: number; +}; ``` -- Background +Type: _AnimatorBackground_ ```javascript - +type: 'background'; +focus: { + backgroundColor: ColorValue; + duration?: number; +}; ``` From 7224fffeed8ec9018d92a738c11b4d5482e29f61 Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Fri, 23 Jun 2023 12:17:47 +0300 Subject: [PATCH 3/7] flash-list doc --- docs/docs/components/flash-list.mdx | 116 ++++++++++++++++ docs/docs/components/recyclable-list.mdx | 163 ----------------------- docs/sidebars.js | 2 +- 3 files changed, 117 insertions(+), 164 deletions(-) create mode 100644 docs/docs/components/flash-list.mdx delete mode 100644 docs/docs/components/recyclable-list.mdx diff --git a/docs/docs/components/flash-list.mdx b/docs/docs/components/flash-list.mdx new file mode 100644 index 000000000..3726427f8 --- /dev/null +++ b/docs/docs/components/flash-list.mdx @@ -0,0 +1,116 @@ +import Badge from '@site/src/components/Badge'; +import Prop from '@site/src/components/Prop'; + +# FlashList + +High performance list powered by [Shopify Flash List](https://github.com/Shopify/flash-list) designed to work with high amount of items. +Flash List reusing views that are no longer visible to render items instead of creating new view object. + +## Usage + +```javascript +import * as React from 'react'; +import { App, FlashList, Pressable, Image, CreateListRenderItemInfo } from '@flexn/create'; + +const MyComponent = () => { + const [data] = React.useState(generateData()); + + const rowRenderer = ({ item, focusRepeatContext }: CreateListRenderItemInfo) => { + return ( + + + + ); + }; + + return ( + + + + + + ); +}; + +export default MyComponent; +``` + +## Props + +It inherits all the properties from [Shopify Flash List](https://github.com/Shopify/flash-list). However this version +is optimized to work with multi platform environment and our Focus Manager so it has some API changes which is described below. + + +{' '} + +Type: _grid | row_ + +Describes type of list either is grid or row. It helps focus manager to navigate. + + +{' '} + +Type: _CreateListRenderItemInfo_ + +Method which returns component to be rendered. It's important to note that `focusRepeatContext`(last parameter of the function) always must be passed down to +Pressable or TouchableOpacity component in your return. + +```javascript +) => { + return ; + }} +/> +``` + + +{' '} + +Type: _RecyclerFocusOptions_ + +Property which holds following related properties: + + + +Type: _ForbiddenFocusDirections[]_ + +Can contain one or more directions. When Flash List has focus and direction is set an example to `down` then Flash List will never lose focus despite the fact we're pressing +down button on our remote. + + + +Type: _string_ | _string[]_ + +Forces next focus direction for component when user navigates up and end of up direction is reached in list. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + + + +Type: _string_ | _string[]_ + +Forces next focus direction for component when user navigates down and end of down direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + + + +Type: _string_ | _string[]_ + +Forces next focus direction for component when user navigates left and end of left direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + + + +Type: _string_ | _string[]_ + +Forces next focus direction for component when user navigates right and end of right direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + + diff --git a/docs/docs/components/recyclable-list.mdx b/docs/docs/components/recyclable-list.mdx deleted file mode 100644 index cc14117d5..000000000 --- a/docs/docs/components/recyclable-list.mdx +++ /dev/null @@ -1,163 +0,0 @@ -import Badge from '@site/src/components/Badge'; -import Prop from '@site/src/components/Prop'; - -# RecyclableList - -High performance list view designed to work with high amount of items. -RecyclableList reusing views that are no longer visible to render items instead of creating new view object. - -## Usage - -```javascript -import * as React from 'react'; -import { Dimensions } from 'react-native'; -import { - App, - Screen, - Image, - Text, - Pressable, - RecyclableList, - RecyclableListDataProvider, - RecyclableListLayoutProvider, - StyleSheet, -} from '@flexn/create'; - -const MARGIN_GUTTER = 20; -const ITEMS_IN_VIEWPORT = 3; - -const { width } = Dimensions.get('window'); - -const dimensions = { - layout: { width: width / ITEMS_IN_VIEWPORT, height: 270 }, - item: { width: width / ITEMS_IN_VIEWPORT - MARGIN_GUTTER, height: 250 }, -}; - -function generateData(items = 50) { - const data: { backgroundImage: string, title: string }[] = []; - for (let index = 0; index < items; index++) { - data.push({ - backgroundImage: `https://placekitten.com/${dimensions.item.width}/${dimensions.item.height + index}`, - title: 'Kitten name', - }); - } - - return data; -} - -const MyComponent = () => { - const [dataProvider] = React.useState( - new RecyclableListDataProvider((r1, r2) => r1 !== r2).cloneWithRows(generateData()) - ); - - const layoutProvider = React.useRef( - new RecyclableListLayoutProvider( - () => '_', - (_: string | number, dim: { width: number, height: number }) => { - dim.width = dimensions.layout.width; - dim.height = dimensions.layout.height; - } - ) - ).current; - - return ( - - - { - return ( - { - console.log('Pressed!'); - }} - > - - - {data.title} - - - ); - }} - horizontal - isHorizontal - style={styles.recycler} - scrollViewProps={{ - showsHorizontalScrollIndicator: false, - }} - /> - - - ); -}; - -const styles = StyleSheet.create({ - recyclerContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - recycler: { - width: '100%', - height: 270, - }, - recyclerItem: { - margin: 5, - justifyContent: 'flex-end', - alignItems: 'center', - }, - recyclerItemText: { - color: '#000000', - fontSize: 14, - }, -}); - -export default MyComponent; -``` - -## Props - -Since as core of RecyclableList Flexn Create using open source library, full API reference can be found at https://github.com/Flipkart/recyclerlistview#props. However this version -is optimized to work with multiplatform environment and our Focus Manager so it has some API changes which is described below. - - -{' '} - -Type: _(type: string | number, data: any, index: number, repeatContext: any) => JSX.Element | JSX.Element[] | null_ - -Method which returns component to be rendered. It's important to note that `repeatContext`(last parameter of the function) always must be passed down to -Pressable or TouchableOpacity component in your return. - -```javascript - { - return ; - }}} -/> -``` - - -{' '} - -Type: _RecyclerFocusOptions_ - -Property which holds following related properties: - - - -Type: _ForbiddenFocusDirections[]_ - -Can contain one or more directions. When RecyclableList has focus and direction is set an example to `down` then RecyclableList will never lose focus despite the fact we're pressing -down button on our remote. diff --git a/docs/sidebars.js b/docs/sidebars.js index 3e5ae73e8..bf49575dd 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -39,7 +39,7 @@ const sidebars = { 'components/image', 'components/modal', 'components/pressable', - 'components/recyclable-list', + 'components/flash-list', 'components/screen', 'components/scrollview', 'components/switch', From 90f649c080c12220987ab69f7c9021d04ec26796 Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Tue, 27 Jun 2023 11:30:56 +0300 Subject: [PATCH 4/7] chore: remove icon component --- docs/docs/components/icon.mdx | 100 ---------------------------------- docs/sidebars.js | 1 - 2 files changed, 101 deletions(-) delete mode 100644 docs/docs/components/icon.mdx diff --git a/docs/docs/components/icon.mdx b/docs/docs/components/icon.mdx deleted file mode 100644 index 2df590393..000000000 --- a/docs/docs/components/icon.mdx +++ /dev/null @@ -1,100 +0,0 @@ -import Badge from '@site/src/components/Badge'; -import Prop from '@site/src/components/Prop'; - -# Icon - -Simple component which renders icon and can be pressed. - -## Usage -```javascript -import * as React from 'react'; -import { App, Screen, Icon, StyleSheet } from '@flexn/create'; - -const MyComponent = () => { - return ( - - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#FFFFFF', - }, -}); - -export default MyComponent; -``` - -## Props - - - -Type: *string* - -Font of the icon which depends on what icon fonts are included, but can be one of the following: -- fontAwesome -- feather -- antDesign -- entypo -- evilIcons -- foundation -- ionicons -- materialIcons -- octicons -- simpleLineIcons -- zocial - ------------------------------------------------------------- - - - -Type: *string* - -Name of the icon of selected fonts. Available icon names can be found at https://oblador.github.io/react-native-vector-icons/ - ------------------------------------------------------------- - - - -Type: *string* - -Size of the icon. If it's not defined then it will use width or height of the icon as width. - ------------------------------------------------------------- - - - -Type: *string* - -Color of the icon - ------------------------------------------------------------- - - - -Type: *() => void* - -Event fired when icon is pressed. - ------------------------------------------------------------- - - - -Type: *StyleProp< TextStyle >* - -Styles of the Icon. - ------------------------------------------------------------- - - - -Type: *string* - -Used to locate this view in end-to-end tests. \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index bf49575dd..f6310a8ac 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -34,7 +34,6 @@ const sidebars = { items: [ 'components/activity-indicator', 'components/app', - 'components/icon', 'components/image-background', 'components/image', 'components/modal', From e629663c040ddb7ffb1e14a903f5b406b4524d48 Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Tue, 27 Jun 2023 14:55:48 +0300 Subject: [PATCH 5/7] screen docs --- docs/docs/components/flash-list.mdx | 6 +- docs/docs/components/screen.mdx | 106 ++++++++++++++-------------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/docs/docs/components/flash-list.mdx b/docs/docs/components/flash-list.mdx index 3726427f8..ee942a081 100644 --- a/docs/docs/components/flash-list.mdx +++ b/docs/docs/components/flash-list.mdx @@ -99,18 +99,18 @@ Forces next focus direction for component when user navigates up and end of up d Type: _string_ | _string[]_ -Forces next focus direction for component when user navigates down and end of down direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. +Forces next focus direction for component when user navigates down and end of down direction is reached in list. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. Type: _string_ | _string[]_ -Forces next focus direction for component when user navigates left and end of left direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. +Forces next focus direction for component when user navigates left and end of left direction is reached in list. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. Type: _string_ | _string[]_ -Forces next focus direction for component when user navigates right and end of right direction is reached in list.. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. +Forces next focus direction for component when user navigates right and end of right direction is reached in list. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. diff --git a/docs/docs/components/screen.mdx b/docs/docs/components/screen.mdx index 316cbdc43..6e692265b 100644 --- a/docs/docs/components/screen.mdx +++ b/docs/docs/components/screen.mdx @@ -49,102 +49,104 @@ Content of the Screen --- - -{' '} - -Type: _foreground | background_ + -Default value: `foreground` +Type: _StyleProp< ViewStyle >_ -If screen has `background` state it remains in the memory, but it's excluded from the Focus Manager engine. That means Focus Manager ignoring that screen when -trying to find next focusable element. +Styles for the View. --- - -{' '} - -Type: _number_ + +{' '} -Default value: `0` +Type: _() => void_ -Property which is very useful working with modals. You can have multiple `foreground` screens at the same time which overlapping each other, but once you define -`screenOrder` property Focus Manager searching for next focusable element only in those screens which has higher `screenOrder`. +Event fired when screen gains focus. --- - -{' '} - -Type: _boolean_ + +{' '} -Default value: `true` +Type: _() => void_ -If there are more screens on the singe page this property tells Focus Manager in which Screen context focus should be initially placed. +Event fired when screen lose focus. --- - + +{' '} -Type: _StyleProp< TextStyle >_ +Type: _ScreenFocusOptions_ -Styles for the View. +Property which holds following related properties: ---- + - -{' '} +Type: _foreground | background_ -Type: _() => void_ +Default value: `foreground` -Event fired when screen gains focus. +If screen has `background` state it remains in the memory, but it's excluded from the Focus Manager engine. That means Focus Manager ignoring that screen when +trying to find next focusable element. ---- + - -{' '} +Type: _number_ -Type: _() => void_ +Default value: `0` -Event fired when screen lose focus. +Property which is very useful working with modals. You can have multiple `foreground` screens at the same time which overlapping each other, but once you define +`screenOrder` property Focus Manager searching for next focusable element only in those screens which has higher `screenOrder`. ---- + - -{' '} +Type: _boolean_ -Type: _ScreenFocusOptions_ +Default value: `true` -Property which holds following related properties: +If there are more screens on the singe page this property tells Focus Manager in which Screen context focus should be initially placed. - + Type: _ForbiddenFocusDirections[]_ Can contain one or more directions. It prevents screen to lose focus when specific direction is defined and that direction remote control button is pressed. - + -Type: _string_ +Type: _string_ | _string[]_ -An unique string which can be used to force focus on specific foreground screen by `focusKey`. +Forces next focus direction for component when user navigates up and end of up direction is reached in screen. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. + + - +Type: _string_ | _string[]_ -Type: _both-edge | low-edge_ +Forces next focus direction for component when user navigates down and end of down direction is reached in screen. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. -Default value: `low-edge` + -Property which makes viewport of the screen horizontally align according to provided value. +Type: _string_ | _string[]_ -When `low-edge` is defined then viewport is allways keeping focused element at the top of the screen. +Forces next focus direction for component when user navigates left and end of left direction is reached in screen. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. -When `both-edge` is defined then focused element is kept more less within the top. + - +Type: _string_ | _string[]_ -Type: _both-edge | low-edge_ +Forces next focus direction for component when user navigates right and end of right direction is reached in screen. It accepts string with focus key or array with multiple focus keys. In that case first found is executed by focus engine. -Default value: `low-edge` + + +Type: _string_ + +An unique string which can be used to force focus on specific foreground screen by `focusKey`. + + + +Type: _number_ +Default value: `50` -Same as `focusOptions.horizontalWindowAlignment` but instead controlling horizontal alingment it affects vertical one. +Offset from focused element to the top of viewport From 33626f41153e6de7acdcb167f806d4bfff321c3a Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Tue, 27 Jun 2023 15:22:14 +0300 Subject: [PATCH 6/7] shrink guide doc --- docs/docs/guides/focus-manager.mdx | 91 +++--------------------------- 1 file changed, 8 insertions(+), 83 deletions(-) diff --git a/docs/docs/guides/focus-manager.mdx b/docs/docs/guides/focus-manager.mdx index fdc0c6406..219469e41 100644 --- a/docs/docs/guides/focus-manager.mdx +++ b/docs/docs/guides/focus-manager.mdx @@ -1,80 +1,5 @@ # Flexn Focus Manager -## Focus Manager in React Native - -Focus Manager is an algorithm for TV's which controls focus. Based on your application structure and remote controller actions decides what to focus next. -React Native doesn't have any specific Focus Manager implementation it utilizing native focus manager and -exports some API functions for React Native developers to help control some parts of it. - -## Why it doesn't work well - -First we need to understand that whole communication between Javascript and native code happening via React Native Bridge which is capable to connect these two worlds. -Native Focus Manager works in a way that whole decision making about what to focus next, what's initial focus and so on happens on native side and we as javascript developers have to obey -to those decisions. The problem is that those decisions are not always right because of various reasons like: - -- Each application has it own layout which sometimes can be very complex; -- Sometimes our application have items which is not perfectly aligned; -- We have different navigation paradigms in our apps; -- and so on... - -Therefore if we have real world application(something way more complex than "Hello world") we're start hitting the problem when native focus manager proving us wrong -decisions and focus start working not as we expecting... - -To have better control over focus manager Google added API which allows developers based on their applications force Focus Manager to do what it wants. In React Native that -works in a way that focusable item needs to provide reference to another focusable item. An example: - -```javascript -import * as React from 'react'; -import { findNodeHandle, View, TouchableOpacity } from 'react-native'; - -const MyComponent = () => { - const ref1 = React.useRef(); - const ref2 = React.useRef(); - const ref3 = React.useRef(); - - React.useEffect(() => { - ref1.current.setNativeProps({ - setNextFocusUp: findNodeHandle(ref2.current), - }); - ref2.current.setNativeProps({ - setNextFocusUp: findNodeHandle(ref3.current), - }); - ref3.current.setNativeProps({ - setNextFocusDown: findNodeHandle(ref2.current), - }); - }, []); - - return ( - - - - - - ); -}; - -export default MyComponent; -``` - -So in this example for Native Focus Manager we're saying that once first TouchableOpacity has focus gained his next up direction -will be second TouchableOpacity, second TouchableOpacity on up command will focus last one and last on once down button is pressed will move focus back to 2nd TouchableOpacity. -Well it looks like we have some control, but it has some fundamental issues like: - -- We are communicating with native code and React Native bridge has delay, so usually these focus enforcements makes focus manager do 2 actions: first native default one and second - our which we wrote in javascript which eventually it can make very bad user experience; -- Having complex apps which has a lot of different components and screens managing these references become tremendous work which also makes our code base very fragile; -- This focus enforcement was working on Android TV only, before React Native cut down support for TV's at all and [this library](https://github.com/react-native-tvos/react-native-tvos) was - created which added support for tvOS also, but it works in slightly different way. - -## Flexn Focus Manager - -With Flexn Focus Manager we took different approach. After lot of discussions and investigation what we can do that javascript and native code communication regards to focus management -could be reliable enough and solve fundamental problems described above we came to the solution that we should remove native part from equation and turn all decision making to Javascript -where we have best control over our applications. - -We still utilizing the best of native and created components library which puts all heavy operations and requires best code performance -like focus animations to native code. Having this combination in mind we managed to create Flexn Focus Manager which finally works! - ## How to use it Nevertheless Flexn Focus Manager is simple enough it has some fundamental rules which we need to comply to use it right. All primitive components must be exported from `@flexn/create` since @@ -129,7 +54,7 @@ const Screen1 = ({ navigation }) => { ); return ( - + navigation.navigate('myScreen2')}> Navigate to screen 2 @@ -153,7 +78,7 @@ const Screen2 = ({ navigation }) => { ); return ( - + navigation.navigate('myScreen1')}> Back to screen 1 @@ -226,7 +151,7 @@ const MyApp = () => { const renderModal = () => { if (modalOpen) { return ( - + Hello from Flexn Create @@ -298,11 +223,11 @@ this needs to be done by manually(which is very easy): ```javascript import * as React from 'react'; -import { App, Screen, Text, Pressable, StyleSheet } from '@flexn/create'; +import { App, Screen, Text, Pressable, StyleSheet, FocusContext } from '@flexn/create'; -const MyCustomComponent = ({ parentContext }: { parentContext?: any }) => { +const MyCustomComponent = ({ focusContext }: { focusContext?: FocusContext }) => { return ( - + Hello from Flexn Create ); @@ -341,7 +266,7 @@ const styles = StyleSheet.create({ export default MyApp; ``` -As you can see in this example we are creating our custom component called `MyCustomComponent` which by default inherits special property called `parentContext` which needs +As you can see in this example we are creating our custom component called `MyCustomComponent` which by default inherits special property called `focusContext` which needs to be passed down to your parent component of return function. After doing that Flexn Focus Manager will take care of the rest. -**IMPORTANT: do not create property for any of your custom component called `parentContext` it's reserved by Flexn Focus Manager.** +**IMPORTANT: do not create property for any of your custom component called `focusContext` it's reserved by Flexn Focus Manager.** From c69b571eb3f88c0c03d95596600abe6534283331 Mon Sep 17 00:00:00 2001 From: aurimasmi Date: Tue, 27 Jun 2023 15:24:41 +0300 Subject: [PATCH 7/7] fix guide doc --- docs/docs/guides/focus-manager.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/focus-manager.mdx b/docs/docs/guides/focus-manager.mdx index 219469e41..bc756aeec 100644 --- a/docs/docs/guides/focus-manager.mdx +++ b/docs/docs/guides/focus-manager.mdx @@ -27,7 +27,7 @@ Screen represents collection of the children's which belongs only for particular has to be like that. With Screen you can create things like modals which overlaps the context, side navigation which typically always visible for user whatever you navigate or anything else depends on your application structure. There are few important rules working with screens: -- You can never wrap screen into another screen. That doesn't work; +- Don't wrap screen into another screen. That might cause side effects and break functionality; - Screen must have states. More about those below; State of the screen tells focus manager whenever your screen is in foreground and visible for user or it's moved to background. Example bellow shows simple implementation with