diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..65365be --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 3011538..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: ['@react-native-community'], -}; diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index 647d44b..0000000 --- a/.flowconfig +++ /dev/null @@ -1,76 +0,0 @@ -[ignore] -; We fork some components by platform -.*/*[.]android.js - - -; Ignore "BUCK" generated dirs -/\.buckd/ - -; Ignore polyfills -node_modules/react-native/Libraries/polyfills/.* - -; These should not be required directly -; require from fbjs/lib instead: require('fbjs/lib/warning') -node_modules/warning/.* - -; Flow doesn't support platforms -.*/Libraries/Utilities/LoadingView.js - -[untyped] -.*/node_modules/@react-native-community/cli/.*/.* - -; menu has a bunch of errors but we want to keep flow syntax in there -.*/src/overflowMenu/vendor/Menu.js - -[include] - -[libs] -node_modules/react-native/interface.js -node_modules/react-native/flow/ - -[options] -emoji=true - -esproposal.optional_chaining=enable -esproposal.nullish_coalescing=enable - -exact_by_default=true - -module.file_ext=.js -module.file_ext=.json -module.file_ext=.ios.js - -munge_underscores=true - -module.name_mapper='^react-native/\(.*\)$' -> '/node_modules/react-native/\1' -module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' - -suppress_type=$FlowIssue -suppress_type=$FlowFixMe -suppress_type=$FlowFixMeProps -suppress_type=$FlowFixMeState - - -[lints] -sketchy-null-number=warn -sketchy-null-mixed=warn -sketchy-number=warn -untyped-type-import=warn -nonstrict-import=warn -deprecated-type=warn -unsafe-getters-setters=warn -unnecessary-invariant=warn -signature-verification-failure=warn -deprecated-utility=error - -[strict] -deprecated-type -nonstrict-import -sketchy-null -unclear-type -unsafe-getters-setters -untyped-import -untyped-type-import - -[version] -^0.137.0 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..030ef14 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.pbxproj -text +# specific for windows script files +*.bat text eol=crlf \ No newline at end of file diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..513993c --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,30 @@ +name: Setup +description: Setup Node.js and install dependencies + +runs: + using: composite + steps: + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: .nvmrc + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + shell: bash + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v3 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock', '**/package.json') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + if: steps.yarn-cache.outputs.cache-hit != 'true' + run: | + yarn install --cwd example --frozen-lockfile + yarn install --frozen-lockfile + shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5d8c2a4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Lint files + run: yarn lint + + - name: Typecheck files + run: yarn typecheck + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run unit tests + run: yarn test --maxWorkers=2 --coverage + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Build package + run: yarn prepack diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml deleted file mode 100644 index 780223b..0000000 --- a/.github/workflows/nodejs.yml +++ /dev/null @@ -1,32 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Node.js CI - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [15.x] - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - - run: yarn install --frozen-lockfile - - run: yarn flow - - run: yarn lint - - run: yarn test - env: - CI: true diff --git a/.gitignore b/.gitignore index b148d5b..7535671 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,70 @@ +# OSX +# +.DS_Store + +# XDE +.expo/ + # VSCode .vscode/ jsconfig.json -# IntelliJ/Webstorm +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle .idea +.project +.settings +local.properties +android.iml -# NodeJS +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ npm-debug.log -node_modules -dist +yarn-debug.log yarn-error.log -# OS X -.DS_Store +# BUCK +buck-out/ +\.buckd/ +android/app/libs +android/keystores/debug.keystore + +# Expo +.expo/ -# Jest -coverage +# Turborepo +.turbo/ -# expo -.expo/packager-info.json -.expo/* +# generated by bob +lib/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..5397c87 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16.18.1 diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..fedc0f1 --- /dev/null +++ b/.yarnrc @@ -0,0 +1,3 @@ +# Override Yarn command so we can automatically setup the repo on running `yarn` + +yarn-path "scripts/bootstrap.js" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45d257b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..43c933c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing + +Contributions are always welcome, no matter how large or small! + +We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). + +## Development workflow + +To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: + +```sh +yarn +``` + +> While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`yarn`](https://classic.yarnpkg.com/), so you'll have an easier time if you use `yarn` for development. + +While developing, you can run the [example app](/example/) to test your changes. Any changes you make in your library's JavaScript code will be reflected in the example app without a rebuild. If you change any native code, then you'll need to rebuild the example app. + +To start the packager: + +```sh +yarn example start +``` + +To run the example app on Android: + +```sh +yarn example android +``` + +To run the example app on iOS: + +```sh +yarn example ios +``` + +To run the example app on Web: + +```sh +yarn example web +``` + +Make sure your code passes TypeScript and ESLint. Run the following to verify: + +```sh +yarn typecheck +yarn lint +``` + +To fix formatting errors, run the following: + +```sh +yarn lint --fix +``` + +Remember to add tests for your change if possible. Run the unit tests by: + +```sh +yarn test +``` + + +### Commit message convention + +We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: + +- `fix`: bug fixes, e.g. fix crash due to deprecated method. +- `feat`: new features, e.g. add new method to the module. +- `refactor`: code refactor, e.g. migrate from class components to hooks. +- `docs`: changes into documentation, e.g. add usage example for the module.. +- `test`: adding or updating tests, e.g. add integration tests using detox. +- `chore`: tooling changes, e.g. change CI config. + +Our pre-commit hooks verify that your commit message matches this format when committing. + +### Linting and tests + +[ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) + +We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. + +Our pre-commit hooks verify that the linter and tests pass when committing. + +### Publishing to npm + +We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. + +To publish new versions, run the following: + +```sh +yarn release +``` + +### Scripts + +The `package.json` file contains various scripts for common tasks: + +- `yarn bootstrap`: setup project by installing all dependencies and pods. +- `yarn typecheck`: type-check files with TypeScript. +- `yarn lint`: lint files with ESLint. +- `yarn test`: run unit tests with Jest. +- `yarn example start`: start the Metro server for the example app. +- `yarn example android`: run the example app on Android. +- `yarn example ios`: run the example app on iOS. + +### Sending a pull request + +> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). + +When you're sending a pull request: + +- Prefer small pull requests focused on one change. +- Verify that linters and tests are passing. +- Review the documentation to make sure it looks good. +- Follow the pull request template when opening a pull request. +- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. diff --git a/LICENSE b/LICENSE index 24a751c..a0fdb00 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ MIT License -Copyright (c) 2018 Vojtech Novak - +Copyright (c) 2023 Vojtech Novak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/README.md b/README.md index 6369956..5103186 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,29 @@ -## react-navigation-header-buttons +# react-navigation-header-buttons -This package will help you render buttons in the navigation bar and handle the styling so you don't have to. It tries to mimic the appearance of native navbar buttons and attempts to offer simple and flexible interface for you to interact with. Typed with Flow and ships with TS typings. Supports iOS and Android, web support is experimental. +This package will help you render buttons in the navigation bar and handle the styling so you don't have to. It mimics the appearance of native navbar buttons and offers a simple and flexible interface for you to interact with. + +- DRY library api +- Works great with icons from `@expo/vector-icons` / `react-native-vector-icons` or any other icon library +- Supports both [JS](https://reactnavigation.org/docs/stack-navigator) and [native](https://reactnavigation.org/docs/native-stack-navigator/) stack +- Beautiful overflow menus for items that don't fit into the navbar +- [Recipes](#recipes) and examples included +- Written in TS #### Demo App -Contains many examples and is [available via expo](https://expo.io/@vonovak/navbar-buttons-demo). Sources are in the [example folder](https://github.com/vonovak/react-navigation-header-buttons/tree/master/example/screens). I highly recommend you check out both links to get a better idea of the api. +Contains many examples in the [example folder](https://github.com/vonovak/react-navigation-header-buttons/tree/master/example/screens). I highly recommend you check it out to get a better idea of the api. #### Quick Example - - +demo The corresponding code: -```js +```tsx import React from 'react'; -import { Ionicons } from '@expo/vector-icons'; +import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import { Text } from 'react-native'; import { HeaderButtons, @@ -25,32 +31,40 @@ import { Item, HiddenItem, OverflowMenu, + HeaderButtonProps, } from 'react-navigation-header-buttons'; -const IoniconsHeaderButton = (props) => ( +const MaterialHeaderButton = (props: HeaderButtonProps) => ( // the `props` here come from // you may access them and pass something else to `HeaderButton` if you like - + ); const ReusableItem = ({ onPress }) => ; -const ReusableHiddenItem = ({ onPress }) => ; +const ReusableHiddenItem = ({ onPress }) => ( + +); export function UsageWithIcons({ navigation }) { - React.useLayoutEffect(() => { + useLayoutEffect(() => { navigation.setOptions({ - // in your app, you can extract the arrow function into a separate component - // to avoid creating a new one every time you update the options + title: 'Demo', headerRight: () => ( - - alert('search')} /> - alert('Edit')} /> + + alert('Edit')} /> + alert('search')} + /> } + OverflowIcon={({ color }) => ( + + )} > alert('hidden1')} /> + alert('hidden2')} /> @@ -58,86 +72,90 @@ export function UsageWithIcons({ navigation }) { }); }, [navigation]); - return demo!; + return demo!; } ``` -#### Setup +## Setup -1. `yarn add react-navigation-header-buttons` +Version >= 11 requires React Native 0.71 / Expo 48 or newer. Use version 10 if you're on older version of RN / Expo. -2. Wrap your root component in `OverflowMenuProvider`, as seen in [example's App.tsx](https://github.com/vonovak/react-navigation-header-buttons/tree/master/example/App.tsx). **IMPORTANT** `OverflowMenuProvider` must be placed so that it is a child of `NavigationContainer`, otherwise this library may not receive the correct theme from React Navigation. +1. `yarn add react-navigation-header-buttons` -#### Note on theming +2. Wrap your root component in `HeaderButtonsProvider` and pass the `stackType` prop (`'native' | 'js'`), as seen in [example's App.tsx](https://github.com/vonovak/react-navigation-header-buttons/tree/master/example/App.tsx). -Version 7 and later gets colors for Android ripple effect, text and icons from [React Navigation's theme](https://reactnavigation.org/docs/themes/), so you will not need to work with colors, with the exception of `OverflowIcon` as seen above. You can always override colors of text&icons (using `color` prop) or of the ripple effect on Android (using `pressColor` prop) as documented below. +**IMPORTANT** `HeaderButtonsProvider` must be placed so that it is a child of `NavigationContainer`, otherwise this library may not receive the correct theme from React Navigation. -### Usage +## Usage -#### `HeaderButtons` +### `HeaderButtons` Is a wrapper over all the visible header buttons (those can be text-buttons, icon-button, or any custom react elements). -The most important prop is `HeaderButtonComponent` that defines how all icons rendered in children will look. -In particular, it allows setting their `IconComponent`, `size` and `color` once so that you don't need to repeat it for each icon-button - but you can override those for each `Item` if you like. + +You should provide the `HeaderButtonComponent` prop that encapsulates how all buttons rendered in children will look. Typically, you'll want to provide a component that wraps [`HeaderButton`](#headerbutton) as seen in the example. However, you're free to use your own component (see `HeaderButton.tsx` for reference). + +In particular, it allows setting `IconComponent`, `size` and `color` once so that you don't need to repeat it for each icon-button - but you can override those for each `Item` if you like. `HeaderButtons` accepts: -| prop and type | description | note | -| ------------------------------------------------ | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| HeaderButtonComponent?: React.ComponentType | component that renders the buttons, `HeaderButton` by default | Typically, you'll want to provide a component that wraps `HeaderButton` provided by this package, as seen in the [quick example](#quick-example). However, you're free to use your own component (see `HeaderButton.js` for reference). | -| children: React.Node | whatever you want to render inside | Typically, `Item` or your component that renders `Item`, but it can be any React element. | -| left?: boolean | whether the `HeaderButtons` are on the left from header title | false by default, it only influences styling in a subtle way | +| prop and type | description | note | +| ---------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| HeaderButtonComponent?: `ComponentType` | component that renders the buttons, `HeaderButton` by default | Typically, you'll want to provide a component that wraps `HeaderButton` provided by this package, as seen in the [quick example](#quick-example). | +| children: ReactNode | whatever you want to render inside | Typically, `Item` or your component that renders `Item`, but it can be any React element. | +| left?: boolean | whether the `HeaderButtons` are on the left from header title | false by default, it only influences styling in a subtle way | -#### `Item` +### `Item` -Renders text, or icon, and has an `onPress` handler. Take a look at the example to see how to use it. +Renders text, or icon inside a [PlatformPressable](https://reactnavigation.org/docs/elements/#platformpressable). Take a look at the example to see how to use it. `Item` accepts: -| prop and type | description | -| --------------------------- | --------------------------------------------------------------------------- | -| title: string | title for the button, required | -| onPress: ?() => any | function to call on press | -| iconName?: string | icon name, used together with the `IconComponent` prop | -| style?: ViewStyleProp | style to apply to the touchable element that wraps the button | -| buttonStyle?: ViewStyleProp | style to apply to the text / icon | -| testID?: string | testID to locate view in e2e tests | -| other props | whatever else you want to pass to the underlying touchable (eg. `disabled`) | +| prop and type | description | +| ----------------------- | ------------------------------------------------------------------------------------- | +| title: string | title for the button, required | +| onPress: ?() => any | function to call on press | +| iconName?: string | icon name, used together with the `IconComponent` prop | +| style?: ViewStyle | style to apply to the touchable element that wraps the button | +| buttonStyle?: ViewStyle | style to apply to the text / icon | +| other props | whatever else you want to pass to the underlying `PlatformPressable` (eg. `disabled`) | `Item` also accepts other props that you'll typically not need to pass because `HeaderButtonComponent` already knows them (eg. `iconSize`) or because they are pulled from the React Navigation's theme object (`color`). -| additional props and type | description | note | -| ---------------------------------------- | ---------------------------------------------------------------------------- | ---- | -| IconComponent?: React.ComponentType | component to use for the icons, for example from `react-native-vector-icons` | | -| iconSize?: number | iconSize | | -| color?: string | color of icons and buttons | | +| additional props and type | description | note | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---- | +| IconComponent?: ComponentType | component to use for the icons, for example from `react-native-vector-icons` | | +| iconSize?: number | iconSize | | +| color?: string | color of icons and buttons | | +| renderButton?: (params: VisibleButtonProps) => ReactNode | renders the body of the button (text or icon), defaults to `defaultRenderVisibleButton` | | -#### `OverflowMenu` +### `OverflowMenu` -Is the place to define the behavior for overflow button (if there is one). Please note you can render `OverflowMenu` only by itself too, you do not need to wrap it in `HeaderButtons`. +Defines the behavior for overflow button (if there is one). Please note you can render `OverflowMenu` only by itself too, you do not need to wrap it in `HeaderButtons`. The most important prop is `onPress` which defines what kind of overflow menu we should show. The package exports common handlers you can use, but you can provide your own too (via the `onPress` prop): -| exported handler | description | -| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `overflowMenuPressHandlerActionSheet` | This is iOS-only: it displays overflow items in an `ActionSheetIOS` | -| `overflowMenuPressHandlerPopupMenu` | This is Android-only: it displays overflow items using `UIManager.showPopupMenu` | -| `overflowMenuPressHandlerDropdownMenu` | Can be used in iOS, Android and Web. Displays overflow items in a material popup adapted from [react-native-paper](https://callstack.github.io/react-native-paper/menu.html), credit for amazing job goes to them. This `Menu` is bundled in this library (no dependency on `react-native-paper`). | -| `defaultOnOverflowMenuPress` | The default. Uses `overflowMenuPressHandlerActionSheet` on iOS, and `overflowMenuPressHandlerDropdownMenu` otherwise. | +| exported handler | description | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `overflowMenuPressHandlerActionSheet` | This is iOS-only: it displays overflow items in an `ActionSheetIOS` | +| `overflowMenuPressHandlerPopupMenu` | This is Android-only: it displays overflow items using `UIManager.showPopupMenu` | +| `overflowMenuPressHandlerDropdownMenu` | Can be used in iOS, Android and Web. Displays overflow items in a material popup adapted from [react-native-paper](https://callstack.github.io/react-native-paper/menu.html), credit for an amazing job goes to them. This `Menu` is bundled in this library (no dependency on `react-native-paper`). | +| `defaultOnOverflowMenuPress` | The default. Uses `overflowMenuPressHandlerActionSheet` on iOS, and `overflowMenuPressHandlerDropdownMenu` otherwise. | + +You can also use the [react-native-menu](https://github.com/react-native-menu/menu) to show the overflow menu, as seen in the example app. `OverflowMenu` accepts: -| prop and type | description | note | -| ------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -| OverflowIcon: React.Element \| React.ComponentType | React element or component for the overflow icon | if you provide a component, it will receive `color` prop as seen in example above | -| style?: ViewStyleProp | optional styles for overflow button | there are some default styles set, as seen in `OverflowButton.js` | -| onPress?: (OnOverflowMenuPressParams) => any | function that is called when overflow menu is pressed. | This will override the default handler. Note the default handler offers (limited) customization. See more in "Recipes". | -| testID?: string | testID to locate the overflow button in e2e tests | the default is available under `import { OVERFLOW_BUTTON_TEST_ID } from 'react-navigation-header-buttons/e2e'` | -| accessibilityLabel?: string | | 'More options' by default | -| left?: boolean | whether the `OverflowMenu` is on the left from header title | false by default, it just influences styling. No need to pass this if you already passed it to `HeaderButtons`. | -| children: React.Node | the overflow items | typically `HiddenItem`s, please read the note below | -| other props | props passed to the nested Touchable | pass eg. `pressColor` to control ripple color on Android | +| prop and type | description | note | +| -------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| OverflowIcon: ReactElement \| ComponentType | React element or component for the overflow icon | if you provide a component, it will receive `color` prop as seen in example above | +| style?: ViewStyle | optional styles for overflow button | there are some default styles set, as seen in `OverflowButton.js` | +| onPress?: (OnOverflowMenuPressParams) => any | function that is called when overflow menu is pressed. | This will override the default handler. Note the default handler offers (limited) customization. See more in "Recipes". | +| testID?: string | testID to locate the overflow button in e2e tests | the default is available under `import { OVERFLOW_BUTTON_TEST_ID } from 'react-navigation-header-buttons/e2e'` | +| accessibilityLabel?: string | | 'More options' by default | +| left?: boolean | whether the `OverflowMenu` is on the left from header title | false by default, it just influences styling. No need to pass this if you already passed it to `HeaderButtons`. | +| children: ReactNode | the overflow items | typically `HiddenItem`s, please read the note below | +| other props | props passed to the nested `PlatformPressable` | pass eg. `pressColor` to control ripple color on Android | ##### Important note @@ -167,10 +185,14 @@ These will NOT work with `overflowMenuPressHandlerActionSheet` and `overflowMenu ```js function MyComponent({ title }) { const [titleFromState, setTitle] = React.useState('from state hook'); - return alert('fail')} />; + return ( + alert('fail')} /> + ); } -}> +} +> ; ``` @@ -193,29 +215,29 @@ const HiddenItemWrappedTwice = ()=> `HiddenItem` accepts: -| prop and type | description | note | -| -------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | -| title: string | title for the button, required | | -| style?: ViewStyleProp | style to apply to the touchable element that wraps the text | | -| titleStyle?: ViewStyleProp | style to apply to the text | | -| onPress: ?() => any | function to call on press | | -| testID?: string | testID to locate view in e2e tests | | -| disabled?: boolean | disabled 'item' is greyed out and `onPress` is not called on touch | | -| destructive?: boolean | flag specifying whether this item is destructive | only applies to items shown with `overflowMenuPressHandlerActionSheet` | +| prop and type | description | note | +| ---------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------- | +| title: string | title for the button, required | | +| style?: ViewStyle | style to apply to the touchable element that wraps the text | | +| titleStyle?: ViewStyle | style to apply to the text | | +| onPress: ?() => any | function to call on press | | +| testID?: string | testID to locate view in e2e tests | | +| disabled?: boolean | disabled 'item' is greyed out and `onPress` is not called on touch | | +| destructive?: boolean | flag specifying whether this item is destructive | only applies to items shown with `overflowMenuPressHandlerActionSheet` | + +### `HeaderButtonsProvider` -#### `OverflowMenuProvider` +You need to wrap your root component with ``. `stackType` is a required prop, which indicates whether you're using a native or JS stack. -This is a React context provider needed for `overflowMenuPressHandlerDropdownMenu` to work. If you're not using `overflowMenuPressHandlerDropdownMenu` then you don't need it. -By default, you need to wrap your root component with it. +Optional `spaceAboveMenu` prop can be used to set the distance between the top of the screen and the top of the overflow menu. -`OverflowMenuProvider` accepts an optional `spaceAboveMenu` prop, which can be used to set the distance between the top of the screen and the top of the overflow menu. +### `HeaderButton` -#### `HeaderButton` +`HeaderButton` is where all the `onPress`, `title` and Icon-related props (color, size) meet to render actual button. -You will typically not use `HeaderButton` directly. `HeaderButton` is where all the `onPress`, `title` and Icon-related props (color, size) meet to render actual button. -See the source if you want to customize it. +You can fully customize what it renders inside of the `PlatformPressable` using the `renderButton?: (params: VisibleButtonProps) => ReactNode` prop. -### Recipes +## Recipes #### Customizing the overflow menu @@ -223,6 +245,8 @@ The default handler for overflow menu on iOS is `overflowMenuPressHandlerActionS One of the usual things you may want to do is override the cancel button label on iOS - see [example](example/screens/UsageWithOverflow.tsx). +You can also use the [react-native-menu](https://github.com/react-native-menu/menu) to show the overflow menu, as seen in the example app. + #### Using custom text transforms Use the `buttonStyle` prop to set [`textTransform`](https://reactnative.dev/docs/text-style-props#texttransform) styles for button titles. @@ -242,11 +266,18 @@ import { HeaderButtons, HeaderButton } from 'react-navigation-header-buttons'; // define IconComponent, color, sizes and OverflowIcon in one place const MaterialHeaderButton = (props) => ( - + ); export const MaterialHeaderButtons = (props) => { - return ; + return ( + + ); }; ``` @@ -263,7 +294,11 @@ React.useLayoutEffect(() => { // use MaterialHeaderButtons with consistent styling across your app headerRight: () => ( - console.warn('add')} /> + console.warn('add')} + /> console.warn('edit')} /> ), @@ -271,10 +306,14 @@ React.useLayoutEffect(() => { }, [navigation]); ``` +### Theming + +Colors for Android ripple effect, text and icons come from [React Navigation's theme](https://reactnavigation.org/docs/themes/), so you do not need to work with colors, with the exception of `OverflowIcon`. You can always override colors of text&icons (using `color` prop) or of the ripple effect on Android (using `pressColor` prop) as [documented](#item). + ### Known issues - it appears that when screen title is long, it might interfere with buttons (does not happen when using native stack). This is more probably a react-navigation error, but needs investigation. -- TS typings need improvement, plus I'd like to check their validity via the example project which is using TS. Please get in touch if you wanna help. - missing styling support for material dropdown menu - item margins need to be reviewed and polished; don't hesitate to contribute - [this](https://github.com/infinitered/reactotron/blob/master/docs/plugin-overlay.md) should help - RTL is not tested +- web support is experimental diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..f842b77 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'], +}; diff --git a/example/.expo-shared/assets.json b/example/.expo-shared/assets.json deleted file mode 100644 index 17ad228..0000000 --- a/example/.expo-shared/assets.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, - "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true -} \ No newline at end of file diff --git a/example/.expo/settings.json b/example/.expo/settings.json deleted file mode 100644 index f580aba..0000000 --- a/example/.expo/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "hostType": "lan", - "lanType": "ip", - "dev": true, - "minify": false, - "urlRandomness": "7k-e6f", - "https": false, - "scheme": null, - "devClient": false -} diff --git a/example/.gitignore b/example/.gitignore index c409cf6..c87d03a 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,5 +1,7 @@ -node_modules/**/* -.expo/* +.envrc +node_modules/ +.expo/ +dist/ npm-debug.* *.jks *.p8 @@ -8,7 +10,62 @@ npm-debug.* *.mobileprovision *.orig.* web-build/ -web-report/ + # macOS .DS_Store + +# @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 +# The following patterns were generated by expo-cli + +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +.cxx/ +*.keystore +!debug.keystore + +# node.js +# +npm-debug.log +yarn-error.log + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/ios/Pods/ + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + +# @end expo-cli diff --git a/example/App.js b/example/App.js new file mode 100644 index 0000000..08d43a1 --- /dev/null +++ b/example/App.js @@ -0,0 +1,3 @@ +import 'react-native-gesture-handler'; + +export { default } from './src/App'; diff --git a/example/App.tsx b/example/App.tsx deleted file mode 100644 index e150095..0000000 --- a/example/App.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import 'react-native-gesture-handler'; -import { enableScreens } from 'react-native-screens'; - -enableScreens(); -import { - UsageCustom, - UsageWithIcons, - UsageWithOverflow, - UsageLeft, - UsageDisabled, - UsageDifferentFontFamilies, - HomeScreen, - UsageWithCustomOverflow, - UsageWithOverflowComplex, -} from './screens'; -import React, { useContext } from 'react'; -import { NavigationContainer } from '@react-navigation/native'; -import { OverflowMenuProvider } from 'react-navigation-header-buttons'; -// just for custom overflow menu onPress action -import { ActionSheetProvider } from '@expo/react-native-action-sheet'; -import { StatusBar } from 'expo-status-bar'; -import { createStackNavigator } from '@react-navigation/stack'; -import { ThemeContext, ThemeProvider } from './ThemeProvider'; -// import { createNativeStackNavigator as createStackNavigator } from 'react-native-screens/native-stack'; - -const screens = { - HomeScreen, - UsageWithIcons, - UsageWithOverflowComplex, - UsageLeft, - UsageCustom, - UsageDisabled, - UsageWithOverflow, - UsageDifferentFontFamilies, - UsageWithCustomOverflow, -}; - -const Stack = createStackNavigator(); - -const Body = () => { - // console.warn('render'); - return ( - <> - - - {Object.keys(screens).map((screenName) => { - return ( - - ); - })} - - - ); -}; - -const ThemedApp = () => { - const { theme } = useContext(ThemeContext); - return ( - - - - - - - - ); -}; - -export default function App() { - return ( - - - - ); -} diff --git a/example/app.json b/example/app.json index a3c9c63..2f782fb 100644 --- a/example/app.json +++ b/example/app.json @@ -2,28 +2,31 @@ "expo": { "name": "navbar-buttons-demo", "slug": "navbar-buttons-demo", - "privacy": "public", - "platforms": [ - "ios", - "android", - "web" - ], "version": "1.0.0", "orientation": "default", "icon": "./assets/icon.png", + "userInterfaceStyle": "automatic", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "updates": { - "fallbackToCacheTimeout": 0 - }, "assetBundlePatterns": [ "**/*" ], "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.vonovak.navbar-buttons-demo" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "com.vonovak.navbarbuttonsdemo" + }, + "web": { + "favicon": "./assets/favicon.png" } } } diff --git a/example/assets/adaptive-icon.png b/example/assets/adaptive-icon.png new file mode 100644 index 0000000..03d6f6b Binary files /dev/null and b/example/assets/adaptive-icon.png differ diff --git a/example/assets/favicon.png b/example/assets/favicon.png new file mode 100644 index 0000000..e75f697 Binary files /dev/null and b/example/assets/favicon.png differ diff --git a/example/assets/icon.png b/example/assets/icon.png index 7f5e01c..a0b1526 100644 Binary files a/example/assets/icon.png and b/example/assets/icon.png differ diff --git a/example/assets/splash.png b/example/assets/splash.png index 4f9ade6..0e89705 100644 Binary files a/example/assets/splash.png and b/example/assets/splash.png differ diff --git a/example/babel.config.js b/example/babel.config.js index 2900afe..b85e43d 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,6 +1,22 @@ -module.exports = function(api) { +const path = require('path'); +const pak = require('../package.json'); + +module.exports = function (api) { api.cache(true); + return { presets: ['babel-preset-expo'], + plugins: [ + [ + 'module-resolver', + { + extensions: ['.tsx', '.ts', '.js', '.json'], + alias: { + // For development, we want to alias the library to the source + [pak.name]: path.join(__dirname, '..', pak.source), + }, + }, + ], + ], }; }; diff --git a/example/metro.config.js b/example/metro.config.js new file mode 100644 index 0000000..9439309 --- /dev/null +++ b/example/metro.config.js @@ -0,0 +1,38 @@ +const path = require('path'); +const escape = require('escape-string-regexp'); +const { getDefaultConfig } = require('@expo/metro-config'); +const exclusionList = require('metro-config/src/defaults/exclusionList'); +const pak = require('../package.json'); + +const root = path.resolve(__dirname, '..'); + +const modules = Object.keys({ + ...pak.peerDependencies, +}); + +const defaultConfig = getDefaultConfig(__dirname); + +module.exports = { + ...defaultConfig, + + projectRoot: __dirname, + watchFolders: [root], + + // We need to make sure that only one version is loaded for peerDependencies + // So we block them at the root, and alias them to the versions in example's node_modules + resolver: { + ...defaultConfig.resolver, + + blockList: exclusionList( + modules.map( + (m) => + new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) + ) + ), + + extraNodeModules: modules.reduce((acc, name) => { + acc[name] = path.join(__dirname, 'node_modules', name); + return acc; + }, {}), + }, +}; diff --git a/example/package.json b/example/package.json index 1f32362..251c2a7 100644 --- a/example/package.json +++ b/example/package.json @@ -1,42 +1,34 @@ { - "main": "node_modules/expo/AppEntry.js", + "name": "example", + "version": "1.0.0", "scripts": { - "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "eject": "expo eject", - "prepare": "relative-deps", - "watch": "fswatch -o ../src/ ../index.js ../index.d.ts | xargs -n1 -I{} ./node_modules/.bin/relative-deps" - }, - "relativeDependencies": { - "react-navigation-header-buttons": "../" + "start": "expo start --dev-client", + "android": "expo run:android", + "ios": "expo run:ios", + "web": "expo start --web" }, "dependencies": { - "@expo/react-native-action-sheet": "^3.10.0", - "@react-native-community/masked-view": "0.1.10", - "@react-navigation/native": "^6.0.2", - "@react-navigation/stack": "^6.0.7", - "expo": "^42.0.0", - "expo-status-bar": "~1.0.4", - "react": "16.13.1", - "react-dom": "16.13.1", - "react-native": "https://github.com/expo/react-native/archive/sdk-42.0.0.tar.gz", - "react-native-gesture-handler": "~1.10.2", - "react-native-paper": "^3.6.0", - "react-native-reanimated": "~2.2.0", - "react-native-safe-area-context": "3.2.0", - "react-native-screens": "~3.4.0", - "react-native-web": "~0.13.12", - "react-navigation-header-buttons": "^6.0.0" + "@expo/react-native-action-sheet": "^4.0.1", + "@react-native-menu/menu": "^0.8.0", + "@react-navigation/native": "^6.1.6", + "@react-navigation/native-stack": "^6.9.12", + "@react-navigation/stack": "^6.3.16", + "expo": "~48.0.18", + "expo-splash-screen": "~0.18.2", + "expo-status-bar": "~1.4.4", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-native": "0.71.8", + "react-native-gesture-handler": "~2.9.0", + "react-native-safe-area-context": "4.5.0", + "react-native-screens": "~3.20.0", + "react-native-web": "~0.18.10" }, "devDependencies": { - "@babel/core": "~7.9.0", - "@types/react": "~16.9.35", - "@types/react-native": "~0.63.2", - "babel-preset-expo": "8.4.1", - "relative-deps": "^1.0.7", - "typescript": "~4.3.5" + "@babel/core": "^7.20.0", + "@expo/webpack-config": "^18.0.1", + "babel-loader": "^8.1.0", + "babel-plugin-module-resolver": "^4.1.0" }, "private": true } diff --git a/example/screens/HomeScreen.tsx b/example/screens/HomeScreen.tsx deleted file mode 100644 index 3bbc3cb..0000000 --- a/example/screens/HomeScreen.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useContext } from 'react'; -import { Text, View, ScrollView } from 'react-native'; -import { Button } from './PaddedButton'; -import { ThemeContext } from '../ThemeProvider'; - -export function HomeScreen({ navigation }) { - React.useLayoutEffect(() => { - navigation.setOptions({ - title: 'Header Buttons demo', - headerLargeTitle: true, - }); - }, [navigation]); - const _navigateTo = (destinationScreen: string) => { - navigation.navigate(destinationScreen); - }; - const { toggleTheme } = useContext(ThemeContext); - - return ( - - - Explore possible usages with: -