diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index a70746a..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,25 +0,0 @@
-module.exports = {
- parser: "babel-eslint",
- extends: "airbnb",
- plugins: ["react", "react-native"],
- env: {
- jest: true,
- "react-native/react-native": true
- },
- rules: {
- // allow js file extension
- "react/jsx-filename-extension": [
- "error",
- {
- extensions: [".js", ".jsx"]
- }
- ],
- // for post defining style object in react-native
- "no-use-before-define": ["error", { variables: false }],
- // react-native rules
- "react-native/no-unused-styles": 2,
- "react-native/split-platform-components": 2,
- "react-native/no-inline-styles": 2,
- "react-native/no-raw-text": 2
- }
-};
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..c3798fc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,41 @@
+---
+name: Bug report
+about: Report a problem with react-native-header-search-bar
+title: "[Bug] "
+labels: bug
+---
+
+## Describe the bug
+
+A clear and concise description of what the bug is.
+
+## Reproduction
+
+Steps to reproduce, ideally with a minimal code snippet or Expo Snack:
+
+```jsx
+import { HeaderSearchBar } from "react-native-header-search-bar";
+
+// ...
+```
+
+## Expected behavior
+
+What you expected to happen.
+
+## Screenshots / video
+
+If applicable, add screenshots or a screen recording.
+
+## Environment
+
+- `react-native-header-search-bar` version:
+- `react-native` version:
+- `react` version:
+- Platform: iOS / Android
+- New Architecture (Fabric) enabled: yes / no
+- Expo: yes / no (SDK version if yes)
+
+## Additional context
+
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..0086358
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..8c15d22
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,22 @@
+---
+name: Feature request
+about: Suggest an idea or enhancement
+title: "[Feature] "
+labels: enhancement
+---
+
+## Problem
+
+Is your feature request related to a problem? A clear and concise description of what the problem is.
+
+## Proposed solution
+
+A clear and concise description of what you want to happen, including any new props or API surface.
+
+## Alternatives considered
+
+Any alternative solutions or features you've considered.
+
+## Additional context
+
+Add any other context, mockups, or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..c75259c
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,26 @@
+
+
+## Summary
+
+
+
+## Type of change
+
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would change existing behavior/API)
+- [ ] Documentation only
+- [ ] Tooling / CI
+
+## Checklist
+
+- [ ] `npm run lint` passes
+- [ ] `npm run typecheck` passes
+- [ ] `npm test` passes
+- [ ] `npm run build` succeeds
+- [ ] I updated the `CHANGELOG.md` under `[Unreleased]`
+- [ ] I did not change any public prop names or runtime defaults (or I documented the breaking change)
+
+## Related issues
+
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 4872c5a..6bc2a4b 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,7 +1,26 @@
version: 2
updates:
-- package-ecosystem: npm
- directory: "/"
- schedule:
- interval: daily
- open-pull-requests-limit: 10
+ # Library root (devDependencies, tooling)
+ - package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 10
+ labels:
+ - dependencies
+ # Example app
+ - package-ecosystem: npm
+ directory: "/example"
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ labels:
+ - dependencies
+ - example
+ # GitHub Actions
+ - package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: weekly
+ labels:
+ - ci
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..b030473
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,40 @@
+name: CI
+
+on:
+ push:
+ branches: [master, main]
+ pull_request:
+ branches: [master, main]
+
+jobs:
+ verify:
+ name: Lint, typecheck, test & build (Node ${{ matrix.node }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node: [18, 20, 22]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node ${{ matrix.node }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ cache: npm
+
+ - name: Install dependencies
+ run: npm install --legacy-peer-deps --ignore-scripts
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Typecheck
+ run: npm run typecheck
+
+ - name: Test
+ run: npm test -- --ci --coverage
+
+ - name: Build
+ run: npm run build
diff --git a/.gitignore b/.gitignore
index a7acc97..f9ed8a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,12 @@ package-lock.json
node_modules
+# build output (generated by react-native-builder-bob on prepare)
+lib/
+
+# test coverage
+coverage/
+
# Xcode
#
build/
diff --git a/.npmignore b/.npmignore
deleted file mode 100644
index 0ddec1c..0000000
--- a/.npmignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Node Modules
-**/node_modules
-node_modules
-# Example
-example
-# Assets
-Assets
-assets
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..bb570f6
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,16 @@
+# dependencies
+node_modules/
+
+# build output
+lib/
+coverage/
+
+# logs
+npm-debug.log
+yarn-error.log
+
+# os
+.DS_Store
+
+# lockfile (library)
+package-lock.json
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..d54500c
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "arrowParens": "always",
+ "trailingComma": "all",
+ "tabWidth": 2,
+ "semi": true,
+ "singleQuote": false
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d34e95f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,70 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+> This release is a deep modernization of a library that had not been touched
+> since 2020. It migrates the source to TypeScript and ships compiled output via
+> `react-native-builder-bob`. The **public API is unchanged** (same named exports
+> `HeaderSearchBar` / `HeaderClassicSearchBar`, same default object export, same
+> prop names and runtime defaults), but the published package layout and minimum
+> peer versions changed — see **Breaking** below. A major version bump is
+> recommended.
+
+### Added
+
+- TypeScript source with shipped declaration files (`.d.ts`). Exported prop
+ interfaces: `HeaderSearchBarProps`, `HeaderClassicSearchBarProps`, `SearchBoxProps`.
+- `react-native-builder-bob` build pipeline producing CommonJS, ES module and
+ TypeScript targets under `lib/`.
+- `package.json` `exports`, `module`, `react-native`, `source`, `types` and a
+ `files` allowlist so only `src` + `lib` + docs are published.
+- Real test suite: Jest + `@testing-library/react-native` (21 tests across both
+ header variants and `SearchBox`, including default props, `onChangeText`
+ wiring, `disableTextInput`, custom `iconComponent`, and snapshots).
+- GitHub Actions CI (`.github/workflows/ci.yml`): install + lint + typecheck +
+ test + build on a Node 18/20/22 matrix for pushes and PRs.
+- `CHANGELOG.md`, `CONTRIBUTING.md`, issue templates and a pull request template.
+- Accessibility roles/labels on the hamburger button, filter ripple target and
+ search box.
+- `react-native-material-ripple` is now declared in `peerDependencies` (it was
+ imported by `HeaderClassicSearchBar` but never declared).
+
+### Changed
+
+- Migrated linting from the deprecated `tslint`/`babel-eslint` + `airbnb` setup
+ to ESLint 9 flat config (`eslint.config.js`) with `typescript-eslint`,
+ `eslint-plugin-react` and Prettier, and added an actual `.prettierrc`
+ (the README advertised a Prettier badge but no config existed).
+- `HeaderClassicSearchBar` now uses `useSafeAreaInsets()` instead of the removed
+ `useSafeArea()` API, so it works on `react-native-safe-area-context` v3/v4/v5+.
+- Components are now wrapped in `React.memo` and `React.forwardRef`, and drop the
+ deprecated `defaultProps`/`PropTypes` in favor of typed default parameters.
+- Stopped spreading `{...props}` blindly onto the menu `Icon`, the filter `Ripple`
+ and the filter `Icon`; only intended props are forwarded now. The deliberate
+ prop passthrough to the inner `SearchBox` is preserved.
+- Repository, homepage and bugs URLs now point at `kuraydev`.
+
+### Fixed
+
+- `SearchBox` no longer creates an implicit global on every render — the inner
+ `renderTextInput` helper is now a proper local `const`.
+- `secondTitleFontSize` is now honored. The second-title style previously
+ hard-coded `fontSize: 18` and silently ignored the documented prop.
+- `prop-types` is no longer an undeclared import (PropTypes were removed in the
+ TypeScript migration).
+
+### Removed
+
+- Raw, uncompiled `lib/src/*.js` source is no longer the package entry point.
+ Consumers that deep-imported `react-native-header-search-bar/lib/src/...` must
+ switch to the package root import (see migration notes in the README/PR).
+- Runtime `PropTypes` validation (replaced by compile-time TypeScript types).
+
+## [0.1.10] - 2020-03-09
+
+- Last legacy release. See git history.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..3d3f6ef
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,51 @@
+# Contributing
+
+Thanks for your interest in improving **react-native-header-search-bar**! 🎉
+
+## Development setup
+
+```sh
+git clone https://github.com/kuraydev/react-native-header-search-bar.git
+cd react-native-header-search-bar
+npm install --legacy-peer-deps
+```
+
+> The native peer dependencies (`react-native-androw`,
+> `react-native-dynamic-vector-icons`, `react-native-material-ripple`,
+> `@freakycoder/react-native-helpers`) are intentionally **not** installed in the
+> library's own `node_modules` — they are peer dependencies provided by the host
+> app. They are mocked in the test suite (`jest.setup.js`).
+
+## Project layout
+
+- `src/` — TypeScript source (the only thing you edit).
+- `lib/` — generated build output (CommonJS + ESM + types). **Do not edit**; it is
+ git-ignored and produced by `npm run build`.
+- `example/` — a bare React Native example app.
+
+## Useful scripts
+
+| Script | Description |
+| ------------------- | -------------------------------------------- |
+| `npm run lint` | ESLint (flat config) |
+| `npm run typecheck` | `tsc --noEmit` |
+| `npm test` | Jest test suite |
+| `npm run build` | Build `lib/` with `react-native-builder-bob` |
+| `npm run clean` | Remove `lib/` |
+
+Please make sure `lint`, `typecheck`, `test` and `build` all pass before opening
+a pull request — CI runs the same checks on Node 18, 20 and 22.
+
+## Pull requests
+
+- Use [Conventional Commits](https://www.conventionalcommits.org/) for commit
+ messages (e.g. `fix:`, `feat:`, `chore:`, `docs:`).
+- Add or update tests for any behavior change.
+- Add an entry under `## [Unreleased]` in `CHANGELOG.md`.
+- **Do not** rename public exports or change runtime prop defaults without
+ flagging it as a breaking change.
+
+## Code style
+
+This project uses Prettier and ESLint. Run `npm run lint:fix` and
+`npx prettier --write .` before committing.
diff --git a/README.md b/README.md
index 00e4880..5081157 100644
--- a/README.md
+++ b/README.md
@@ -1,134 +1,186 @@
-[](https://github.com/WrathChaos/react-native-header-search-bar)
-
-[](https://github.com/WrathChaos/react-native-header-search-bar)
+[](https://github.com/kuraydev/react-native-header-search-bar)
[](https://www.npmjs.com/package/react-native-header-search-bar)
-[](https://www.npmjs.com/package/react-native-header-search-bar)
+[](https://www.npmjs.com/package/react-native-header-search-bar)

+
[](https://opensource.org/licenses/MIT)
[](https://github.com/prettier/prettier)
+Two pre-styled, fully customizable header search bars for React Native:
+`HeaderSearchBar` (titles + hamburger) and `HeaderClassicSearchBar` (search +
+filter ripple). Pure JavaScript/TypeScript — no native module of its own.
+
- |
- Header Search Bar
- |
-
- Header Classic Search Bar
- |
+ Header Search Bar |
+ Header Classic Search Bar |
-
+
|
-
+
|
## Installation
-Add the dependency:
-
-### React Native:
-
-```ruby
-npm i react-native-header-search-bar
+```sh
+npm install react-native-header-search-bar
+# or
+yarn add react-native-header-search-bar
```
-## Peer Dependencies
+### Peer dependencies
-###### IMPORTANT! You need install them.
+This library relies on a few native peers. Install them in your app as well:
-```js
-"react-native-androw": ">= 0.0.33",
-"react-native-vector-icons": ">= 6.6.0",
-"react-native-safe-area-context": ">= 0.6.1",
-"react-native-dynamic-vector-icons": ">= 0.2.1",
-"@freakycoder/react-native-helpers": ">= 0.0.21"
+```sh
+npm install \
+ react-native-androw \
+ react-native-vector-icons \
+ react-native-dynamic-vector-icons \
+ react-native-material-ripple \
+ react-native-safe-area-context \
+ @freakycoder/react-native-helpers
```
-# Component Options
+> `react-native-material-ripple` is required by `HeaderClassicSearchBar`.
+> `react-native-androw` (Android drop shadow) and `react-native-vector-icons`
+> are declared **optional** peers — install them if you use `HeaderSearchBar`'s
+> shadow / the bundled icons.
+
+`HeaderClassicSearchBar` reads safe-area insets, so wrap your app in a
+`SafeAreaProvider` (from `react-native-safe-area-context`) once, near the root:
+
+```jsx
+import { SafeAreaProvider } from "react-native-safe-area-context";
-- HeaderSearchBar
-- HeaderClassicSearchBar
+export default function App() {
+ return {/* ... */};
+}
+```
-## Import
+## Usage
-```js
+```jsx
import {
HeaderSearchBar,
- HeaderClassicSearchBar
+ HeaderClassicSearchBar,
} from "react-native-header-search-bar";
-```
-# HeaderSearchBar Usage
+// Titles + hamburger
+ console.log(text)} />;
-```jsx
- console.log(text)} />
+// Classic search + filter
+ console.log(text)}
+ onPress={() => console.log("filter tapped")}
+/>;
```
-# HeaderClassicSearchBar Usage
+### TypeScript
-```jsx
- console.log(text)} />
+Types ship with the package — no `@types/...` needed. The prop interfaces are
+exported:
+
+```tsx
+import {
+ HeaderSearchBar,
+ HeaderClassicSearchBar,
+ type HeaderSearchBarProps,
+ type HeaderClassicSearchBarProps,
+ type SearchBoxProps,
+} from "react-native-header-search-bar";
```
-# Configuration - Props
-
-## Main Props
-
-| Property | Type | Default | Description |
-| -------------------- | :-------: | :---------------: | ---------------------------------------------------------- |
-| backgroundColor | color | #fff | change the background color |
-| shadowColor | color | #000 | change the main shadow color |
-| firstTitle | string | Find your | set the first title itself |
-| secondTitle | string | Favorite Art Work | set the second title itself |
-| firstTitleColor | color | #959597 | change the first title's text color |
-| secondTitleColor | color | #34343b | change the second title's text color |
-| firstTitleFontSize | number | 18 | change the first title font size |
-| secondTitleFontSize | number | 18 | change the second title font size |
-| iconComponent | component | hamburger icon | set your own component instead of hamburger icon component |
-| onPressHamburgerIcon | function | null | set your own function when hamburger icon is on pressed |
-
-## SearchBox Props
-
-| Property | Type | Default | Description |
-| ------------------------ | :-------: | :-----------------------: | ----------------------------------------------------- |
-| iconName | string | search | set the icon name |
-| iconType | string | EvilIcons | set the icon type |
-| iconSize | number | 25 | set the icon size |
-| iconColor | color | #000 | set the icon color |
-| onChangeText | function | function | set your own logic for changing text |
-| value | string | undefined | set the value for search box's text input |
-| searchBoxText | string | What are you looking for? | set the search box's text |
-| searchBoxTextStyle | style | default | set your own style for text input's style |
-| searchBoxOnPress | function | function | set your own logic when tapping the search box itself |
-| searchBoxWidth | number | 95% | change the search box's width |
-| searchBoxBorderRadius | number | 8 | change the search box's border radius |
-| searchBoxBackgroundColor | color | #f5f5f5 | change the search box's background color |
-| iconComponent | component | Icon | set your own icon component instead of Icon |
-| disableTextInput | boolean | false | disable or enable the text input itself |
-
-## Future Plans
-
-- [x] ~~LICENSE~~
-- [x] ~~Header Classic Search Bar Component~~
-- [ ] Write an article about the lib on Medium
-
-## Inspiration
-
-Header Search Bar Component is designed by: [Jawadur Rahman](https://www.instagram.com/p/B3RSEG9APnH/)
+## Props
+
+> Both header components also accept every [`SearchBox` prop](#searchbox-props)
+> and forward it to the inner search box.
+
+### `HeaderSearchBar`
+
+| Property | Type | Default | Description |
+| ---------------------- | :----------: | :-----------------: | ---------------------------------------- |
+| `backgroundColor` | color | `#fff` | Header background color |
+| `shadowColor` | color | `#000` | Drop shadow color |
+| `firstTitle` | string | `Find your` | Top (first) title text |
+| `secondTitle` | string | `Favorite Art Work` | Bottom (second) title text |
+| `firstTitleColor` | color | `#959597` | First title color |
+| `secondTitleColor` | color | `#34343b` | Second title color |
+| `firstTitleFontSize` | number | `18` | First title font size |
+| `secondTitleFontSize` | number | `18` | Second title font size |
+| `iconComponent` | `ReactNode` | hamburger icon | Custom component for the hamburger slot |
+| `onPressHamburgerIcon` | `() => void` | — | Called when the hamburger icon is tapped |
+
+### `HeaderClassicSearchBar`
+
+| Property | Type | Default | Description |
+| ----------------- | :----------: | :-----: | ------------------------------------- |
+| `backgroundColor` | color | `#fff` | Header background color |
+| `shadowColor` | color | `#000` | Drop shadow color |
+| `onPress` | `() => void` | — | Called when the filter icon is tapped |
+
+### `SearchBox` props
+
+| Property | Type | Default | Description |
+| -------------------------- | :-------------------: | :-------------------------: | ---------------------------------------------- |
+| `iconName` | string | `search` | Leading icon name |
+| `iconType` | string | `EvilIcons` | Leading icon set/type |
+| `iconSize` | number | `25` | Leading icon size |
+| `iconColor` | color | `black` | Leading icon color |
+| `iconComponent` | `ReactNode` | default `Icon` | Custom leading icon component |
+| `value` | string | `undefined` | Controlled value of the text input |
+| `onChangeText` | `(t: string) => void` | — | Called when the search text changes |
+| `onFocus` | `() => void` | — | Called when the text input gains focus |
+| `searchBoxText` | string | `What are you looking for?` | Placeholder (or label if `disableTextInput`) |
+| `searchBoxTextStyle` | `TextStyle` | default | Style override for the text element |
+| `searchBoxOnPress` | `() => void` | — | Called when the search box is pressed |
+| `searchBoxWidth` | number \| string | `95%` | Search box container width |
+| `searchBoxBorderRadius` | number | `8` | Search box border radius |
+| `searchBoxBackgroundColor` | color | `#f5f5f5` | Search box background color |
+| `disableTextInput` | boolean | `false` | Render a static label instead of a `TextInput` |
+
+## New Architecture & Expo
+
+This is a pure JS/TS component with **no native module of its own**, so it is
+compatible with the React Native **New Architecture (Fabric)** out of the box.
+Compatibility on the New Architecture therefore depends on the versions of the
+native peer dependencies listed above.
+
+It also works with **Expo** (custom dev client / prebuild) as long as the native
+peer dependencies are installed and built into your app.
+
+## Migrating from `0.1.x`
+
+The public API is unchanged (same exports, same prop names, same default values),
+but `0.x` shipped raw `lib/src/*.js`. From the modernized release:
+
+- Import from the package root: `import { HeaderSearchBar } from "react-native-header-search-bar"`.
+ Deep imports such as `react-native-header-search-bar/lib/src/...` no longer
+ resolve — use the package root instead.
+- The minimum `react-native-safe-area-context` is now `>= 3.0.0`
+ (`HeaderClassicSearchBar` uses `useSafeAreaInsets`).
+- `secondTitleFontSize` is now actually applied (it was a no-op before).
+
+See [`CHANGELOG.md`](CHANGELOG.md) for the full list.
+
+## Contributing
+
+Contributions are welcome! Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) and
+the [changelog](CHANGELOG.md) before opening a pull request.
## Author
-FreakyCoder, kurayogun@gmail.com
+Kuray (FreakyCoder), kurayogun@gmail.com
## License
-React Native Header Search Bar is available under the MIT license. See the LICENSE file for more info.
+React Native Header Search Bar is available under the MIT license. See the
+[LICENSE](LICENSE) file for more info.
diff --git a/babel.config.js b/babel.config.js
new file mode 100644
index 0000000..3c25e5d
--- /dev/null
+++ b/babel.config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ presets: ["module:@react-native/babel-preset"],
+};
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..f065b52
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,54 @@
+const tsParser = require("@typescript-eslint/parser");
+const tsPlugin = require("@typescript-eslint/eslint-plugin");
+const reactPlugin = require("eslint-plugin-react");
+const prettierConfig = require("eslint-config-prettier");
+const globals = require("globals");
+
+module.exports = [
+ {
+ ignores: [
+ "lib/**",
+ "example/**",
+ "node_modules/**",
+ "coverage/**",
+ "**/*.d.ts",
+ ],
+ },
+ {
+ files: ["src/**/*.{ts,tsx}"],
+ languageOptions: {
+ parser: tsParser,
+ parserOptions: {
+ ecmaFeatures: { jsx: true },
+ sourceType: "module",
+ },
+ globals: {
+ ...globals.es2021,
+ ...globals.node,
+ },
+ },
+ plugins: {
+ "@typescript-eslint": tsPlugin,
+ react: reactPlugin,
+ },
+ settings: {
+ react: { version: "detect" },
+ },
+ rules: {
+ ...tsPlugin.configs.recommended.rules,
+ ...reactPlugin.configs.recommended.rules,
+ "react/react-in-jsx-scope": "off",
+ "react/prop-types": "off",
+ "@typescript-eslint/no-explicit-any": "warn",
+ },
+ },
+ {
+ files: ["src/**/__tests__/**/*.{ts,tsx}"],
+ languageOptions: {
+ globals: {
+ ...globals.jest,
+ },
+ },
+ },
+ prettierConfig,
+];
diff --git a/example/App.js b/example/App.js
index 616b183..4f1545c 100644
--- a/example/App.js
+++ b/example/App.js
@@ -1,11 +1,7 @@
import React from "react";
import { View, StatusBar } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context";
-// import {
-// HeaderSearchBar,
-// HeaderClassicSearchBar
-// } from "react-native-header-search-bar";
-import HeaderClassicSearchBar from "./lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar";
+import { HeaderClassicSearchBar } from "react-native-header-search-bar";
const App = () => {
return (
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 0000000..b5495c8
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,59 @@
+/* eslint-disable @typescript-eslint/no-require-imports */
+
+// These native peer dependencies are intentionally NOT installed in the
+// library's own node_modules (they are peerDependencies the consumer provides),
+// so they are registered as virtual mocks.
+jest.mock(
+ "react-native-androw",
+ () => {
+ const React = require("react");
+ const { View } = require("react-native");
+ const Androw = (props) => React.createElement(View, props, props.children);
+ return { __esModule: true, default: Androw };
+ },
+ { virtual: true },
+);
+
+jest.mock(
+ "react-native-dynamic-vector-icons",
+ () => {
+ const React = require("react");
+ const { Text } = require("react-native");
+ const Icon = ({ name }) =>
+ React.createElement(Text, { testID: `icon-${name}` }, name);
+ return { __esModule: true, default: Icon };
+ },
+ { virtual: true },
+);
+
+jest.mock(
+ "react-native-material-ripple",
+ () => {
+ const React = require("react");
+ const { TouchableOpacity } = require("react-native");
+ const Ripple = (props) =>
+ React.createElement(TouchableOpacity, props, props.children);
+ return { __esModule: true, default: Ripple };
+ },
+ { virtual: true },
+);
+
+jest.mock(
+ "@freakycoder/react-native-helpers",
+ () => ({
+ isAndroid: false,
+ hasNotch: () => false,
+ isIPhoneXFamily: () => false,
+ getStatusBarHeight: () => 20,
+ }),
+ { virtual: true },
+);
+
+jest.mock("react-native-safe-area-context", () => {
+ const React = require("react");
+ return {
+ useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
+ SafeAreaProvider: ({ children }) =>
+ React.createElement(React.Fragment, null, children),
+ };
+});
diff --git a/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.js b/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.js
deleted file mode 100644
index 4f0cb31..0000000
--- a/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { View } from "react-native";
-import styles, {
- shadowStyle,
- container,
- searchboxContainer
-} from "../HeaderClassicSearchBar/HeaderClassicSearchBar.style";
-import Androw from "react-native-androw";
-import SearchBox from "../components/SearchBox/SearchBox";
-import Icon from "react-native-dynamic-vector-icons";
-import Ripple from "react-native-material-ripple";
-import { useSafeArea } from "react-native-safe-area-context";
-
-const HeaderClassicSearchBar = props => {
- const { shadowColor, backgroundColor, onPress } = props;
- const insets = useSafeArea();
- return (
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-HeaderClassicSearchBar.propTypes = {
- shadowColor: PropTypes.string,
- backgroundColor: PropTypes.string
-};
-
-HeaderClassicSearchBar.defaultProps = {
- shadowColor: "#000",
- backgroundColor: "#fff"
-};
-
-export default HeaderClassicSearchBar;
diff --git a/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.js b/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.js
deleted file mode 100644
index 94fb4eb..0000000
--- a/lib/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Platform } from "react-native";
-import {
- getStatusBarHeight,
- isIPhoneXFamily,
- isAndroid
-} from "@freakycoder/react-native-helpers";
-
-export const shadowStyle = shadowColor => {
- return {
- shadowColor,
- shadowRadius: 9,
- shadowOpacity: 0.2,
- shadowOffset: {
- width: 0,
- height: 4
- }
- };
-};
-
-export const container = (backgroundColor, insets) => {
- return {
- width: "100%",
- backgroundColor,
- borderBottomEndRadius: 24,
- borderBottomStartRadius: 24,
- height: isAndroid ? insets.top + 75 : isIPhoneXFamily() ? 115 : 85
- };
-};
-
-export const searchboxContainer = insets => ({
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-around",
- ...Platform.select({
- ios: {
- marginTop: isIPhoneXFamily() ? getStatusBarHeight() + 23 : 38
- },
- android: {
- marginTop: insets.top + 25
- }
- })
-});
-
-export default {
- rippleContainer: {
- bottom: 2,
- width: 35,
- height: 35,
- alignItems: "center",
- justifyContent: "center"
- }
-};
diff --git a/lib/src/HeaderSearchBar/HeaderSearchBar.js b/lib/src/HeaderSearchBar/HeaderSearchBar.js
deleted file mode 100644
index 24a45a6..0000000
--- a/lib/src/HeaderSearchBar/HeaderSearchBar.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import { Text, View, TouchableOpacity } from "react-native";
-import Androw from "react-native-androw";
-import Icon from "react-native-dynamic-vector-icons";
-import SearchBox from "../components/SearchBox/SearchBox";
-import styles, {
- shadowStyle,
- container,
- firstTitleStyle,
- secondTitleStyle
-} from "./HeaderSearchBar.style";
-
-const HeaderSearchBar = props => {
- const {
- firstTitle,
- secondTitle,
- shadowColor,
- iconComponent,
- backgroundColor,
- firstTitleColor,
- secondTitleColor,
- firstTitleFontSize,
- secondTitleFontSize,
- onPressHamburgerIcon
- } = props;
- return (
-
-
-
-
- {iconComponent || (
-
- )}
-
-
-
- {firstTitle}
-
-
- {secondTitle}
-
-
-
-
-
-
-
-
- );
-};
-
-HeaderSearchBar.propTypes = {
- firstTitle: PropTypes.string,
- secondTitle: PropTypes.string,
- shadowColor: PropTypes.string,
- backgroundColor: PropTypes.string,
- firstTitleColor: PropTypes.string,
- secondTitleColor: PropTypes.string,
- firstTitleFontSize: PropTypes.number,
- secondTitleFontSize: PropTypes.number
-};
-
-HeaderSearchBar.defaultProps = {
- shadowColor: "#000",
- firstTitleFontSize: 18,
- secondTitleFontSize: 18,
- backgroundColor: "#fff",
- firstTitle: "Find your",
- firstTitleColor: "#959597",
- secondTitleColor: "#34343b",
- secondTitle: "Favorite Art Work"
-};
-
-export default HeaderSearchBar;
diff --git a/lib/src/HeaderSearchBar/HeaderSearchBar.style.js b/lib/src/HeaderSearchBar/HeaderSearchBar.style.js
deleted file mode 100644
index d136f15..0000000
--- a/lib/src/HeaderSearchBar/HeaderSearchBar.style.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import {
- hasNotch,
- getStatusBarHeight
-} from "@freakycoder/react-native-helpers";
-
-export const shadowStyle = shadowColor => {
- return {
- shadowColor,
- shadowRadius: 9,
- shadowOpacity: 0.2,
- shadowOffset: {
- width: 0,
- height: 4
- }
- };
-};
-
-export const container = backgroundColor => {
- return {
- height: 210,
- width: "100%",
- backgroundColor,
- borderBottomEndRadius: 24,
- borderBottomStartRadius: 24
- };
-};
-
-export const firstTitleStyle = (color, fontSize) => {
- return {
- color,
- fontSize,
- fontWeight: "bold"
- };
-};
-
-export const secondTitleStyle = (color, fontSize) => {
- return {
- fontSize: 18,
- marginTop: 3,
- color: color,
- fontWeight: "bold"
- };
-};
-
-export default {
- containerGlue: {
- marginLeft: 24,
- marginTop: hasNotch() ? getStatusBarHeight() : 0
- },
- titleContainer: {
- marginTop: 12,
- marginLeft: 3
- },
- searchboxContainer: {
- top: 8,
- margin: 16
- }
-};
diff --git a/lib/src/components/SearchBox/SearchBox.js b/lib/src/components/SearchBox/SearchBox.js
deleted file mode 100644
index 0aa9f14..0000000
--- a/lib/src/components/SearchBox/SearchBox.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import React from "react";
-import PropTypes from "prop-types";
-import Icon from "react-native-dynamic-vector-icons";
-import { TextInput, TouchableOpacity, Text, View } from "react-native";
-import styles, { searchBoxContainer } from "./SearchBox.style";
-
-const SearchBox = props => {
- const {
- value,
- onFocus,
- iconName,
- iconType,
- iconSize,
- iconColor,
- onChangeText,
- searchBoxText,
- iconComponent,
- searchBoxWidth,
- searchBoxOnPress,
- disableTextInput,
- searchBoxTextStyle,
- searchBoxBorderRadius,
- searchBoxBackgroundColor
- } = props;
-
- renderTextInput = () => {
- if (disableTextInput)
- return {searchBoxText};
- return (
-
- );
- };
-
- return (
-
-
- {iconComponent || (
-
- )}
- {renderTextInput()}
-
-
- );
-};
-
-SearchBox.propTypes = {
- iconName: PropTypes.string,
- iconType: PropTypes.string,
- iconColor: PropTypes.string,
- iconSize: PropTypes.number,
- searchBoxText: PropTypes.string,
- disableTextInput: PropTypes.bool,
- searchBoxBorderRadius: PropTypes.number,
- searchBoxBackgroundColor: PropTypes.string
-};
-
-SearchBox.defaultProps = {
- iconSize: 25,
- iconName: "search",
- iconColor: "black",
- iconType: "EvilIcons",
- disableTextInput: false,
- searchBoxWidth: "95%",
- searchBoxBorderRadius: 8,
- searchBoxBackgroundColor: "#f5f5f5",
- searchBoxText: "What are you looking for?"
-};
-
-export default SearchBox;
diff --git a/lib/src/components/SearchBox/SearchBox.style.js b/lib/src/components/SearchBox/SearchBox.style.js
deleted file mode 100644
index 360b60c..0000000
--- a/lib/src/components/SearchBox/SearchBox.style.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { isAndroid } from "@freakycoder/react-native-helpers";
-
-export function searchBoxContainer(
- searchBoxWidth,
- searchBoxBorderRadius,
- searchBoxBackgroundColor
-) {
- return {
- left: 8,
- padding: 6,
- alignItems: "center",
- width: searchBoxWidth,
- justifyContent: "center",
- height: isAndroid ? 30 : null,
- borderRadius: searchBoxBorderRadius,
- backgroundColor: searchBoxBackgroundColor
- };
-}
-
-export default {
- searchBoxGlue: {
- alignItems: "center",
- flexDirection: "row"
- },
- searchBoxTextStyle: {
- left: 3,
- width: "85%",
- fontSize: 13,
- color: "#000",
- height: isAndroid ? 50 : null,
- backgroundColor: "transparent"
- }
-};
diff --git a/package.json b/package.json
index 769d112..28cf723 100644
--- a/package.json
+++ b/package.json
@@ -1,37 +1,133 @@
{
"name": "react-native-header-search-bar",
- "version": "0.1.10",
+ "version": "1.0.0",
"description": "Fully customizable header search bar for React Native",
"keywords": [
+ "react-native",
+ "react",
"header",
"search",
- "bar",
+ "search-bar",
"header-search",
"header-search-bar",
- "search-bar",
- "FreakyCoder",
- "freakycoder",
- "kuray",
- "Kuray",
- "react",
- "react-native",
- "javascript",
- "ui-lib",
- "rn"
+ "ios",
+ "android",
+ "typescript",
+ "ui-component"
],
- "homepage": "https://www.freakycoder.com",
- "bugs": "https://github.com/WrathChaos/react-native-header-search-bar/issues",
- "main": "./lib/src/index.js",
- "repository": "git@github.com:WrathChaos/react-native-header-search-bar.git",
- "author": "Kuray (FreakyCoder) ",
+ "homepage": "https://github.com/kuraydev/react-native-header-search-bar#readme",
+ "bugs": {
+ "url": "https://github.com/kuraydev/react-native-header-search-bar/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/kuraydev/react-native-header-search-bar.git"
+ },
"license": "MIT",
+ "author": "Kuray (FreakyCoder) ",
+ "main": "./lib/commonjs/index.js",
+ "module": "./lib/module/index.js",
+ "react-native": "./src/index.tsx",
+ "source": "./src/index.tsx",
+ "types": "./lib/typescript/index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./lib/typescript/index.d.ts",
+ "react-native": "./src/index.tsx",
+ "default": "./lib/commonjs/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "files": [
+ "src",
+ "lib",
+ "README.md",
+ "LICENSE",
+ "CHANGELOG.md",
+ "!**/__tests__",
+ "!**/__mocks__",
+ "!**/*.test.ts",
+ "!**/*.test.tsx"
+ ],
+ "scripts": {
+ "build": "bob build",
+ "clean": "del-cli lib",
+ "typecheck": "tsc --noEmit",
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
+ "test": "jest",
+ "prepare": "bob build",
+ "prepack": "bob build"
+ },
"peerDependencies": {
- "react": ">= 16.x.x",
- "react-native": ">= 0.55.x",
- "react-native-androw": ">= 0.0.33",
- "react-native-vector-icons": ">= 6.6.0",
- "react-native-safe-area-context": ">= 0.6.1",
- "react-native-dynamic-vector-icons": ">= 0.2.1",
- "@freakycoder/react-native-helpers": ">= 0.0.21"
+ "react": ">=16.8.0",
+ "react-native": ">=0.60.0",
+ "react-native-androw": ">=0.0.33",
+ "react-native-dynamic-vector-icons": ">=0.2.0",
+ "react-native-material-ripple": ">=0.9.0",
+ "react-native-safe-area-context": ">=3.0.0",
+ "react-native-vector-icons": ">=6.0.0",
+ "@freakycoder/react-native-helpers": ">=0.0.21"
+ },
+ "peerDependenciesMeta": {
+ "react-native-androw": {
+ "optional": true
+ },
+ "react-native-vector-icons": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.0",
+ "@babel/preset-env": "^7.25.0",
+ "@babel/runtime": "^7.25.0",
+ "@react-native/babel-preset": "0.76.5",
+ "@testing-library/react-native": "^12.5.0",
+ "@types/jest": "^29.5.12",
+ "@types/react": "^18.3.0",
+ "@types/react-test-renderer": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
+ "@typescript-eslint/parser": "^8.0.0",
+ "babel-jest": "^29.7.0",
+ "del-cli": "^5.1.0",
+ "eslint": "^9.0.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-react": "^7.35.0",
+ "globals": "^15.9.0",
+ "jest": "^29.7.0",
+ "prettier": "^3.3.0",
+ "react": "18.3.1",
+ "react-native": "0.76.5",
+ "react-native-builder-bob": "^0.30.0",
+ "react-native-safe-area-context": "^4.11.0",
+ "react-test-renderer": "18.3.1",
+ "typescript": "^5.5.0"
+ },
+ "react-native-builder-bob": {
+ "source": "src",
+ "output": "lib",
+ "targets": [
+ "commonjs",
+ "module",
+ [
+ "typescript",
+ {
+ "project": "tsconfig.build.json"
+ }
+ ]
+ ]
+ },
+ "jest": {
+ "preset": "react-native",
+ "setupFiles": [
+ "/jest.setup.js"
+ ],
+ "modulePathIgnorePatterns": [
+ "/lib/",
+ "/example/"
+ ],
+ "transformIgnorePatterns": [
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?|react-native-safe-area-context)/)"
+ ]
}
}
diff --git a/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.ts b/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.ts
new file mode 100644
index 0000000..23e61f6
--- /dev/null
+++ b/src/HeaderClassicSearchBar/HeaderClassicSearchBar.style.ts
@@ -0,0 +1,55 @@
+import { Platform } from "react-native";
+import {
+ getStatusBarHeight,
+ isIPhoneXFamily,
+ isAndroid,
+} from "@freakycoder/react-native-helpers";
+import type { EdgeInsets } from "react-native-safe-area-context";
+import type { ViewStyle } from "react-native";
+
+export const shadowStyle = (shadowColor: string | undefined): ViewStyle => ({
+ shadowColor,
+ shadowRadius: 9,
+ shadowOpacity: 0.2,
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+});
+
+export const container = (
+ backgroundColor: string | undefined,
+ insets: EdgeInsets,
+): ViewStyle => ({
+ width: "100%",
+ backgroundColor,
+ borderBottomEndRadius: 24,
+ borderBottomStartRadius: 24,
+ height: isAndroid ? insets.top + 75 : isIPhoneXFamily() ? 115 : 85,
+});
+
+export const searchboxContainer = (insets: EdgeInsets): ViewStyle => ({
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-around",
+ ...Platform.select({
+ ios: {
+ marginTop: isIPhoneXFamily() ? getStatusBarHeight() + 23 : 38,
+ },
+ android: {
+ marginTop: insets.top + 25,
+ },
+ }),
+});
+
+const styles: { rippleContainer: ViewStyle } = {
+ rippleContainer: {
+ bottom: 2,
+ width: 35,
+ height: 35,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+};
+
+export default styles;
diff --git a/src/HeaderClassicSearchBar/HeaderClassicSearchBar.tsx b/src/HeaderClassicSearchBar/HeaderClassicSearchBar.tsx
new file mode 100644
index 0000000..5e3d3bf
--- /dev/null
+++ b/src/HeaderClassicSearchBar/HeaderClassicSearchBar.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import { View } from "react-native";
+import Androw from "react-native-androw";
+import Icon from "react-native-dynamic-vector-icons";
+import Ripple from "react-native-material-ripple";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import SearchBox, {
+ type SearchBoxProps,
+} from "../components/SearchBox/SearchBox";
+import styles, {
+ shadowStyle,
+ container,
+ searchboxContainer,
+} from "./HeaderClassicSearchBar.style";
+
+export interface HeaderClassicSearchBarProps extends SearchBoxProps {
+ /** Drop shadow color of the header. */
+ shadowColor?: string;
+ /** Header background color. */
+ backgroundColor?: string;
+ /** Called when the trailing filter icon is pressed. */
+ onPress?: () => void;
+}
+
+const HeaderClassicSearchBar = React.forwardRef<
+ React.ComponentRef,
+ HeaderClassicSearchBarProps
+>((props, ref) => {
+ const { shadowColor = "#000", backgroundColor = "#fff", onPress } = props;
+ const insets = useSafeAreaInsets();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+HeaderClassicSearchBar.displayName = "HeaderClassicSearchBar";
+
+export default React.memo(HeaderClassicSearchBar);
diff --git a/src/HeaderSearchBar/HeaderSearchBar.style.ts b/src/HeaderSearchBar/HeaderSearchBar.style.ts
new file mode 100644
index 0000000..9451663
--- /dev/null
+++ b/src/HeaderSearchBar/HeaderSearchBar.style.ts
@@ -0,0 +1,63 @@
+import {
+ hasNotch,
+ getStatusBarHeight,
+} from "@freakycoder/react-native-helpers";
+import type { TextStyle, ViewStyle } from "react-native";
+
+export const shadowStyle = (shadowColor: string | undefined): ViewStyle => ({
+ shadowColor,
+ shadowRadius: 9,
+ shadowOpacity: 0.2,
+ shadowOffset: {
+ width: 0,
+ height: 4,
+ },
+});
+
+export const container = (backgroundColor: string | undefined): ViewStyle => ({
+ height: 210,
+ width: "100%",
+ backgroundColor,
+ borderBottomEndRadius: 24,
+ borderBottomStartRadius: 24,
+});
+
+export const firstTitleStyle = (
+ color: string | undefined,
+ fontSize: number | undefined,
+): TextStyle => ({
+ color,
+ fontSize,
+ fontWeight: "bold",
+});
+
+export const secondTitleStyle = (
+ color: string | undefined,
+ fontSize: number | undefined,
+): TextStyle => ({
+ color,
+ fontSize,
+ marginTop: 3,
+ fontWeight: "bold",
+});
+
+const styles: {
+ containerGlue: ViewStyle;
+ titleContainer: ViewStyle;
+ searchboxContainer: ViewStyle;
+} = {
+ containerGlue: {
+ marginLeft: 24,
+ marginTop: hasNotch() ? getStatusBarHeight() : 0,
+ },
+ titleContainer: {
+ marginTop: 12,
+ marginLeft: 3,
+ },
+ searchboxContainer: {
+ top: 8,
+ margin: 16,
+ },
+};
+
+export default styles;
diff --git a/src/HeaderSearchBar/HeaderSearchBar.tsx b/src/HeaderSearchBar/HeaderSearchBar.tsx
new file mode 100644
index 0000000..35a3856
--- /dev/null
+++ b/src/HeaderSearchBar/HeaderSearchBar.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { Text, View, TouchableOpacity } from "react-native";
+import Androw from "react-native-androw";
+import Icon from "react-native-dynamic-vector-icons";
+import SearchBox, {
+ type SearchBoxProps,
+} from "../components/SearchBox/SearchBox";
+import styles, {
+ shadowStyle,
+ container,
+ firstTitleStyle,
+ secondTitleStyle,
+} from "./HeaderSearchBar.style";
+
+export interface HeaderSearchBarProps extends SearchBoxProps {
+ /** Bold leading title (top line). */
+ firstTitle?: string;
+ /** Bold trailing title (bottom line). */
+ secondTitle?: string;
+ /** Drop shadow color of the header. */
+ shadowColor?: string;
+ /** Replace the hamburger icon with a custom component. */
+ iconComponent?: React.ReactNode;
+ /** Header background color. */
+ backgroundColor?: string;
+ /** Color of the first title. */
+ firstTitleColor?: string;
+ /** Color of the second title. */
+ secondTitleColor?: string;
+ /** Font size of the first title. */
+ firstTitleFontSize?: number;
+ /** Font size of the second title. */
+ secondTitleFontSize?: number;
+ /** Called when the hamburger icon is pressed. */
+ onPressHamburgerIcon?: () => void;
+}
+
+const HeaderSearchBar = React.forwardRef<
+ React.ComponentRef,
+ HeaderSearchBarProps
+>((props, ref) => {
+ const {
+ firstTitle = "Find your",
+ secondTitle = "Favorite Art Work",
+ shadowColor = "#000",
+ iconComponent,
+ backgroundColor = "#fff",
+ firstTitleColor = "#959597",
+ secondTitleColor = "#34343b",
+ firstTitleFontSize = 18,
+ secondTitleFontSize = 18,
+ onPressHamburgerIcon,
+ } = props;
+
+ return (
+
+
+
+
+ {iconComponent || (
+
+ )}
+
+
+
+ {firstTitle}
+
+
+ {secondTitle}
+
+
+
+
+
+
+
+
+ );
+});
+
+HeaderSearchBar.displayName = "HeaderSearchBar";
+
+export default React.memo(HeaderSearchBar);
diff --git a/src/__tests__/HeaderClassicSearchBar.test.tsx b/src/__tests__/HeaderClassicSearchBar.test.tsx
new file mode 100644
index 0000000..d264c11
--- /dev/null
+++ b/src/__tests__/HeaderClassicSearchBar.test.tsx
@@ -0,0 +1,34 @@
+import { render, fireEvent } from "@testing-library/react-native";
+import HeaderClassicSearchBar from "../HeaderClassicSearchBar/HeaderClassicSearchBar";
+
+describe("HeaderClassicSearchBar", () => {
+ it("renders the default search placeholder", () => {
+ const { getByPlaceholderText } = render();
+ expect(getByPlaceholderText("What are you looking for?")).toBeTruthy();
+ });
+
+ it("wires onChangeText through to the SearchBox", () => {
+ const onChangeText = jest.fn();
+ const { getByPlaceholderText } = render(
+ ,
+ );
+ fireEvent.changeText(getByPlaceholderText("Find"), "query");
+ expect(onChangeText).toHaveBeenCalledWith("query");
+ });
+
+ it("renders the filter icon", () => {
+ const { getByText } = render();
+ expect(getByText("filter")).toBeTruthy();
+ });
+
+ it("does not crash without a SafeAreaProvider (uses useSafeAreaInsets)", () => {
+ expect(() => render()).not.toThrow();
+ });
+
+ it("matches snapshot", () => {
+ expect(render().toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/__tests__/HeaderSearchBar.test.tsx b/src/__tests__/HeaderSearchBar.test.tsx
new file mode 100644
index 0000000..1257896
--- /dev/null
+++ b/src/__tests__/HeaderSearchBar.test.tsx
@@ -0,0 +1,60 @@
+import { Text } from "react-native";
+import { render, fireEvent } from "@testing-library/react-native";
+import HeaderSearchBar from "../HeaderSearchBar/HeaderSearchBar";
+
+describe("HeaderSearchBar", () => {
+ it("renders the default first and second titles", () => {
+ const { getByText } = render();
+ expect(getByText("Find your")).toBeTruthy();
+ expect(getByText("Favorite Art Work")).toBeTruthy();
+ });
+
+ it("honors custom title props", () => {
+ const { getByText } = render(
+ ,
+ );
+ expect(getByText("Hello")).toBeTruthy();
+ expect(getByText("World")).toBeTruthy();
+ });
+
+ it("applies secondTitleFontSize to the second title (regression: it used to be ignored)", () => {
+ const { getByText } = render(
+ ,
+ );
+ const node = getByText("Sized");
+ const flat = Array.isArray(node.props.style)
+ ? Object.assign({}, ...node.props.style)
+ : node.props.style;
+ expect(flat.fontSize).toBe(42);
+ });
+
+ it("calls onPressHamburgerIcon when the menu button is pressed", () => {
+ const onPress = jest.fn();
+ const { getByLabelText } = render(
+ ,
+ );
+ fireEvent.press(getByLabelText("Open menu"));
+ expect(onPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("forwards search box props through to the inner SearchBox", () => {
+ const onChangeText = jest.fn();
+ const { getByPlaceholderText } = render(
+ ,
+ );
+ fireEvent.changeText(getByPlaceholderText("Inner"), "abc");
+ expect(onChangeText).toHaveBeenCalledWith("abc");
+ });
+
+ it("renders a custom iconComponent (shared by the hamburger and the inner SearchBox)", () => {
+ const { getAllByText } = render(
+ my-menu} />,
+ );
+ // iconComponent is forwarded to both the hamburger slot and SearchBox.
+ expect(getAllByText("my-menu").length).toBeGreaterThanOrEqual(1);
+ });
+
+ it("matches snapshot", () => {
+ expect(render().toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/src/__tests__/SearchBox.test.tsx b/src/__tests__/SearchBox.test.tsx
new file mode 100644
index 0000000..66993cc
--- /dev/null
+++ b/src/__tests__/SearchBox.test.tsx
@@ -0,0 +1,61 @@
+import { Text } from "react-native";
+import { render, fireEvent } from "@testing-library/react-native";
+import SearchBox from "../components/SearchBox/SearchBox";
+
+describe("SearchBox", () => {
+ it("renders the default placeholder text", () => {
+ const { getByPlaceholderText } = render();
+ expect(getByPlaceholderText("What are you looking for?")).toBeTruthy();
+ });
+
+ it("renders a custom placeholder via searchBoxText", () => {
+ const { getByPlaceholderText } = render(
+ ,
+ );
+ expect(getByPlaceholderText("Search products")).toBeTruthy();
+ });
+
+ it("wires onChangeText to the TextInput", () => {
+ const onChangeText = jest.fn();
+ const { getByPlaceholderText } = render(
+ ,
+ );
+ fireEvent.changeText(getByPlaceholderText("Search"), "hello");
+ expect(onChangeText).toHaveBeenCalledWith("hello");
+ });
+
+ it("renders a static label instead of a TextInput when disableTextInput is true", () => {
+ const { queryByPlaceholderText, getByText } = render(
+ ,
+ );
+ expect(queryByPlaceholderText("Static label")).toBeNull();
+ expect(getByText("Static label")).toBeTruthy();
+ });
+
+ it("renders a custom iconComponent in place of the default icon", () => {
+ const { getByText, queryByText } = render(
+ custom-icon} />,
+ );
+ expect(getByText("custom-icon")).toBeTruthy();
+ // default EvilIcons "search" icon should not be rendered
+ expect(queryByText("search")).toBeNull();
+ });
+
+ it("fires searchBoxOnPress when the container is pressed", () => {
+ const searchBoxOnPress = jest.fn();
+ const { getByText } = render(
+ ,
+ );
+ fireEvent.press(getByText("press-target"));
+ expect(searchBoxOnPress).toHaveBeenCalledTimes(1);
+ });
+
+ it("matches snapshot", () => {
+ const tree = render().toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+});
diff --git a/src/__tests__/__snapshots__/HeaderClassicSearchBar.test.tsx.snap b/src/__tests__/__snapshots__/HeaderClassicSearchBar.test.tsx.snap
new file mode 100644
index 0000000..19ae42d
--- /dev/null
+++ b/src/__tests__/__snapshots__/HeaderClassicSearchBar.test.tsx.snap
@@ -0,0 +1,158 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HeaderClassicSearchBar matches snapshot 1`] = `
+
+
+
+
+
+
+ search
+
+
+
+
+
+
+ filter
+
+
+
+
+
+`;
diff --git a/src/__tests__/__snapshots__/HeaderSearchBar.test.tsx.snap b/src/__tests__/__snapshots__/HeaderSearchBar.test.tsx.snap
new file mode 100644
index 0000000..612d906
--- /dev/null
+++ b/src/__tests__/__snapshots__/HeaderSearchBar.test.tsx.snap
@@ -0,0 +1,194 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`HeaderSearchBar matches snapshot 1`] = `
+
+
+
+
+
+ menu
+
+
+
+
+ Find your
+
+
+ Favorite Art Work
+
+
+
+
+
+
+
+ search
+
+
+
+
+
+
+
+`;
diff --git a/src/__tests__/__snapshots__/SearchBox.test.tsx.snap b/src/__tests__/__snapshots__/SearchBox.test.tsx.snap
new file mode 100644
index 0000000..56476ad
--- /dev/null
+++ b/src/__tests__/__snapshots__/SearchBox.test.tsx.snap
@@ -0,0 +1,76 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchBox matches snapshot 1`] = `
+
+
+
+ search
+
+
+
+
+`;
diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
new file mode 100644
index 0000000..329a6d7
--- /dev/null
+++ b/src/__tests__/index.test.ts
@@ -0,0 +1,18 @@
+import DefaultExport, {
+ HeaderSearchBar,
+ HeaderClassicSearchBar,
+} from "../index";
+
+describe("public API surface", () => {
+ it("exposes the named component exports", () => {
+ expect(HeaderSearchBar).toBeDefined();
+ expect(HeaderClassicSearchBar).toBeDefined();
+ });
+
+ it("preserves the default object export shape", () => {
+ expect(DefaultExport).toEqual({
+ HeaderSearchBar,
+ HeaderClassicSearchBar,
+ });
+ });
+});
diff --git a/src/components/SearchBox/SearchBox.style.ts b/src/components/SearchBox/SearchBox.style.ts
new file mode 100644
index 0000000..1999e70
--- /dev/null
+++ b/src/components/SearchBox/SearchBox.style.ts
@@ -0,0 +1,41 @@
+import { isAndroid } from "@freakycoder/react-native-helpers";
+import type {
+ DimensionValue,
+ StyleProp,
+ TextStyle,
+ ViewStyle,
+} from "react-native";
+
+export function searchBoxContainer(
+ searchBoxWidth: DimensionValue | undefined,
+ searchBoxBorderRadius: number | undefined,
+ searchBoxBackgroundColor: string | undefined,
+): StyleProp {
+ return {
+ left: 8,
+ padding: 6,
+ alignItems: "center",
+ width: searchBoxWidth,
+ justifyContent: "center",
+ height: isAndroid ? 30 : undefined,
+ borderRadius: searchBoxBorderRadius,
+ backgroundColor: searchBoxBackgroundColor,
+ };
+}
+
+const styles: { searchBoxGlue: ViewStyle; searchBoxTextStyle: TextStyle } = {
+ searchBoxGlue: {
+ alignItems: "center",
+ flexDirection: "row",
+ },
+ searchBoxTextStyle: {
+ left: 3,
+ width: "85%",
+ fontSize: 13,
+ color: "#000",
+ height: isAndroid ? 50 : undefined,
+ backgroundColor: "transparent",
+ },
+};
+
+export default styles;
diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx
new file mode 100644
index 0000000..686a9ce
--- /dev/null
+++ b/src/components/SearchBox/SearchBox.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+import Icon from "react-native-dynamic-vector-icons";
+import {
+ Text,
+ View,
+ TextInput,
+ TouchableOpacity,
+ type DimensionValue,
+ type StyleProp,
+ type TextStyle,
+} from "react-native";
+import styles, { searchBoxContainer } from "./SearchBox.style";
+
+export interface SearchBoxProps {
+ /** Controlled value for the inner TextInput. */
+ value?: string;
+ /** Called when the inner TextInput receives focus. */
+ onFocus?: () => void;
+ /** Icon name passed to the leading icon. */
+ iconName?: string;
+ /** Icon set/type passed to the leading icon. */
+ iconType?: string;
+ /** Leading icon size. */
+ iconSize?: number;
+ /** Leading icon color. */
+ iconColor?: string;
+ /** Called whenever the search text changes. */
+ onChangeText?: (text: string) => void;
+ /** Placeholder text (or static label when `disableTextInput` is true). */
+ searchBoxText?: string;
+ /** Replace the leading icon with a custom component. */
+ iconComponent?: React.ReactNode;
+ /** Width of the search box container. */
+ searchBoxWidth?: DimensionValue;
+ /** Called when the search box container itself is pressed. */
+ searchBoxOnPress?: () => void;
+ /** Render a static label instead of an editable TextInput. */
+ disableTextInput?: boolean;
+ /** Style override for the inner text element. */
+ searchBoxTextStyle?: StyleProp;
+ /** Border radius of the search box container. */
+ searchBoxBorderRadius?: number;
+ /** Background color of the search box container. */
+ searchBoxBackgroundColor?: string;
+}
+
+const SearchBox = React.forwardRef<
+ React.ComponentRef,
+ SearchBoxProps
+>(
+ (
+ {
+ value,
+ onFocus,
+ iconName = "search",
+ iconType = "EvilIcons",
+ iconSize = 25,
+ iconColor = "black",
+ onChangeText,
+ searchBoxText = "What are you looking for?",
+ iconComponent,
+ searchBoxWidth = "95%",
+ searchBoxOnPress,
+ disableTextInput = false,
+ searchBoxTextStyle,
+ searchBoxBorderRadius = 8,
+ searchBoxBackgroundColor = "#f5f5f5",
+ },
+ ref,
+ ) => {
+ const renderTextInput = () => {
+ if (disableTextInput) {
+ return {searchBoxText};
+ }
+ return (
+
+ );
+ };
+
+ return (
+
+
+ {iconComponent || (
+
+ )}
+ {renderTextInput()}
+
+
+ );
+ },
+);
+
+SearchBox.displayName = "SearchBox";
+
+export default React.memo(SearchBox);
diff --git a/src/declarations.d.ts b/src/declarations.d.ts
new file mode 100644
index 0000000..e9f6cc2
--- /dev/null
+++ b/src/declarations.d.ts
@@ -0,0 +1,49 @@
+/**
+ * Ambient module declarations for native peer dependencies that do not ship
+ * their own TypeScript types. These keep `tsc --noEmit` honest without
+ * pretending to fully type third-party libraries.
+ */
+declare module "react-native-androw" {
+ import type { ComponentType } from "react";
+ import type { ViewProps } from "react-native";
+
+ const Androw: ComponentType;
+ export default Androw;
+}
+
+declare module "react-native-dynamic-vector-icons" {
+ import type { ComponentType } from "react";
+ import type { TextProps } from "react-native";
+
+ export interface DynamicVectorIconProps extends TextProps {
+ name: string;
+ type?: string;
+ size?: number;
+ color?: string;
+ }
+
+ const Icon: ComponentType;
+ export default Icon;
+}
+
+declare module "react-native-material-ripple" {
+ import type { ComponentType, ReactNode } from "react";
+ import type { ViewProps } from "react-native";
+
+ export interface RippleProps extends ViewProps {
+ rippleColor?: string;
+ rippleContainerBorderRadius?: number;
+ onPress?: () => void;
+ children?: ReactNode;
+ }
+
+ const Ripple: ComponentType;
+ export default Ripple;
+}
+
+declare module "@freakycoder/react-native-helpers" {
+ export const isAndroid: boolean;
+ export function hasNotch(): boolean;
+ export function isIPhoneXFamily(): boolean;
+ export function getStatusBarHeight(safe?: boolean): number;
+}
diff --git a/lib/src/index.js b/src/index.tsx
similarity index 51%
rename from lib/src/index.js
rename to src/index.tsx
index d9c8d1c..4bb2795 100644
--- a/lib/src/index.js
+++ b/src/index.tsx
@@ -2,4 +2,8 @@ import HeaderSearchBar from "./HeaderSearchBar/HeaderSearchBar";
import HeaderClassicSearchBar from "./HeaderClassicSearchBar/HeaderClassicSearchBar";
export { HeaderSearchBar, HeaderClassicSearchBar };
+export type { HeaderSearchBarProps } from "./HeaderSearchBar/HeaderSearchBar";
+export type { HeaderClassicSearchBarProps } from "./HeaderClassicSearchBar/HeaderClassicSearchBar";
+export type { SearchBoxProps } from "./components/SearchBox/SearchBox";
+
export default { HeaderSearchBar, HeaderClassicSearchBar };
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 0000000..6a95b63
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": [
+ "node_modules",
+ "lib",
+ "example",
+ "**/__tests__/**",
+ "**/*.test.tsx",
+ "**/*.test.ts"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e538f90
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["esnext"],
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "declaration": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "lib", "example"]
+}