diff --git a/.eslintrc.json b/.eslintrc.json index b825584..27fcff2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,29 +1,29 @@ { - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - "plugin:react/jsx-runtime" - ], - "settings": { - "react": { - "version": "detect" - } - }, - "overrides": [], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": ["react", "@typescript-eslint"], - "rules": { - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-empty-function": "off" - } + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react-hooks/recommended", + "plugin:react/jsx-runtime" + ], + "settings": { + "react": { + "version": "detect" + } + }, + "overrides": [], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["react", "@typescript-eslint"], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-empty-function": "off" + } } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36689c2..cf3506e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,28 +1,22 @@ name: Tests -on: - pull_request: - push: - workflow_call: +on: [push, pull_request] jobs: - cypress-run: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: 20 - - uses: pnpm/action-setup@v2 - name: Install pnpm - with: - version: 8 - run_install: true - - name: Test with Cypress - uses: cypress-io/github-action@v5 - with: - env: CI=true - component: true - install: true - browser: chrome + cypress-run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16.x' + - name: Install dependencies + run: yarn + - name: Install Linux Webkit deps + run: npx playwright install-deps webkit + - name: Cypress Chrome + uses: cypress-io/github-action@v5 + with: + install: false + component: true + browser: chrome diff --git a/.gitignore b/.gitignore index 68a47bf..7aada72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,10 @@ +yarn-error.log* node_modules dist - .vscode/* !.vscode/extensions.json .DS_Store - cypress/downloads cypress/videos -cypress/screenshots - *.tgz -.eslintcache - -pnpm-lock.yaml +.eslintcache \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 2c68508..fdc9424 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,17 +1,15 @@ { - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 120, - "useTabs": false, - "tabWidth": 3, - "semi": false, - "overrides": [ - { - "files": "README.md", - "options": { - "printWidth": 80, - "tabWidth": 2 - } - } - ] + "trailingComma": "none", + "singleQuote": true, + "printWidth": 100, + "useTabs": true, + "overrides": [ + { + "files": "README.md", + "options": { + "printWidth": 90, + "useTabs": false + } + } + ] } diff --git a/LICENSE b/LICENSE index f9fcbe6..9e4ebec 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ -Copyright (c) 2023-present Simone Mastromattei +Copyright (c) 2022 Simone Mastromattei 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 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index c64fc39..5cad36b 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,64 @@ -# React Email Autocomplete - ![npm](https://img.shields.io/npm/v/@smastrom/react-email-autocomplete?color=46c119) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/smastrom/react-email-autocomplete/tests.yml?branch=main&label=tests) ![dependency-count](https://img.shields.io/badge/dependency%20count-0-success) -| Before typing `@` | After typing `@` (optional) | -| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| ![@smastrom/react-email-autocomplete](https://i.ibb.co/SNTkHJQ/Screenshot-2022-12-07-alle-13-50-59.png) | ![@smastrom/react-email-autocomplete](https://i.ibb.co/DWQBQw7/Screenshot-2022-12-07-alle-13-54-23.png) | +# React Email Autocomplete -
+[Demo and examples](https://@smastrom/react-email-autocomplete.netlify.app) — [Stackblitz](https://stackblitz.com/edit/react-4kufqv?file=src/App.js) — [NextJS SSR](https://stackblitz.com/edit/nextjs-6ttxsv?file=pages%2F_app.js) + +![@smastrom/react-email-autocomplete](https://i.ibb.co/DWQBQw7/Screenshot-2022-12-07-alle-13-54-23.png) -**React Email Autocomplete** is an unstyled, zero-dependency component inspired by some european flight booking websites. As soon as users start typing their email address, it will suggest the most common email providers. +**React Email Autocomplete** is an unstyled, zero-dependency controlled component inspired by multiple european flight booking websites. As soon as users start typing their email address, it will suggest the most common email providers. - Completely unstyled and white labeled (ships with zero CSS) -- Fully accessible with superlative keyboard controls -- Forward any event and attribute to the `` element or control it with React Hook Form +- Fully accessible with great keyboard controls +- Forward most common event handlers and attributes +- Controllable with React Hook Form -[Demo and examples](https://@smastrom/react-email-autocomplete.netlify.app) — [Stackblitz](https://stackblitz.com/edit/react-4kufqv?file=src/App.js) — [NextJS](https://stackblitz.com/edit/nextjs-6ttxsv?file=pages%2F_app.js) +> :bulb: **React Email Autocomplete** also ships with a curated list of ~160 world's most popular email providers to get started quicky (thanks to **@mailcheck**).
## :floppy_disk: Installation ```bash -pnpm add @smastrom/react-email-autocomplete -# npm i @smastrom/react-email-autocomplete -# yarn add @smastrom/react-email-autocomplete +npm i -S @smastrom/react-email-autocomplete +# yarn add @smastrom/react-email-autocomplete +# pnpm add @smastrom/react-email-autocomplete ```
-## :art: Usage / Styling +## :cyclone: Props + +| Prop | Description | Type | Default | Required | +| -------------- | ----------------------------------------------------- | -------------------------------------- | --------- | ------------------ | +| `value` | State or portion of state to hold the email | _string_ | undefined | :white_check_mark: | +| `onChange` | State setter or custom dispatcher to update the email | _OnChange_ | undefined | :white_check_mark: | +| `baseList` | Domains to suggest while typing the username | _string[]_ | undefined | :white_check_mark: | +| `refineList` | Domains to refine suggestions after typing `@` | _string[]_ | [] | :x: | +| `onSelect` | Custom callback on suggestion select | _OnSelect_ | () => {} | :x: | +| `minChars` | Minimum chars required to display suggestions | _1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8_ | 2 | :x: | +| `maxResults` | Maximum number of suggestions to display | _2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8_ | 6 | :x: | +| `placement` | Dropdown placement | _auto_ \| _bottom_ | `auto` | :x: | +| `classNames` | Class names for each element | _ClassNames_ | undefined | :x: | +| `className` | Class name of the wrapper element | _string_ | undefined | :x: | +| `wrapperId` | DOM ID of the wrapper element | _string_ | undefined | :x: | +| `customPrefix` | Custom prefix for dropdown unique ID | _string_ | `rbe_` | :x: | +| `isInvalid` | Value of `aria-invalid` | _boolean_ | undefined | :x: | + +### Are also available: + +- Events: `onBlur`, `onFocus`, `onInput` and `onKeyDown`. + +- Attributes: `id`, `name`, `placeholder`, `required`, `disabled`, `readOnly` and `pattern`. + +- React's `ref`. + +> :bulb: They are all forwarded to the `` element. + +
+ +## :art: Styling The component renders a single `div` with a very simple structure: @@ -41,100 +70,42 @@ Wrapper — div └──[username - span:first-of-type] [@domain.com - span:last-of-type] ``` -Specify `classNames` for each element you'd like to style: +You can either specify `classNames` for any element you'd like to style: ```jsx -import { Email } from '@smastrom/react-email-autocomplete' - -const classNames = { +const myClassNames = { wrapper: 'my-wrapper', input: 'my-input', dropdown: 'my-dropdown', suggestion: 'my-suggestion', username: 'my-username', - domain: 'my-domain', -} - -const baseList = ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'msn.com'] + domain: 'my-domain' +}; function App() { - const [email, setEmail] = useState('') + const [email, setEmail] = useState(''); return ( customSetter(newValue) value={email} /> - ) + ); } ``` -
NextJS App Router -
- -`src/components/Email.tsx` - -```tsx -'use client' - -import { useState } from 'react' -import { Email as EmailAutocomplete } from '@smastrom/react-email-autocomplete' - -const classNames = { - wrapper: 'my-wrapper', - input: 'my-input', - dropdown: 'my-dropdown', - suggestion: 'my-suggestion', - username: 'my-username', - domain: 'my-domain', -} - -const baseList = ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'msn.com'] - -export function Email() { - const [email, setEmail] = useState('') - - return ( - customSetter(newValue) - value={email} - /> - ) -} -``` - -`app/page.tsx` - -```tsx -import { Email } from '@/components/Email' - -export default function Home() { - return ( -
- {/* ... */} - - {/* ... */} -
- ) -} -``` - -
-
TypeScript
```ts -import type { ClassNames } from '@smastrom/react-email-autocomplete' +import type { ClassNames } from '@smastrom/react-email-autocomplete'; const myClassNames: ClassNames = { wrapper: 'my-wrapper', - input: 'my-input', -} + input: 'my-input' +}; ```
@@ -154,6 +125,24 @@ You can add a this property in VSCode's `settings.json` in order to enable autco +Or add a class to the wrapper `div` via `className` prop and target any child: + +```css +.my-wrapper { + /* Wrapper */ +} + +.my-wrapper input { + /* Input field */ +} + +.my-wrapper li > span:first-of-type { + /* Username */ +} + +/* ... */ +``` +
Basic styles
@@ -193,7 +182,7 @@ This package ships with **zero css**. Initial styles enough to see the component ### Focus/Hover styles -Although you can target the pseudo classes `:hover` and `:focus`, it is recommended instead to target the attribute `data-active-email` in order to avoid `:hover` styles to be applied to a suggestion as soon as the dropdown is opened (in case the cursor is hovering it). +Although you can target the pseudo classes `:hover` and `:focus`, it is recommended instead to target the attribute `data-active-email` in order to avoid `:hover` styles to be applied to a suggestion as soon as the dropdown is opened (in case the cursor was hovering it). ```css .my-suggestion[data-active-email='true'] { @@ -205,26 +194,28 @@ Although you can target the pseudo classes `:hover` and `:focus`, it is recommen } ``` -The attribute name can also be customized via `activeDataAttr` prop: +### Dynamic Dropdown Position -```jsx - -``` +According to the available space, the dropdown will automatically be positioned above or below the input field. + +Do not play with `top` or `bottom` properties. Let the component handle it. Just define the positioning and the distance from the input field: ```css -.my-suggestion[data-custom-attr='true'] { - background-color: aliceblue; +.wrapper { + position: relative; +} + +.dropdown { + position: absolute; + margin-top: 5px; /* Distance when placed below */ + margin-bottom: 5px; /* Distance when placed above */ } ``` +The feature can be disabled placing the dropdown always below the input field. Set the `placement` prop to `bottom`. Please note that the above CSS must be added anyway. + +
+ ## :dna: Modes ### 1. Basic Mode @@ -236,7 +227,7 @@ Once users start typing, it displays a list of _base_ suggestions and hides it o | ![@smastrom/react-email-autocomplete](https://i.ibb.co/SNTkHJQ/Screenshot-2022-12-07-alle-13-50-59.png) | ![@smastrom/react-email-autocomplete](https://i.ibb.co/ZgWCPkg/Screenshot-2022-12-07-alle-13-52-46.png) | ```jsx -import { Email } from '@smastrom/react-email-autocomplete' +import { Email } from '@smastrom/react-email-autocomplete'; const baseList = [ 'gmail.com', @@ -244,19 +235,20 @@ const baseList = [ 'hotmail.com', 'aol.com', 'msn.com', - 'proton.me', -] + 'proton.me' +]; function App() { - const [email, setEmail] = useState('') + const [email, setEmail] = useState(''); return ( customSetter(newValue) value={email} /> - ) + ); } ``` @@ -271,7 +263,7 @@ Acts like **Basic Mode** until users type `@` . Then as they start typing the do All you have to do is to provide a second array of domains to `refineList` prop. This package ships with a [curated list](https://github.com/smastrom/@smastrom/react-email-autocomplete/blob/main/src/domains.json) of the ~160 most popular world domains that you can directly import and use (thanks to **@mailcheck**): ```jsx -import { Email, domains } from '@smastrom/react-email-autocomplete' +import { Email, domains } from '@smastrom/react-email-autocomplete'; const baseList = [ 'gmail.com', @@ -279,175 +271,110 @@ const baseList = [ 'hotmail.com', 'aol.com', 'msn.com', - 'proton.me', -] + 'proton.me' +]; function App() { - const [email, setEmail] = useState('') + const [email, setEmail] = useState(''); return ( customSetter(newValue) value={email} /> - ) + ); } ``` -Alternatively, you can use your own array of domains or [search]() for the one that best suits your audience. +Alternatively, you can create your own array of domains or [search]() for the one that more suits your audience.
## :globe_with_meridians: Localization -This package ships with an optional hook that simplifies managing different lists of domains according to the [browser's locale](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language). +It is great to display different suggestions according to [user's browser locale](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language). **React Email Autocomplete** includes a very simple hook to do exactly that. **1 - Create an object and define lists for each browser locale:** ```js -export const emailProviders = { - default: [ - 'gmail.com', - 'yahoo.com', - 'hotmail.com', - 'aol.com', - // ... - ], - it: [ - 'gmail.com', - 'yahoo.com', - 'yahoo.it', - 'tiscali.it', - // ... - ], - 'it-CH': [ - 'gmail.com', - 'outlook.com', - 'bluewin.ch', - 'gmx.de', - // ... - ], -} +export const lists = { + default: ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'msn.com', 'proton.me'], // Required + it: ['gmail.com', 'yahoo.com', 'yahoo.it', 'tiscali.it', 'libero.it', 'outlook.com'], + 'it-CH': ['gmail.com', 'outlook.com', 'bluewin.ch', 'gmx.de', 'libero.it', 'sunrise.ch'] +}; ``` +> :warning: Make sure to define the object outside of your component, otherwise it will be recreated on every render causing an infinite loop. Alternatively you can wrap the object in a `useMemo` hook. +
TypeScript
```ts -import type { LocalizedList } from '@smastrom/react-email-autocomplete' - -export const emailProviders: LocalizedList = { - default: [ - 'gmail.com', - 'yahoo.com', - 'hotmail.com', - 'aol.com', - // ... - ], - // ... -} +import type { LocalizedList } from '@smastrom/react-email-autocomplete'; + +export const lists: LocalizedList = { + default: ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'msn.com', 'proton.me'], // Required + it: ['gmail.com', 'yahoo.com', 'yahoo.it', 'tiscali.it', 'libero.it', 'outlook.com'] +}; ```
-You may define [lang codes](https://www.localeplanet.com/icu/iso639.html) with or without country codes. +You can define [lang codes](https://www.localeplanet.com/icu/iso639.html) with or without country codes. -For languages without country code (such as `it`), by default it will match all browser locales beginning with it such as `it`, `it-CH`, `it-IT` and so on. +If you define a language without country code (such as `it`), by default it will match browser locales such as `it`, `it-CH`, `it-IT` and so on. -For languages with country code (`it-CH`) it will match `it-CH` but not `it` or `it-IT`. +If you define `it-CH` it will match `it-CH` but not `it` or `it-IT`. If you define both `it-CH` and `it`, `it-CH` will match only `it-CH` and `it` will match `it`, `it-IT` and so on. **2 - Use the hook:** ```jsx -import { Email, useLocalizedList } from '@smastrom/react-email-autocomplete' -import { emailProviders } from '@/src/static/locales' +import { lists } from './lists'; +import { Email, useLocalizedList } from '@smastrom/react-email-autocomplete'; function App() { - const baseList = useLocalizedList(emailProviders) - const [email, setEmail] = useState('') + const baseList = useLocalizedList(lists); + const [email, setEmail] = useState(''); return ( customSetter(newValue) value={email} /> - ) + ); } ``` -### Usage with internationalization frameworks or SSR +### Usage with internationalization frameworks -To manually set the locale, pass its code as second argument: +If you prefer to keep the suggestions in line with your app locale instead of the browser's one, you can directly pass the locale string as second argument: ```jsx -import { useRouter } from 'next/router' -import { emailProviders } from '@/src/static/locales' -import { Email, useLocalizedList } from '@smastrom/react-email-autocomplete' +import lists from './lists'; +import { useRouter } from 'next/router'; +import { Email, useLocalizedList } from '@smastrom/react-email-autocomplete'; function App() { - const { locale } = useRouter() - const baseList = useLocalizedList(emailProviders, locale) + const { locale } = useRouter(); + const baseList = useLocalizedList(lists, locale); - const [email, setEmail] = useState('') + const [email, setEmail] = useState(''); return ( customSetter(newValue) value={email} /> - ) -} -``` - -Or with NextJS App router: - -`src/components/Email.tsx` - -```tsx -'use client' - -import { - Email as EmailAutocomplete, - useLocalizedList, -} from '@smastrom/react-email-autocomplete' -import { emailProviders } from '@/src/static/locales' - -export function Email({ lang }: { lang: string }) { - const baseList = useLocalizedList(emailProviders, lang) - const [email, setEmail] = useState('') - - return ( - - ) -} -``` - -`app/page.tsx` - -```tsx -import { Email } from '@/components/Email' -import { headers } from 'next/headers' - -export default function Home() { - const headersList = headers() - const lang = headersList.get('accept-language')?.split(',')[0] - - return ( -
- -
- ) + ); } ``` @@ -455,26 +382,28 @@ export default function Home() { ## :8ball: onSelect callback -To invoke a callback everytime a suggestion is selected (either with mouse or keyboard), pass a callback to `onSelect` prop: +If you need to invoke a callback everytime a suggestion is selected (either with mouse or keyboard), you can do that by passing a function to `onSelect` prop: ```jsx -import { Email } from '@smastrom/react-email-autocomplete' - -function handleSelect(data) { - console.log(data) // { value: 'johndoe@gmail.com', keyboard: true, position: 0 } -} +import { Email, domains } from '@smastrom/react-email-autocomplete'; function App() { - const [email, setEmail] = useState('') + const [email, setEmail] = useState(''); + + function handleSelect({ value, keyboard, position }) { + console.log(value, keyboard, position); + } return ( customSetter(newValue) onSelect={handleSelect} value={email} /> - ) + ); } ``` @@ -483,36 +412,21 @@ function App() { ```ts type OnSelectData = { - value: string - keyboard: boolean - position: number -} + value: string; + keyboard: boolean; + position: number; +}; -type OnSelect = (object: OnSelectData) => void | Promise +type OnSelect = (object: OnSelectData) => void | Promise; ```

-## :cyclone: Props - -| Prop | Description | Type | Default | Required | -| -------------- | ----------------------------------------------------- | -------------------------------------- | --------- | ------------------ | -| `value` | State or portion of state that holds the email value | _string_ | undefined | :white_check_mark: | -| `onChange` | State setter or custom dispatcher to update the email | _OnChange_ | undefined | :white_check_mark: | -| `baseList` | Domains to suggest while typing the username | _string[]_ | undefined | :white_check_mark: | -| `refineList` | Domains to refine suggestions after typing `@` | _string[]_ | [] | :x: | -| `onSelect` | Custom callback on suggestion select | _OnSelect_ | () => {} | :x: | -| `minChars` | Minimum chars required to display suggestions | _1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8_ | 2 | :x: | -| `maxResults` | Maximum number of suggestions to display | _2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8_ | 6 | :x: | -| `classNames` | Class names for each element | _ClassNames_ | undefined | :x: | -| `className` | Class name of the wrapper element | _string_ | undefined | :x: | -| `wrapperId` | DOM ID of the wrapper element | _string_ | undefined | :x: | -| `customPrefix` | Custom prefix for dropdown unique ID | _string_ | `rbe_` | :x: | -| `isInvalid` | Value of `aria-invalid` | _boolean_ | undefined | :x: | +## React Hook Form -:bulb: React's `ref` and any other `HTMLInputElement` attribute can be passed as prop to the component and it will be forwarded to the input element. +No special configuration needed, it just works. Just follow the official React Hook Form's [Controller documentation](https://react-hook-form.com/api/usecontroller/controller).
@@ -527,12 +441,6 @@ type OnSelect = (object: OnSelectData) => void | Promise
-## React Hook Form - -No special configuration needed, it just works. Just follow the official React Hook Form's [Controller documentation](https://react-hook-form.com/api/usecontroller/controller). - -
- ## :dvd: License MIT diff --git a/app/App.tsx b/app/App.tsx index ca2cbb4..2901c04 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -1,22 +1,22 @@ -import { useLayoutEffect } from 'react' -import { Header } from './Header' -import { Footer } from './Footer' -import { BasicMode } from './Examples/BasicMode' -import { RefineMode } from './Examples/RefineMode' -import { EventsChildren } from './Examples/EventsChildren' +import { useLayoutEffect } from 'react'; +import { Header } from './Header'; +import { Footer } from './Footer'; +import { BasicMode } from './Examples/BasicMode'; +import { RefineMode } from './Examples/RefineMode'; +import { EventsChildren } from './Examples/EventsChildren'; export function App() { - useLayoutEffect(() => { - document.getElementsByTagName('input')[0].focus() - }, []) + useLayoutEffect(() => { + document.getElementsByTagName('input')[0].focus(); + }, []); - return ( - <> -
- - - -