├── .nvmrc ├── pnpm-workspace.yaml ├── packages ├── example │ ├── src │ │ ├── vite-env.d.ts │ │ ├── App.css │ │ ├── main.tsx │ │ ├── index.css │ │ ├── favicon.svg │ │ └── App.tsx │ ├── .gitignore │ ├── vite.config.ts │ ├── index.html │ ├── tsconfog.json │ └── package.json └── react-dadata │ ├── src │ ├── http-cache │ │ ├── index.ts │ │ ├── types.ts │ │ ├── abstract.ts │ │ └── default-cache.ts │ ├── variants │ │ ├── email │ │ │ ├── email-types.ts │ │ │ └── email-suggestions.tsx │ │ ├── fio │ │ │ ├── fio-types.ts │ │ │ └── fio-suggestions.tsx │ │ ├── bank │ │ │ ├── bank-types.ts │ │ │ └── bank-suggestions.tsx │ │ ├── party_belarus │ │ │ ├── party-belarus-types.ts │ │ │ └── party-belarus-suggestions.tsx │ │ ├── party_kazakhstan │ │ │ ├── party-kazakhstan-types.ts │ │ │ └── party-kazakhstan-suggestions.tsx │ │ ├── address │ │ │ ├── address-suggestions.tsx │ │ │ └── address-types.ts │ │ └── party_russia │ │ │ ├── party-russia-types.ts │ │ │ └── party-russia-suggestions.tsx │ ├── highlight-words.tsx │ ├── core-types.ts │ ├── request.ts │ ├── react-dadata.css │ ├── index.tsx │ ├── __tests__ │ │ ├── default-http-cache.test.ts │ │ ├── PartySuggestions.test.tsx │ │ └── AddressSuggestions.test.tsx │ └── base-suggestions.tsx │ ├── tsconfig.build.json │ ├── .npmignore │ ├── tsconfig.types.json │ ├── setupTests.ts │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── package.json │ └── readme.md ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── renovate.yml │ ├── linter.yml │ ├── size-limit.yml │ ├── build.yml │ └── test.yml ├── .vscode └── settings.json ├── renovate.json ├── biome.json ├── package.json ├── license └── readme.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.15.1 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /packages/example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | pnpm-lock.yaml 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | dist/ 4 | coverage/ 5 | .DS_Store 6 | .vitest-preview/ 7 | packages/*/license 8 | -------------------------------------------------------------------------------- /packages/example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | margin: 10% auto 16px; 4 | max-width: 700px; 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-dadata/src/http-cache/index.ts: -------------------------------------------------------------------------------- 1 | export { HttpCache } from './abstract'; 2 | export { DefaultHttpCache } from './default-cache'; 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /packages/example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactRefresh from '@vitejs/plugin-react-refresh'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [reactRefresh()], 6 | }); 7 | -------------------------------------------------------------------------------- /packages/react-dadata/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "exclude": [ 7 | "./setupTests.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-dadata/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | pnpm-lock.yaml 3 | 4 | .editorconfig 5 | 6 | *.log 7 | *.tgz 8 | 9 | .idea 10 | .vscode 11 | src/ 12 | tsconfig.json 13 | setupTests.* 14 | *__tests__* 15 | .github 16 | coverage/ 17 | example/ 18 | -------------------------------------------------------------------------------- /packages/react-dadata/tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./dist" 7 | }, 8 | "exclude": [ 9 | "./setupTests.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /packages/react-dadata/src/http-cache/types.ts: -------------------------------------------------------------------------------- 1 | export interface HttpCacheEntry { 2 | data: unknown; 3 | expires: number; 4 | } 5 | 6 | export interface SerializeCacheKeyPayload { 7 | headers?: Record; 8 | method?: string; 9 | url: string; 10 | body?: Record; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-dadata/setupTests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import '@testing-library/jest-dom/vitest'; 3 | import { cleanup } from '@testing-library/react'; 4 | import { afterEach } from 'vitest'; 5 | import "./src/react-dadata.css"; 6 | 7 | afterEach(() => { 8 | cleanup(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/react-dadata/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "jsdom", 6 | setupFiles: ["./setupTests.ts"], 7 | coverage: { 8 | reporter: ["text", "json", "clover", "lcov"], 9 | include: ["src/"], 10 | exclude: ["**/__tests__/**/*.*"], 11 | }, 12 | css: true, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React-DaData Example 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | schedule: 4 | - cron: '0 */4 * * *' 5 | jobs: 6 | renovate: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Self-hosted Renovate 12 | uses: renovatebot/github-action@v36.0.0 13 | with: 14 | configurationFile: renovate.json 15 | token: ${{ secrets.RENOVATE_TOKEN }} 16 | -------------------------------------------------------------------------------- /packages/example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/email/email-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | 3 | export interface DaDataEmail { 4 | local: Nullable; 5 | domain: Nullable; 6 | // type, qc и source зарезервированы для стандартизации и приходят как null в подсказках 7 | type: null; 8 | qc: null; 9 | source: null; 10 | } 11 | 12 | export type DaDataEmailSuggestion = DaDataSuggestion; 13 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/fio/fio-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | 3 | export type DaDataGender = 'MALE' | 'FEMALE' | 'UNKNOWN'; 4 | 5 | export interface DaDataFio { 6 | surname: Nullable; 7 | name: Nullable; 8 | patronymic: Nullable; 9 | gender: DaDataGender; 10 | qc: '0' | '1'; 11 | source: null; 12 | } 13 | 14 | export type DaDataFioSuggestion = DaDataSuggestion; 15 | -------------------------------------------------------------------------------- /packages/react-dadata/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "lib": ["es6", "es2017", "dom"], 5 | "module": "es6", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "target": "esnext", 9 | "skipLibCheck": true, 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "declaration": false, 14 | "typeRoots": ["node_modules/@types", "typings"] 15 | }, 16 | "include": [ 17 | "./src/*", 18 | "./setupTests.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/bower_components": true, 5 | "**/*.code-search": true, 6 | "**/.git": true, 7 | "**/.idea": true, 8 | "**/.svn": true, 9 | "**/.hg": true, 10 | "**/CVS": true, 11 | "**/.DS_Store": true, 12 | "**/Thumbs.db": true, 13 | "coverage/": true, 14 | "dist/": true 15 | }, 16 | "editor.codeActionsOnSave":{ 17 | "source.organizeImports.biome": "explicit", 18 | "quickfix.biome": "explicit" 19 | }, 20 | "[javascript]": { 21 | "editor.defaultFormatter": "biomejs.biome" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/example/tsconfog.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react" 18 | }, 19 | "include": ["./src"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dadata-example", 3 | "private": true, 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "tsc && vite build", 7 | "serve": "vite preview", 8 | "test:lint-package": "biome check src/" 9 | }, 10 | "dependencies": { 11 | "react": "^18.0.0", 12 | "react-dom": "^18.0.0", 13 | "react-dadata": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.0", 17 | "@types/react-dom": "^18.0.0", 18 | "@vitejs/plugin-react-refresh": "^1.3.1", 19 | "typescript": "^5.0.0", 20 | "vite": "^4.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Biome 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the repository 14 | uses: actions/checkout@v4 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v4 17 | - name: Install Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: pnpm 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile --ignore-scripts 24 | - name: Linter 25 | run: pnpm test:lint 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: Size Limit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | size-limit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the repository 14 | uses: actions/checkout@v4 15 | - name: Install pnpm 16 | uses: pnpm/action-setup@v4 17 | - name: Install Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: pnpm 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile --ignore-scripts 24 | - name: Run Size Limit 25 | run: pnpm --filter react-dadata test:size-limit 26 | 27 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "mergeConfidence:all-badges"], 4 | "platform": "github", 5 | "repositories": ["vitalybaev/react-dadata"], 6 | "packageRules": [ 7 | { 8 | "matchManagers": ["npm"], 9 | "matchUpdateTypes": ["minor", "patch"], 10 | "automergeType": "branch", 11 | "commitMessagePrefix": "chore(deps):" 12 | }, 13 | { 14 | "matchManagers": ["npm"], 15 | "matchUpdateTypes": ["major"], 16 | "commitMessagePrefix": "feat(deps):" 17 | } 18 | ], 19 | "labels": ["dependencies"], 20 | "minimumReleaseAge": "3 days", 21 | "prConcurrentLimit": 5, 22 | "prHourlyLimit": 2 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | Build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20] 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | - name: Install Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: pnpm 25 | - name: Install dependencies 26 | run: pnpm install --frozen-lockfile --ignore-scripts 27 | - name: Run build 28 | run: pnpm --filter=react-dadata build:ci 29 | 30 | 31 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "formatter": { 4 | "enabled": true, 5 | "include": ["src/**"], 6 | "formatWithErrors": false, 7 | "indentStyle": "space", 8 | "indentWidth": 2, 9 | "lineEnding": "lf", 10 | "lineWidth": 120, 11 | "attributePosition": "auto" 12 | }, 13 | "organizeImports": { "enabled": true }, 14 | "linter": { "enabled": true, "include": ["src/**"], "rules": { "recommended": true } }, 15 | "javascript": { 16 | "formatter": { 17 | "jsxQuoteStyle": "double", 18 | "quoteProperties": "asNeeded", 19 | "trailingCommas": "all", 20 | "semicolons": "always", 21 | "arrowParentheses": "always", 22 | "bracketSpacing": true, 23 | "bracketSameLine": false, 24 | "quoteStyle": "single", 25 | "attributePosition": "auto" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vitalybaev/react-dadata", 3 | "private": true, 4 | "description": "React-компонент для подсказок адресов, организаций и банков с помощью сервиса DaData.ru", 5 | "keywords": [ 6 | "react", 7 | "reactjs", 8 | "dadata", 9 | "suggestions", 10 | "autocomplete", 11 | "address", 12 | "party", 13 | "bank" 14 | ], 15 | "author": "Vitaly Baev ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/vitalybaev/react-dadata/issues" 19 | }, 20 | "scripts": { 21 | "test": "pnpm --filter './packages/**' --if-present test", 22 | "test:lint": "pnpm --filter './packages/**' test:lint-package" 23 | }, 24 | "homepage": "https://vitalybaev.github.io/react-dadata/", 25 | "devDependencies": { 26 | "@biomejs/biome": "1.8.3", 27 | "rimraf": "^5.0.0" 28 | }, 29 | "packageManager": "pnpm@9.15.2" 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20] 15 | steps: 16 | - name: Checkout the repository 17 | uses: actions/checkout@v4 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | - name: Install Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: pnpm 25 | - name: Install dependencies 26 | run: pnpm install --frozen-lockfile --ignore-scripts 27 | - name: Type Check 28 | run: pnpm --filter react-dadata test:type-check 29 | - name: Tests 30 | run: 31 | pnpm test 32 | - name: Coveralls 33 | uses: coverallsapp/github-action@v2 34 | with: 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | base-path: packages/react-dadata 37 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/bank/bank-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | import type { DaDataAddress } from '../../variants/address/address-types'; 3 | 4 | export type DaDataBankType = 'BANK' | 'BANK_BRANCH' | 'NKO' | 'NKO_BRANCH' | 'RKC' | 'OTHER'; 5 | 6 | export type DaDataBankStatus = 'ACTIVE' | 'LIQUIDATING' | 'LIQUIDATED'; 7 | 8 | export interface DaDataBank { 9 | bic: string; 10 | swift: string; 11 | inn: string; 12 | kpp: string; 13 | registration_number: string; 14 | correspondent_account: string; 15 | name: { 16 | payment: string; 17 | full: null; 18 | short: string; 19 | }; 20 | payment_city: string; 21 | opf: { 22 | type: DaDataBankType; 23 | full: null; 24 | short: null; 25 | }; 26 | address: DaDataSuggestion; 27 | state: { 28 | actuality_date: number; 29 | registration_date: number; 30 | liquidation_date: Nullable; 31 | status: DaDataBankStatus; 32 | }; 33 | okpo: null; 34 | phone: number; 35 | rkc: number; 36 | } 37 | 38 | export type DaDataBankSuggestion = DaDataSuggestion; 39 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2022 Vitaly Baev , baev.dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react-dadata/src/highlight-words.tsx: -------------------------------------------------------------------------------- 1 | import highlightWords from 'highlight-words'; 2 | import React, { type ElementType, PureComponent, type ReactNode } from 'react'; 3 | 4 | interface Props { 5 | text: string; 6 | words: string | string[]; 7 | highlightClassName?: string; 8 | tagName?: ElementType; 9 | } 10 | 11 | export class HighlightWords extends PureComponent { 12 | render(): ReactNode { 13 | const { text, words, highlightClassName, tagName = 'span' } = this.props; 14 | const query = typeof words === 'string' ? words : words.join(' '); 15 | 16 | const chunks = highlightWords({ text, query }); 17 | 18 | return ( 19 | 20 | {chunks.map((chunk) => { 21 | if (!chunk.match) { 22 | return ( 23 | 24 | {chunk.text} 25 | 26 | ); 27 | } 28 | 29 | const Component = tagName; 30 | 31 | return ( 32 | 33 | {chunk.text} 34 | 35 | ); 36 | })} 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/email/email-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { DaDataEmail, DaDataEmailSuggestion } from './email-types'; 5 | 6 | type Props = BaseProps; 7 | 8 | export class EmailSuggestions extends BaseSuggestions { 9 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/email'; 10 | 11 | getLoadSuggestionsData = (): Record => { 12 | const { count } = this.props; 13 | const { query } = this.state; 14 | 15 | return { 16 | query, 17 | count: count || 10, 18 | }; 19 | }; 20 | 21 | protected getSuggestionKey = (suggestion: DaDataEmailSuggestion): string => suggestion.value; 22 | 23 | protected renderOption = (suggestion: DaDataEmailSuggestion): ReactNode => { 24 | const { renderOption, highlightClassName } = this.props; 25 | const { query } = this.state; 26 | 27 | return renderOption ? ( 28 | renderOption(suggestion, query) 29 | ) : ( 30 |
31 | 36 |
37 | ); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_belarus/party-belarus-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | 3 | /** 4 | * Типы для подсказок по организациям в Беларуси 🇧🇾 5 | */ 6 | 7 | export type DaDataPartyBelarusType = 'LEGAL' | 'INDIVIDUAL'; 8 | 9 | export type DaDataPartyBelarusStatus = 10 | | 'ACTIVE' 11 | | 'LIQUIDATING' 12 | | 'LIQUIDATED' 13 | | 'REORGANIZING' 14 | | 'BANKRUPT' 15 | | 'SUSPENDED'; 16 | 17 | export interface DaDataPartyBelarus { 18 | unp: string; 19 | registration_date: string; 20 | removal_date: Nullable; 21 | actuality_date: string; 22 | status: DaDataPartyBelarusStatus; 23 | type: DaDataPartyBelarusType; 24 | full_name_ru: string; 25 | full_name_by: Nullable; 26 | short_name_ru: Nullable; 27 | short_name_by: Nullable; 28 | trade_name_ru: Nullable; 29 | trade_name_by: Nullable; 30 | fio_ru: Nullable; 31 | fio_by: Nullable; 32 | address: Nullable; 33 | oked: string; 34 | oked_name: string; 35 | } 36 | 37 | type DaDataBelarusRequestFilterItem = { status: DaDataPartyBelarusStatus } | { type: DaDataPartyBelarusType }; 38 | 39 | export interface DaDataBelarusRequestPayload extends Record { 40 | query: string; 41 | count: number; 42 | filters?: DaDataBelarusRequestFilterItem[]; 43 | } 44 | 45 | export type DaDataPartyBelarusSuggestion = DaDataSuggestion; 46 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_kazakhstan/party-kazakhstan-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion } from '../../core-types'; 2 | 3 | export type DaDataPartyKazakhstanStatus = 'ACTIVE' | 'INACTIVE' | 'LIQUIDATING' | 'LIQUIDATED' | 'SUSPENDED'; 4 | 5 | export type DaDataPartyKazakhstanType = 6 | | 'LEGAL' 7 | | 'BRANCH' 8 | | 'INDIVIDUAL' 9 | | 'INDIVIDUAL_JOINT_VENTURE' 10 | | 'FOREIGN_BRANCH'; 11 | 12 | export interface DaDataPartyKazakhstan { 13 | bin: string; 14 | registration_date: string; 15 | status: DaDataPartyKazakhstanStatus; 16 | type: DaDataPartyKazakhstanType; 17 | name_ru: string; 18 | name_kz: string; 19 | fio: string; 20 | kato: string; 21 | address_ru: string; 22 | address_kz: string; 23 | address_settlement_ru: string; 24 | address_settlement_kz: string; 25 | address_local: string; 26 | oked: string; 27 | oked_name_ru: string; 28 | oked_name_kz: string; 29 | krp: string; 30 | krp_name_ru: string; 31 | krp_name_kz: string; 32 | kse: string; 33 | kse_name_ru: string; 34 | kse_name_kz: string; 35 | kfs: string; 36 | kfs_name_ru: string; 37 | kfs_name_kz: string; 38 | } 39 | 40 | type DaDataPartyKazakhstanRequestFilterItem = { type: DaDataPartyKazakhstanType }; 41 | 42 | export interface DaDataPartyKazakhstanRequestPayload extends Record { 43 | query: string; 44 | count: number; 45 | filters?: DaDataPartyKazakhstanRequestFilterItem[]; 46 | } 47 | 48 | export type DaDataPartyKazakhstanSuggestion = DaDataSuggestion; 49 | -------------------------------------------------------------------------------- /packages/example/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/react-dadata/src/core-types.ts: -------------------------------------------------------------------------------- 1 | import type { ElementType, HTMLProps, ReactNode } from 'react'; 2 | import type { HttpCache } from './http-cache'; 3 | 4 | export type Nullable = T | null; 5 | 6 | export interface DaDataSuggestion { 7 | value: string; 8 | unrestricted_value: string; 9 | data: T; 10 | } 11 | 12 | /** 13 | * Общие пропсы для всех видов компонента подсказов 14 | */ 15 | export interface CommonProps { 16 | token: string; 17 | value?: DaDataSuggestion; 18 | url?: string; 19 | defaultQuery?: string; 20 | autoload?: boolean; 21 | delay?: number; 22 | count?: number; 23 | onChange?: (suggestion?: DaDataSuggestion) => void; 24 | inputProps?: HTMLProps; 25 | hintText?: ReactNode; 26 | renderOption?: (suggestion: DaDataSuggestion, inputValue: string) => ReactNode; 27 | renderNoSuggestions?: () => ReactNode; 28 | containerClassName?: string; 29 | suggestionsClassName?: string; 30 | suggestionClassName?: string; 31 | currentSuggestionClassName?: string; 32 | hintClassName?: string; 33 | highlightClassName?: string; 34 | minChars?: number; 35 | customInput?: ElementType; 36 | selectOnBlur?: boolean; 37 | uid?: string; 38 | /** 39 | * Необходимо ли кешировать HTTP-запросы? 40 | * Возможно передать собственный кеш наследующий {@link HttpCache}. 41 | */ 42 | httpCache?: boolean | HttpCache; 43 | /** 44 | * Время жизни кеша в миллисекундах. 45 | * Игнорируется если был передан собственный {@link HttpCache}. 46 | */ 47 | httpCacheTtl?: number; 48 | children?: ReactNode | undefined; 49 | } 50 | -------------------------------------------------------------------------------- /packages/react-dadata/src/http-cache/abstract.ts: -------------------------------------------------------------------------------- 1 | import type { SerializeCacheKeyPayload } from './types'; 2 | 3 | export abstract class HttpCache { 4 | /** 5 | * Получить данные из кеша 6 | * @param key - Уникальный ключ кеша 7 | * @example 8 | * ```ts 9 | * cache.get('key'); 10 | * ``` 11 | */ 12 | public abstract get(key: string): T | null; 13 | 14 | /** 15 | * Добавить данные в кеш 16 | * @param key - Уникальный ключ кеша 17 | * @param data - Данные для добавления 18 | * @example 19 | * ```ts 20 | * cache.set('key', { ok: true }); 21 | * ``` 22 | */ 23 | public abstract set(key: string, data: unknown, ...rest: unknown[]): unknown; 24 | 25 | /** 26 | * Удалить закешированные данные по ключу 27 | * @param key - Уникальный ключ кеша 28 | * @xample 29 | * ```ts 30 | * cache.delete('key'); 31 | * ``` 32 | */ 33 | public abstract delete(key: string): unknown; 34 | 35 | /** 36 | * Полностью очистить кеш 37 | */ 38 | public abstract reset(): unknown; 39 | 40 | /** 41 | * Сгенерировать уникальный ключ кеша из параметров http-запроса 42 | * @example 43 | * ```ts 44 | * cache.serializeCacheKey({ 45 | * url: 'https://example.com', 46 | * body: { key: "value" }, 47 | * method: "POST" 48 | * }) 49 | * ``` 50 | */ 51 | public serializeCacheKey(payload: SerializeCacheKeyPayload): string { 52 | try { 53 | return JSON.stringify(payload); 54 | } catch (_e) { 55 | // на случай попытки сериализации объекта с циклическими зависимостями внутри 56 | return payload.url + String(Math.random()); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/react-dadata/src/request.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion } from './core-types'; 2 | import type { HttpCache } from './http-cache'; 3 | 4 | export interface RequestOptions { 5 | headers: { [header: string]: string }; 6 | // biome-ignore lint/suspicious/noExplicitAny: 7 | json: any; 8 | } 9 | 10 | let xhr: XMLHttpRequest; 11 | 12 | export const makeRequest = ( 13 | method: string, 14 | endpoint: string, 15 | data: RequestOptions, 16 | cache: HttpCache | null, 17 | onReceiveData: (response: Array>) => void, 18 | ): void => { 19 | if (xhr) { 20 | xhr.abort(); 21 | } 22 | 23 | let cacheKey: string; 24 | if (cache) { 25 | cacheKey = cache.serializeCacheKey({ 26 | headers: data.headers, 27 | body: data.json, 28 | url: endpoint, 29 | method, 30 | }); 31 | const cachedData = cache.get(cacheKey) as Array>; 32 | if (cachedData) { 33 | onReceiveData(cachedData); 34 | return; 35 | } 36 | } 37 | xhr = new XMLHttpRequest(); 38 | xhr.open(method, endpoint); 39 | if (data.headers) { 40 | for (const [header, headerValue] of Object.entries(data.headers)) { 41 | xhr.setRequestHeader(header, headerValue); 42 | } 43 | } 44 | xhr.send(JSON.stringify(data.json)); 45 | 46 | xhr.onreadystatechange = () => { 47 | if (!xhr || xhr.readyState !== 4) { 48 | return; 49 | } 50 | 51 | if (xhr.status === 200) { 52 | const payload = JSON.parse(xhr.response)?.suggestions; 53 | if (payload) { 54 | cache?.set(cacheKey, payload); 55 | onReceiveData(payload); 56 | } 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/fio/fio-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { DaDataFio, DaDataFioSuggestion, DaDataGender } from './fio-types'; 5 | 6 | interface Props extends BaseProps { 7 | filterGender?: DaDataGender[]; 8 | filterParts?: string[]; 9 | } 10 | 11 | export class FioSuggestions extends BaseSuggestions { 12 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/fio'; 13 | 14 | getLoadSuggestionsData = (): Record => { 15 | const { count, filterGender, filterParts } = this.props; 16 | const { query } = this.state; 17 | 18 | const requestPayload: Record = { 19 | query, 20 | count: count || 10, 21 | }; 22 | 23 | // Ограничение по полу 24 | if (filterGender) { 25 | requestPayload.gender = filterGender; 26 | } 27 | 28 | // Ограничение по части ФИО 29 | if (filterParts) { 30 | requestPayload.parts = filterParts; 31 | } 32 | 33 | return requestPayload; 34 | }; 35 | 36 | protected getSuggestionKey = (suggestion: DaDataFioSuggestion): string => 37 | `name:${suggestion.data.name || ''}surname:${suggestion.data.surname || ''}patronymic:${ 38 | suggestion.data.patronymic || '' 39 | }`; 40 | 41 | protected renderOption = (suggestion: DaDataFioSuggestion): ReactNode => { 42 | const { renderOption, highlightClassName } = this.props; 43 | const { query } = this.state; 44 | 45 | return renderOption ? ( 46 | renderOption(suggestion, query) 47 | ) : ( 48 |
49 | 54 |
55 | ); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-dadata/src/react-dadata.css: -------------------------------------------------------------------------------- 1 | .react-dadata__container { 2 | position: relative; 3 | } 4 | 5 | .react-dadata__input { 6 | display: block; 7 | box-sizing: border-box; 8 | height: 38px; 9 | border: 1px solid #ccc; 10 | border-radius: 4px; 11 | width: 100%; 12 | font-size: 16px; 13 | padding: 0 10px; 14 | outline: none; 15 | } 16 | 17 | .react-dadata__input:focus { 18 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 124, 214, 0.3); 19 | border-color: #007CD6; 20 | } 21 | 22 | .react-dadata__suggestions { 23 | position: absolute; 24 | list-style: none; 25 | padding: 0; 26 | margin: 0; 27 | z-index: 10; 28 | background-color: #fff; 29 | box-shadow: 0 1px 6px 3px rgba(0,0,0,.1); 30 | top: calc(100% + 8px); 31 | border-radius: 4px; 32 | overflow: hidden; 33 | left: 0; 34 | right: 0; 35 | text-align: left; 36 | } 37 | 38 | .react-dadata__suggestion-note { 39 | font-size: 14px; 40 | color: #828282; 41 | padding: 10px 10px 5px 10px; 42 | } 43 | 44 | .react-dadata__suggestion { 45 | font-size: 15px; 46 | padding: 7px 10px; 47 | cursor: pointer; 48 | box-sizing: border-box; 49 | width: 100%; 50 | display: block; 51 | background: none; 52 | border: none; 53 | text-align: left; 54 | } 55 | 56 | .react-dadata__suggestion--line-through { 57 | text-decoration: line-through; 58 | } 59 | 60 | .react-dadata__suggestion-subtitle { 61 | font-size: 14px; 62 | margin-top: 4px; 63 | color: #777777; 64 | } 65 | 66 | .react-dadata__suggestion-subtitle-item { 67 | display: inline-block; 68 | margin-right: 16px; 69 | margin-bottom: 4px; 70 | } 71 | 72 | .react-dadata__suggestion-subtitle-item:last-child { 73 | margin-right: 0; 74 | } 75 | 76 | .react-dadata__suggestion--current { 77 | background-color: rgba(0, 124, 214, 0.15); 78 | } 79 | 80 | .react-dadata__suggestion:hover { 81 | background-color: rgba(0, 124, 214, 0.1); 82 | } 83 | 84 | .react-dadata mark { 85 | background: none; 86 | } 87 | 88 | .react-dadata--highlighted { 89 | color: #0094FF; 90 | } 91 | -------------------------------------------------------------------------------- /packages/react-dadata/src/http-cache/default-cache.ts: -------------------------------------------------------------------------------- 1 | import { HttpCache } from './abstract'; 2 | import type { HttpCacheEntry } from './types'; 3 | 4 | const minute = 60000; 5 | 6 | export class DefaultHttpCache extends HttpCache { 7 | private static sharedInstance: DefaultHttpCache; 8 | 9 | private _map = new Map(); 10 | 11 | private _ttl = 10 * minute; 12 | 13 | /** 14 | * Синглтон 15 | * @example 16 | * ```ts 17 | * cache.shared.get('key'); 18 | * ``` 19 | */ 20 | public static get shared(): DefaultHttpCache { 21 | if (!DefaultHttpCache.sharedInstance) { 22 | DefaultHttpCache.sharedInstance = new DefaultHttpCache(); 23 | } 24 | return DefaultHttpCache.sharedInstance; 25 | } 26 | 27 | /** 28 | * Время жизни кеша в миллисекундах 29 | * @example 30 | * ```ts 31 | * cache.ttl = 60000; 32 | * cache.ttl = Infinity; 33 | * cache.tll = 0; 34 | * 35 | * // негативные значения игнорируются 36 | * cache.ttl = -1; 37 | * cache.ttl = Number.NEGATIVE_INFINITY; 38 | * ``` 39 | */ 40 | public get ttl(): number { 41 | return this._ttl; 42 | } 43 | 44 | public set ttl(ttl: number) { 45 | if (typeof ttl === 'number' && ttl >= 0) { 46 | this._ttl = ttl; 47 | } 48 | } 49 | 50 | /** 51 | * Количество элементов в кеше 52 | */ 53 | public get size(): number { 54 | return this._map.size; 55 | } 56 | 57 | public get(key: string) { 58 | const data = this._map.get(key); 59 | if (!data) return null; 60 | if (data.expires <= Date.now()) { 61 | this.delete(key); 62 | return null; 63 | } 64 | return data.data as T; 65 | } 66 | 67 | public set(key: string, data: unknown): this { 68 | this._map.set(key, { 69 | data, 70 | expires: Date.now() + this.ttl, 71 | }); 72 | return this; 73 | } 74 | 75 | public delete(key: string): this { 76 | this._map.delete(key); 77 | return this; 78 | } 79 | 80 | public reset(): this { 81 | this._map.clear(); 82 | return this; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/react-dadata/src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Общие типы 3 | */ 4 | export type { DaDataSuggestion } from './core-types'; 5 | export { HttpCache } from './http-cache'; 6 | 7 | /** 8 | * Адреса 9 | */ 10 | export type { DaDataAddress, DaDataAddressBounds, DaDataAddressSuggestion } from './variants/address/address-types'; 11 | export { AddressSuggestions } from './variants/address/address-suggestions'; 12 | 13 | /** 14 | * Организации в России 🇷🇺 15 | */ 16 | export type { 17 | DaDataPartyRussia, 18 | DaDataPartyType, 19 | DaDataPartyStatus, 20 | DaDataPartyBranchType, 21 | DaDataPartyRussiaFio, 22 | DaDataPartyRussiaSuggestion, 23 | DaDataPartyRussiaStatus, 24 | DaDataPartySuggestion, 25 | } from './variants/party_russia/party-russia-types'; 26 | export { 27 | PartySuggestions, 28 | PartySuggestions as PartyRussiaSuggestions, 29 | } from './variants/party_russia/party-russia-suggestions'; 30 | 31 | /** 32 | * Организации в Беларуси 🇧🇾 33 | */ 34 | export type { 35 | DaDataPartyBelarus, 36 | DaDataPartyBelarusType, 37 | DaDataPartyBelarusStatus, 38 | DaDataPartyBelarusSuggestion, 39 | } from './variants/party_belarus/party-belarus-types'; 40 | export { PartyBelarusSuggestions } from './variants/party_belarus/party-belarus-suggestions'; 41 | 42 | /** 43 | * Организации в Казахстане 🇰🇿 44 | */ 45 | export type { 46 | DaDataPartyKazakhstan, 47 | DaDataPartyKazakhstanSuggestion, 48 | DaDataPartyKazakhstanType, 49 | DaDataPartyKazakhstanStatus, 50 | } from './variants/party_kazakhstan/party-kazakhstan-types'; 51 | export { PartyKazakhstanSuggestions } from './variants/party_kazakhstan/party-kazakhstan-suggestions'; 52 | 53 | /** 54 | * Банки 55 | */ 56 | export type { DaDataBank, DaDataBankStatus, DaDataBankType, DaDataBankSuggestion } from './variants/bank/bank-types'; 57 | export { BankSuggestions } from './variants/bank/bank-suggestions'; 58 | 59 | /** 60 | * ФИО 61 | */ 62 | export type { DaDataFio, DaDataFioSuggestion, DaDataGender } from './variants/fio/fio-types'; 63 | export { FioSuggestions } from './variants/fio/fio-suggestions'; 64 | 65 | /** 66 | * Email 67 | */ 68 | export type { DaDataEmail, DaDataEmailSuggestion } from './variants/email/email-types'; 69 | export { EmailSuggestions } from './variants/email/email-suggestions'; 70 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/address/address-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { DaDataAddress, DaDataAddressBounds, DaDataAddressSuggestion } from './address-types'; 5 | 6 | type Dictionary = Record; 7 | 8 | interface Props extends BaseProps { 9 | filterLanguage?: 'ru' | 'en'; 10 | filterFromBound?: DaDataAddressBounds; 11 | filterToBound?: DaDataAddressBounds; 12 | filterLocations?: Dictionary[]; 13 | filterLocationsBoost?: Dictionary[]; 14 | filterRestrictValue?: boolean; 15 | } 16 | 17 | export class AddressSuggestions extends BaseSuggestions { 18 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address'; 19 | 20 | getLoadSuggestionsData = (): Record => { 21 | const { 22 | count, 23 | filterFromBound, 24 | filterToBound, 25 | filterLocations, 26 | filterLocationsBoost, 27 | filterLanguage, 28 | filterRestrictValue, 29 | } = this.props; 30 | const { query } = this.state; 31 | 32 | // TODO: Type this params 33 | const requestPayload: Record = { 34 | query, 35 | count: count || 10, 36 | }; 37 | 38 | // Ограничение поиска по типу 39 | if (filterFromBound && filterToBound) { 40 | requestPayload.from_bound = { value: filterFromBound }; 41 | requestPayload.to_bound = { value: filterToBound }; 42 | } 43 | 44 | // Язык подсказок 45 | if (filterLanguage) { 46 | requestPayload.language = filterLanguage; 47 | } 48 | 49 | // Сужение области поиска 50 | if (filterLocations) { 51 | requestPayload.locations = filterLocations; 52 | } 53 | 54 | // Приоритет города при ранжировании 55 | if (filterLocationsBoost) { 56 | requestPayload.locations_boost = filterLocationsBoost; 57 | } 58 | 59 | // @see https://confluence.hflabs.ru/pages/viewpage.action?pageId=1023737934 60 | if (filterRestrictValue) { 61 | requestPayload.restrict_value = true; 62 | } 63 | 64 | return requestPayload; 65 | }; 66 | 67 | protected renderOption = (suggestion: DaDataAddressSuggestion): ReactNode => { 68 | const { renderOption, highlightClassName } = this.props; 69 | const { query } = this.state; 70 | 71 | return renderOption ? ( 72 | renderOption(suggestion, query) 73 | ) : ( 74 | 80 | ); 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_kazakhstan/party-kazakhstan-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { 5 | DaDataPartyKazakhstan, 6 | DaDataPartyKazakhstanRequestPayload, 7 | DaDataPartyKazakhstanSuggestion, 8 | DaDataPartyKazakhstanType, 9 | } from './party-kazakhstan-types'; 10 | 11 | interface Props extends BaseProps { 12 | filterType?: DaDataPartyKazakhstanType[]; 13 | } 14 | 15 | export class PartyKazakhstanSuggestions extends BaseSuggestions< 16 | DaDataPartyKazakhstan, 17 | Props, 18 | DaDataPartyKazakhstanRequestPayload 19 | > { 20 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party_kz'; 21 | 22 | /** 23 | * Структура запроса для подсказок по организациям в Казахстане 🇰🇿 24 | * @see https://dadata.ru/api/suggest/party_kz/ 25 | */ 26 | getLoadSuggestionsData = () => { 27 | const { count, filterType } = this.props; 28 | const { query } = this.state; 29 | 30 | const requestPayload: DaDataPartyKazakhstanRequestPayload = { 31 | query, 32 | count: count || 10, 33 | filters: [], 34 | }; 35 | 36 | // Ограничение по типу организации 37 | if (filterType) { 38 | for (let i = 0; i < filterType.length; i++) { 39 | requestPayload.filters?.push({ 40 | type: filterType[i], 41 | }); 42 | } 43 | } 44 | 45 | return requestPayload; 46 | }; 47 | 48 | // В России ИНН допускает коллизии, и там существует свойство hid 49 | // В Казахстане такого свойства нет, поэтому используем БИН + name_kz + registration_date 50 | protected getSuggestionKey = (suggestion: DaDataPartyKazakhstanSuggestion): string => 51 | suggestion.data.bin + suggestion.data.name_kz + suggestion.data.registration_date; 52 | 53 | protected renderOption = (suggestion: DaDataPartyKazakhstanSuggestion): ReactNode => { 54 | const { renderOption, highlightClassName } = this.props; 55 | const { query } = this.state; 56 | 57 | return renderOption ? ( 58 | renderOption(suggestion, query) 59 | ) : ( 60 |
61 |
62 | 67 |
68 |
69 | {suggestion.data.address_ru && ( 70 |
71 | 76 |
77 | )} 78 |
79 |
80 | ); 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /packages/react-dadata/src/__tests__/default-http-cache.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { DefaultHttpCache as Cache } from '../http-cache'; 3 | 4 | describe('DefaultHttpCache', () => { 5 | const createCacheWithInfinityTtl = () => { 6 | const cache = new Cache(); 7 | cache.ttl = Number.POSITIVE_INFINITY; 8 | return cache; 9 | }; 10 | 11 | it('should return the same singleton instance on every call', () => { 12 | expect(Cache.shared).toBeInstanceOf(Cache); 13 | expect(Cache.shared).toBe(Cache.shared); 14 | expect(new Cache()).not.toBe(Cache.shared); 15 | }); 16 | 17 | it('should serialize http payload to a string', () => { 18 | const cache = createCacheWithInfinityTtl(); 19 | const payload = { 20 | method: 'GET', 21 | headers: { hello: 'world' }, 22 | body: { hi: 'there' }, 23 | url: 'https://example.com', 24 | }; 25 | const key = cache.serializeCacheKey(payload); 26 | expect(typeof key).toBe('string'); 27 | expect(cache.serializeCacheKey(payload)).toBe(key); 28 | expect( 29 | cache.serializeCacheKey({ 30 | ...payload, 31 | url: 'https://example2.com', 32 | }), 33 | ).not.toBe(key); 34 | expect(cache.serializeCacheKey({ ...payload })).toBe(key); 35 | }); 36 | 37 | it('should update ttl only if one is valid', () => { 38 | const cache = new Cache(); 39 | cache.ttl = 0; 40 | expect(cache.ttl).toBe(0); 41 | cache.ttl = Number.POSITIVE_INFINITY; 42 | expect(cache.ttl).toBe(Number.POSITIVE_INFINITY); 43 | cache.ttl = 10; 44 | expect(cache.ttl).toBe(10); 45 | cache.ttl = -1; 46 | expect(cache.ttl).toBe(10); 47 | cache.ttl = true as unknown as number; 48 | expect(cache.ttl).toBe(10); 49 | }); 50 | 51 | it('should insert new cache entries', () => { 52 | const cache = createCacheWithInfinityTtl(); 53 | expect(cache.set('key', 1).get('key')).toBe(1); 54 | expect(cache.set('key2', { hello: 'world' }).get('key2')).toStrictEqual({ hello: 'world' }); 55 | }); 56 | 57 | it('should delete cache entries', () => { 58 | const cache = createCacheWithInfinityTtl(); 59 | cache.set('key2', 2); 60 | expect(cache.set('key', 1).delete('key').get('key')).toBeNull(); 61 | expect(cache.get('key2')).toBe(2); 62 | }); 63 | 64 | it('should clear cache', () => { 65 | const cache = createCacheWithInfinityTtl(); 66 | cache.set('key', 1).set('key2', 2); 67 | expect(cache.size).toBe(2); 68 | cache.reset(); 69 | expect(cache.size).toBe(0); 70 | }); 71 | 72 | it('should delete cache entries after their expiration', () => 73 | new Promise((done) => { 74 | const cache = createCacheWithInfinityTtl(); 75 | cache.set('key', 1); 76 | cache.ttl = 0; 77 | cache.set('key2', 2); 78 | cache.ttl = 25; 79 | cache.set('key3', 3); 80 | cache.ttl = 100; 81 | cache.set('key4', 4); 82 | 83 | setTimeout(() => { 84 | expect(cache.get('key')).toBe(1); 85 | expect(cache.get('key2')).toBeNull(); 86 | expect(cache.get('key3')).toBeNull(); 87 | expect(cache.get('key4')).toBe(4); 88 | done(); 89 | }, 50); 90 | })); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/bank/bank-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { DaDataBank, DaDataBankStatus, DaDataBankSuggestion, DaDataBankType } from './bank-types'; 5 | 6 | type Dictionary = { [key: string]: unknown }; 7 | 8 | interface Props extends BaseProps { 9 | filterStatus?: DaDataBankStatus[]; 10 | filterType?: DaDataBankType[]; 11 | filterLocations?: Dictionary[]; 12 | filterLocationsBoost?: Dictionary[]; 13 | } 14 | 15 | export class BankSuggestions extends BaseSuggestions { 16 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/bank'; 17 | 18 | getLoadSuggestionsData = (): Record => { 19 | const { count, filterStatus, filterType, filterLocations, filterLocationsBoost } = this.props; 20 | const { query } = this.state; 21 | 22 | const requestPayload: Record = { 23 | query, 24 | count: count || 10, 25 | }; 26 | 27 | // Ограничение по статусу организации 28 | if (filterStatus) { 29 | requestPayload.status = filterStatus; 30 | } 31 | 32 | // Ограничение по типу организации 33 | if (filterType) { 34 | requestPayload.type = filterType; 35 | } 36 | 37 | // Сужение области поиска 38 | if (filterLocations) { 39 | requestPayload.locations = filterLocations; 40 | } 41 | 42 | // Приоритет города при ранжировании 43 | if (filterLocationsBoost) { 44 | requestPayload.locations_boost = filterLocationsBoost; 45 | } 46 | 47 | return requestPayload; 48 | }; 49 | 50 | protected getSuggestionKey = (suggestion: DaDataBankSuggestion): string => `${suggestion.data.bic}`; 51 | 52 | protected renderOption = (suggestion: DaDataBankSuggestion): ReactNode => { 53 | const { renderOption, highlightClassName } = this.props; 54 | const { query } = this.state; 55 | 56 | return renderOption ? ( 57 | renderOption(suggestion, query) 58 | ) : ( 59 |
60 |
65 | 70 |
71 |
72 | {suggestion.data.bic &&
{suggestion.data.bic}
} 73 | {suggestion.data.address?.value && ( 74 |
75 | 80 |
81 | )} 82 |
83 |
84 | ); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_russia/party-russia-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | import type { DaDataAddress } from '../address/address-types'; 3 | 4 | export type DaDataPartyType = 'LEGAL' | 'INDIVIDUAL'; 5 | 6 | export type DaDataPartyStatus = 'ACTIVE' | 'LIQUIDATING' | 'LIQUIDATED' | 'REORGANIZING' | 'BANKRUPT'; 7 | 8 | export type DaDataPartyBranchType = 'MAIN' | 'BRANCH'; 9 | 10 | /** 11 | * @see https://dadata.ru/api/suggest/party/#response 12 | */ 13 | interface DaDataPartyAddress 14 | extends Omit { 15 | qc: '0' | '1' | '3'; 16 | house_cadnum: Nullable; 17 | floor: Nullable; 18 | flat_price: Nullable; 19 | } 20 | 21 | export interface DaDataPartyRussiaFio { 22 | name: string; 23 | patronymic: string; 24 | surname: string; 25 | gender: null; 26 | qc: null; 27 | source: null; 28 | } 29 | 30 | export interface DaDataParty { 31 | inn: string; 32 | kpp: string; 33 | ogrn: string; 34 | ogrn_date: number; 35 | hid: string; 36 | capital: Nullable; 37 | type: DaDataPartyType; 38 | fio?: DaDataPartyRussiaFio; 39 | name: { 40 | full_with_opf: string; 41 | short_with_opf: string; 42 | latin: Nullable; 43 | full: string; 44 | short: string; 45 | }; 46 | okpo: Nullable; 47 | okato: Nullable; 48 | oktmo: Nullable; 49 | okogu: Nullable; 50 | okfs: Nullable; 51 | okved: string; 52 | okved_type: string; 53 | okveds: Nullable; 54 | authorities: null; 55 | documents: null; 56 | licenses: null; 57 | phones: null; 58 | emails: null; 59 | employee_count: Nullable; 60 | finance: Nullable<{ 61 | tax_system: Nullable; 62 | income: Nullable; 63 | expense: Nullable; 64 | debt: Nullable; 65 | penalty: Nullable; 66 | year: Nullable; 67 | }>; 68 | opf: { 69 | code: string; 70 | type: string; 71 | full: string; 72 | short: string; 73 | }; 74 | management?: Nullable<{ 75 | name: string; 76 | post: string; 77 | disqualified: Nullable; 78 | }>; 79 | founders: Nullable; 80 | managers: Nullable; 81 | predecessors: Nullable; 82 | successors: Nullable; 83 | branch_count?: number; 84 | branch_type?: DaDataPartyBranchType; 85 | address: DaDataSuggestion; 86 | state: { 87 | actuality_date: number; 88 | registration_date: number; 89 | liquidation_date: Nullable; 90 | status: DaDataPartyStatus; 91 | // TODO: Добавить информацию по статусам 92 | // https://github.com/hflabs/party-state/blob/master/party-state.csv 93 | code: Nullable; 94 | }; 95 | source: null; 96 | qc: null; 97 | } 98 | 99 | export type DaDataPartySuggestion = DaDataSuggestion; 100 | 101 | /** 102 | * Алиасы для типов по организациям в России 🇷🇺 для консистентности с другими странами 103 | */ 104 | export type DaDataPartyRussiaStatus = DaDataPartyStatus; 105 | export type DaDataPartyRussiaType = DaDataPartyType; 106 | export type DaDataPartyRussia = DaDataParty; 107 | export type DaDataPartyRussiaSuggestion = DaDataSuggestion; 108 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_belarus/party-belarus-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { 5 | DaDataPartyBelarus, 6 | DaDataPartyBelarusStatus, 7 | DaDataPartyBelarusSuggestion, 8 | DaDataPartyBelarusType, 9 | } from './party-belarus-types'; 10 | import type { DaDataBelarusRequestPayload } from './party-belarus-types'; 11 | 12 | interface Props extends BaseProps { 13 | filterStatus?: DaDataPartyBelarusStatus[]; 14 | filterType?: DaDataPartyBelarusType[]; 15 | } 16 | 17 | export class PartyBelarusSuggestions extends BaseSuggestions { 18 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party_by'; 19 | 20 | /** 21 | * Структура запроса для подсказок по организациям в Беларуси 🇧🇾 22 | * @see https://dadata.ru/api/suggest/party_by/ 23 | */ 24 | getLoadSuggestionsData = () => { 25 | const { count, filterStatus, filterType } = this.props; 26 | const { query } = this.state; 27 | 28 | const requestPayload: DaDataBelarusRequestPayload = { 29 | query, 30 | count: count || 10, 31 | filters: [], 32 | }; 33 | 34 | // Ограничение по статусу организации 35 | if (filterStatus) { 36 | for (let i = 0; i < filterStatus.length; i++) { 37 | requestPayload.filters?.push({ 38 | status: filterStatus[i], 39 | }); 40 | } 41 | } 42 | 43 | // Ограничение по типу организации 44 | if (filterType) { 45 | for (let i = 0; i < filterType.length; i++) { 46 | requestPayload.filters?.push({ 47 | type: filterType[i], 48 | }); 49 | } 50 | } 51 | 52 | return requestPayload; 53 | }; 54 | 55 | // В России ИНН допускает коллизии, и там существует свойство hid 56 | // В Беларуси такого свойства нет, поэтому используем UNP + full_name_by + registration_date 57 | protected getSuggestionKey = (suggestion: DaDataPartyBelarusSuggestion): string => 58 | suggestion.data.unp + suggestion.data.full_name_by + suggestion.data.registration_date; 59 | 60 | protected renderOption = (suggestion: DaDataPartyBelarusSuggestion): ReactNode => { 61 | const { renderOption, highlightClassName } = this.props; 62 | const { query } = this.state; 63 | 64 | return renderOption ? ( 65 | renderOption(suggestion, query) 66 | ) : ( 67 |
68 |
69 | 74 |
75 |
76 | {suggestion.data.address && ( 77 |
78 | 83 |
84 | )} 85 |
86 |
87 | ); 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/party_russia/party-russia-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import { type BaseProps, BaseSuggestions } from '../../base-suggestions'; 3 | import { HighlightWords } from '../../highlight-words'; 4 | import type { DaDataParty, DaDataPartyStatus, DaDataPartySuggestion, DaDataPartyType } from './party-russia-types'; 5 | 6 | // biome-ignore lint/suspicious/noExplicitAny: 7 | type Dictionary = { [key: string]: any }; 8 | 9 | interface Props extends BaseProps { 10 | filterStatus?: DaDataPartyStatus[]; 11 | filterType?: DaDataPartyType; 12 | filterOkved?: string[]; 13 | filterLocations?: Dictionary[]; 14 | filterLocationsBoost?: Dictionary[]; 15 | } 16 | 17 | export class PartySuggestions extends BaseSuggestions { 18 | loadSuggestionsUrl = 'https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party'; 19 | 20 | getLoadSuggestionsData = (): Record => { 21 | const { count, filterStatus, filterType, filterOkved, filterLocations, filterLocationsBoost } = this.props; 22 | const { query } = this.state; 23 | 24 | const requestPayload: Record = { 25 | query, 26 | count: count || 10, 27 | }; 28 | 29 | // Ограничение по статусу организации 30 | if (filterStatus) { 31 | requestPayload.status = filterStatus; 32 | } 33 | 34 | // Ограничение по ОКВЭД 35 | // @see https://confluence.hflabs.ru/pages/viewpage.action?pageId=1093075333 36 | if (filterOkved) { 37 | requestPayload.okved = filterOkved; 38 | } 39 | 40 | // Ограничение по типу организации 41 | if (filterType) { 42 | requestPayload.type = filterType; 43 | } 44 | 45 | // Сужение области поиска 46 | if (filterLocations) { 47 | requestPayload.locations = filterLocations; 48 | } 49 | 50 | // Приоритет города при ранжировании 51 | if (filterLocationsBoost) { 52 | requestPayload.locations_boost = filterLocationsBoost; 53 | } 54 | 55 | return requestPayload; 56 | }; 57 | 58 | protected getSuggestionKey = (suggestion: DaDataPartySuggestion): string => suggestion.data.hid; 59 | 60 | protected renderOption = (suggestion: DaDataPartySuggestion): ReactNode => { 61 | const { renderOption, highlightClassName } = this.props; 62 | const { query } = this.state; 63 | 64 | return renderOption ? ( 65 | renderOption(suggestion, query) 66 | ) : ( 67 |
68 |
73 | 78 |
79 |
80 | {suggestion.data.inn &&
{suggestion.data.inn}
} 81 | {suggestion.data.address?.value && ( 82 |
83 | 88 |
89 | )} 90 |
91 |
92 | ); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /packages/example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import './App.css'; 3 | import 'react-dadata/src/react-dadata.css'; 4 | import { 5 | AddressSuggestions, 6 | PartyBelarusSuggestions, 7 | PartyKazakhstanSuggestions, 8 | PartySuggestions, 9 | } from '../../react-dadata/src'; 10 | 11 | const DADATA_TOKEN = '3c2e964517d7358776e07d7d699cc2b0626dac54'; 12 | 13 | function App() { 14 | if (!DADATA_TOKEN) { 15 | return
Пожалуйста, установите ваш API токен для DaData в `example/src/App.tsx:5`
; 16 | } 17 | 18 | const [suggestionsType, setSuggestionsType] = useState< 19 | 'address' | 'party_russia' | 'party_belarus' | 'party_kazakhstan' 20 | >('address'); 21 | 22 | return ( 23 |
24 |
25 |
26 | setSuggestionsType('address')} 33 | /> 34 | 35 |
36 |
37 | setSuggestionsType('party_russia')} 44 | /> 45 | 46 |
47 |
48 | setSuggestionsType('party_belarus')} 55 | /> 56 | 57 |
58 |
59 | setSuggestionsType('party_kazakhstan')} 66 | /> 67 | 68 |
69 |
70 | {suggestionsType === 'address' && ( 71 | 72 | )} 73 | {suggestionsType === 'party_russia' && ( 74 | 79 | )} 80 | {suggestionsType === 'party_belarus' && ( 81 | 86 | )} 87 | {suggestionsType === 'party_kazakhstan' && ( 88 | 93 | )} 94 |
95 | ); 96 | } 97 | 98 | export default App; 99 | -------------------------------------------------------------------------------- /packages/react-dadata/src/variants/address/address-types.ts: -------------------------------------------------------------------------------- 1 | import type { DaDataSuggestion, Nullable } from '../../core-types'; 2 | 3 | export interface DaDataAddressMetro { 4 | name: string; 5 | line: string; 6 | distance: number; 7 | } 8 | 9 | export type DaDataAddressBeltwayHit = 'IN_MKAD' | 'OUT_MKAD' | 'IN_KAD' | 'OUT_KAD'; 10 | 11 | export interface DaDataAddress { 12 | area: Nullable; 13 | area_fias_id: Nullable; 14 | area_kladr_id: Nullable; 15 | area_type: Nullable; 16 | area_type_full: Nullable; 17 | area_with_type: Nullable; 18 | beltway_distance: Nullable; 19 | beltway_hit: Nullable; 20 | block: Nullable; 21 | block_type: Nullable; 22 | block_type_full: Nullable; 23 | federal_district: Nullable; 24 | capital_marker: '0' | '1' | '2' | '3' | '4'; 25 | city: Nullable; 26 | city_area: Nullable; 27 | city_district: Nullable; 28 | city_district_fias_id: Nullable; 29 | city_district_kladr_id: Nullable; 30 | city_district_type: Nullable; 31 | city_district_type_full: Nullable; 32 | city_district_with_type: Nullable; 33 | city_fias_id: Nullable; 34 | city_kladr_id: Nullable; 35 | city_type: Nullable; 36 | city_type_full: Nullable; 37 | city_with_type: Nullable; 38 | country: string; 39 | country_iso_code: string; 40 | fias_id: string; 41 | fias_level: string; 42 | flat: Nullable; 43 | flat_area: Nullable; 44 | flat_price: null; 45 | flat_type: Nullable; 46 | flat_type_full: Nullable; 47 | flat_fias_id?: Nullable; 48 | flat_cadnum?: null; 49 | geo_lat: Nullable; 50 | geo_lon: Nullable; 51 | geoname_id: Nullable; 52 | history_values: Nullable; 53 | house: Nullable; 54 | house_fias_id: Nullable; 55 | house_kladr_id: Nullable; 56 | house_type: Nullable; 57 | house_type_full: Nullable; 58 | house_cadnum?: null; 59 | entrance?: null; 60 | floor?: null; 61 | kladr_id: string; 62 | okato: Nullable; 63 | oktmo: Nullable; 64 | postal_box: Nullable; 65 | postal_code: Nullable; 66 | qc: null; 67 | qc_complete: null; 68 | qc_geo: Nullable<'0' | '1' | '2' | '3' | '4' | '5'>; 69 | qc_house: null; 70 | region: string; 71 | region_fias_id: string; 72 | region_kladr_id: string; 73 | region_type: string; 74 | region_type_full: string; 75 | region_with_type: string; 76 | settlement: Nullable; 77 | settlement_fias_id: Nullable; 78 | settlement_kladr_id: Nullable; 79 | settlement_type: Nullable; 80 | settlement_type_full: Nullable; 81 | settlement_with_type: Nullable; 82 | source: Nullable; 83 | square_meter_price?: Nullable; 84 | street: Nullable; 85 | street_fias_id: Nullable; 86 | street_kladr_id: Nullable; 87 | street_type: Nullable; 88 | street_type_full: Nullable; 89 | street_with_type: Nullable; 90 | stead?: Nullable; 91 | stead_fias_id?: Nullable; 92 | stead_kladr_id?: Nullable; 93 | stead_type?: Nullable; 94 | stead_type_full?: Nullable; 95 | stead_cadnum?: null; 96 | tax_office: Nullable; 97 | tax_office_legal: Nullable; 98 | timezone: Nullable; 99 | unparsed_parts: null; 100 | fias_code: string; 101 | region_iso_code: string; 102 | fias_actuality_state: string; 103 | metro: Nullable; 104 | divisions?: unknown; 105 | } 106 | 107 | export type DaDataAddressBounds = 'country' | 'region' | 'area' | 'city' | 'settlement' | 'street' | 'house'; 108 | 109 | export type DaDataAddressSuggestion = DaDataSuggestion; 110 | -------------------------------------------------------------------------------- /packages/react-dadata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dadata", 3 | "version": "2.27.4", 4 | "description": "React-компонент для подсказок адресов, организаций и банков с помощью сервиса DaData.ru", 5 | "keywords": [ 6 | "react", 7 | "reactjs", 8 | "dadata", 9 | "suggestions", 10 | "autocomplete", 11 | "address", 12 | "party", 13 | "bank" 14 | ], 15 | "author": "Vitaly Baev ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/vitalybaev/react-dadata/issues" 19 | }, 20 | "main": "dist/cjs/index.js", 21 | "module": "dist/esm/index.js", 22 | "types": "dist/index.d.ts", 23 | "files": [ 24 | "dist", 25 | "package.json" 26 | ], 27 | "scripts": { 28 | "prepublishOnly": "cp ../../license ./license && pnpm run test:once && pnpm run build", 29 | "clean": "rimraf dist", 30 | "build:css": "lightningcss --minify --bundle --targets '>0.2%, ie 11' src/react-dadata.css -o dist/react-dadata.css", 31 | "build:css:ci": "./node_modules/lightningcss-cli-linux-x64-gnu/lightningcss --minify --bundle --targets '>0.2%, ie 11' src/react-dadata.css -o dist/react-dadata.css", 32 | "build:cjs": "tsc --project tsconfig.build.json --module commonjs --target es5 --outDir dist/cjs", 33 | "build:esm": "tsc --project tsconfig.build.json --module es2015 --target es5 --outDir dist/esm", 34 | "build:types": "tsc --project tsconfig.types.json", 35 | "build:ci": "NODE_ENV=production pnpm run --sequential '/^(clean|build:css:ci|build:cjs|build:esm|build:types)$/'", 36 | "build": "NODE_ENV=production pnpm run --sequential '/^(clean|build:css|build:cjs|build:esm|build:types)$/'", 37 | "size-build": "pnpm build:esm", 38 | "test:lint-package": "biome check src/", 39 | "test:size-limit": "pnpm build:esm && size-limit", 40 | "test:type-check": "tsc --noEmit", 41 | "test": "vitest --coverage", 42 | "test:once": "vitest run --coverage", 43 | "vitest-preview": "vitest-preview" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/vitalybaev/react-dadata.git" 48 | }, 49 | "dependencies": { 50 | "debounce": "^1.2.1", 51 | "highlight-words": "^1.2.1", 52 | "nanoid": "^3.3.6", 53 | "shallowequal": "^1.1.0" 54 | }, 55 | "devDependencies": { 56 | "@size-limit/preset-small-lib": "^8.0.0", 57 | "@testing-library/jest-dom": "^6.4.8", 58 | "@testing-library/react": "^16.0.0", 59 | "@testing-library/user-event": "^14.5.2", 60 | "@types/debounce": "^1.2.1", 61 | "@types/node": "^18.16.3", 62 | "@types/react": "^18.3.3", 63 | "@types/shallowequal": "^1.1.1", 64 | "@vitest/coverage-v8": "^2.1.8", 65 | "jsdom": "^24.1.1", 66 | "lightningcss-cli": "^1.28.2", 67 | "lightningcss-cli-linux-x64-gnu": "^1.28.2", 68 | "msw": "^2.3.5", 69 | "react": "^18.3.1", 70 | "react-dom": "^18.3.1", 71 | "size-limit": "^8.0.0", 72 | "typescript": "^5.5.4", 73 | "vitest": "^2.1.8", 74 | "vitest-preview": "^0.0.1" 75 | }, 76 | "peerDependencies": { 77 | "react": "^15.6 || ^16.0 || ^17.0 || ^18.0", 78 | "react-dom": "^15.6 || ^16.0 || ^17.0 || ^18.0" 79 | }, 80 | "size-limit": [ 81 | { 82 | "name": "AddressSuggestions", 83 | "path": "dist/esm/index.js", 84 | "import": "{ AddressSuggestions }", 85 | "limit": "5.5 KB" 86 | }, 87 | { 88 | "name": "PartySuggestions", 89 | "path": "dist/esm/index.js", 90 | "import": "{ PartySuggestions }", 91 | "limit": "5.5 KB" 92 | }, 93 | { 94 | "name": "FioSuggestions", 95 | "path": "dist/esm/index.js", 96 | "import": "{ FioSuggestions }", 97 | "limit": "5.5 KB" 98 | }, 99 | { 100 | "name": "BankSuggestions", 101 | "path": "dist/esm/index.js", 102 | "import": "{ BankSuggestions }", 103 | "limit": "5.5 KB" 104 | } 105 | ], 106 | "browserslist": { 107 | "production": [ 108 | ">0.2%", 109 | "ie 11", 110 | "not dead", 111 | "not op_mini all" 112 | ], 113 | "development": [ 114 | "last 1 chrome version", 115 | "last 1 firefox version", 116 | "last 1 safari version" 117 | ] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/react-dadata/src/__tests__/PartySuggestions.test.tsx: -------------------------------------------------------------------------------- 1 | import { getAllByRole, render, screen, waitFor } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { http, HttpResponse } from 'msw'; 4 | import { type SetupServerApi, setupServer } from 'msw/node'; 5 | import React from 'react'; 6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 7 | import { PartySuggestions } from '../variants/party_russia/party-russia-suggestions'; 8 | import { partyMocks } from './mocks'; 9 | 10 | type RequestLog = { 11 | method: string; 12 | endpoint: string; 13 | data: Record; 14 | }; 15 | 16 | let server: SetupServerApi; 17 | let requestCalls: RequestLog[] = []; 18 | 19 | beforeEach(() => { 20 | requestCalls = []; 21 | 22 | server = setupServer( 23 | http.post<{ query: string }, Record>( 24 | '*/suggestions/api/4_1/rs/suggest/party', 25 | async ({ request }) => { 26 | const data = await request.json(); 27 | 28 | requestCalls.push({ method: request.method, endpoint: request.url.toString(), data }); 29 | 30 | const { query } = data; 31 | 32 | if (query && typeof query === 'string') { 33 | return HttpResponse.json({ suggestions: partyMocks[query] }); 34 | } 35 | return HttpResponse.json({ suggestions: [] }); 36 | }, 37 | ), 38 | ); 39 | 40 | server.listen(); 41 | }); 42 | 43 | afterEach(() => { 44 | server.close(); 45 | }); 46 | 47 | describe('PartySuggestions', () => { 48 | it('PartySuggestions renders correctly', () => { 49 | render(); 50 | 51 | expect(screen.getByRole('combobox')).toBeInTheDocument(); 52 | expect(screen.getByRole('textbox')).toBeInTheDocument(); 53 | expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); 54 | }); 55 | 56 | it('fetch 10 suggestions by default', async () => { 57 | render(); 58 | 59 | const input = await screen.findByRole('textbox'); 60 | await userEvent.tab(); 61 | 62 | await waitFor(() => { 63 | expect(requestCalls.length).toBe(1); 64 | expect(requestCalls.length).toBe(1); 65 | expect(requestCalls[0].data).toEqual({ query: '', count: 10 }); 66 | expect(requestCalls[0].endpoint).toBe('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party'); 67 | }); 68 | 69 | await userEvent.type(input, 'Ск'); 70 | 71 | await waitFor(() => { 72 | expect(requestCalls.length).toBe(3); 73 | expect(requestCalls[2].data).toEqual({ 74 | query: 'Ск', 75 | count: 10, 76 | }); 77 | }); 78 | }); 79 | 80 | it("correctly fires input's onChange callback", async () => { 81 | const handleChangeMock = vi.fn(); 82 | 83 | render(); 84 | 85 | const input = await screen.findByRole('textbox'); 86 | 87 | await userEvent.tab(); 88 | await userEvent.type(input, 'С'); 89 | 90 | expect(handleChangeMock).toBeCalledTimes(1); 91 | expect(handleChangeMock.mock.calls[0][0].target.value).toBe('С'); 92 | }); 93 | 94 | it('correctly types and selects suggestions', async () => { 95 | const handleFocusMock = vi.fn(); 96 | const handleChangeMock = vi.fn(); 97 | 98 | render( 99 | , 100 | ); 101 | 102 | const input = await screen.findByRole('textbox'); 103 | 104 | await userEvent.tab(); 105 | 106 | expect(input).toHaveFocus(); 107 | expect(handleFocusMock.mock.calls.length).toBe(1); 108 | 109 | await userEvent.type(input, 'ск'); 110 | 111 | const listBox = await screen.findByRole('listbox'); 112 | expect(listBox).toBeInTheDocument(); 113 | expect(getAllByRole(listBox, 'option')).toHaveLength(7); 114 | await userEvent.click(screen.getByRole('option', { name: /ЖИЛЦЕНТР/i })); 115 | expect(input).toHaveFocus(); 116 | expect(handleChangeMock).toHaveBeenCalledWith(partyMocks.ск[0]); 117 | }); 118 | 119 | it('correctly sends http parameters', async () => { 120 | render( 121 | , 130 | ); 131 | 132 | const input = await screen.findByRole('textbox'); 133 | await userEvent.tab(); 134 | 135 | await waitFor(() => { 136 | expect(requestCalls.length).toBe(1); 137 | expect(requestCalls.length).toBe(1); 138 | expect(requestCalls[0].data.query).toBe(''); 139 | expect(requestCalls[0].endpoint).toBe('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/party'); 140 | }); 141 | 142 | await userEvent.type(input, 'ск'); 143 | 144 | await waitFor(() => { 145 | expect(requestCalls.length).toBe(3); 146 | expect(requestCalls[2].data).toEqual({ 147 | query: 'ск', 148 | count: 20, 149 | status: ['LIQUIDATING', 'LIQUIDATED'], 150 | type: 'INDIVIDUAL', 151 | okved: ['07.1', '07.10', '07.2', '07.21'], 152 | locations: [{ kladr_id: '65' }], 153 | locations_boost: [{ kladr_id: '77' }], 154 | }); 155 | }); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /packages/react-dadata/src/base-suggestions.tsx: -------------------------------------------------------------------------------- 1 | import { debounce } from 'debounce'; 2 | import { nanoid } from 'nanoid'; 3 | import React, { type ChangeEvent, type MouseEvent, type FocusEvent, type ReactNode, type ElementType } from 'react'; 4 | import shallowEqual from 'shallowequal'; 5 | import type { CommonProps, DaDataSuggestion } from './core-types'; 6 | import { DefaultHttpCache, HttpCache } from './http-cache'; 7 | import { makeRequest } from './request'; 8 | 9 | export type BaseProps = CommonProps; 10 | 11 | export interface BaseState { 12 | /** 13 | * Текущая строка в поле ввода 14 | */ 15 | query: string; 16 | 17 | displaySuggestions: boolean; 18 | 19 | /** 20 | * Оригинальная строка в поле поиска, требуется для хранения значения в момент переключения подсказок стрелками 21 | */ 22 | inputQuery: string; 23 | 24 | /** 25 | * Находится ли сейчас фокус в поле ввода 26 | */ 27 | isFocused: boolean; 28 | 29 | /** 30 | * Массив с текущими подсказками 31 | */ 32 | suggestions: Array>; 33 | 34 | /** 35 | * Индекс текущей выбранной подсказки 36 | */ 37 | suggestionIndex: number; 38 | } 39 | 40 | export abstract class BaseSuggestions< 41 | SuggestionType, 42 | OwnProps, 43 | RequestPayload extends Record = Record, 44 | > extends React.PureComponent & OwnProps, BaseState> { 45 | /** 46 | * URL для загрузки подсказок, переопределяется в конкретном компоненте 47 | */ 48 | protected loadSuggestionsUrl = ''; 49 | 50 | protected dontPerformBlurHandler = false; 51 | 52 | protected _uid?: string; 53 | 54 | protected didMount: boolean; 55 | 56 | /** 57 | * HTML-input 58 | */ 59 | private textInput?: HTMLInputElement; 60 | 61 | constructor(props: BaseProps & OwnProps) { 62 | super(props); 63 | 64 | this.didMount = false; 65 | 66 | const { defaultQuery, value, delay } = this.props; 67 | const valueQuery = value ? value.value : undefined; 68 | 69 | this.setupDebounce(delay); 70 | 71 | this.state = { 72 | query: (defaultQuery as string | undefined) || valueQuery || '', 73 | inputQuery: (defaultQuery as string | undefined) || valueQuery || '', 74 | isFocused: false, 75 | displaySuggestions: true, 76 | suggestions: [], 77 | suggestionIndex: -1, 78 | }; 79 | } 80 | 81 | componentDidMount() { 82 | this.didMount = true; 83 | } 84 | 85 | componentDidUpdate(prevProps: Readonly & OwnProps>): void { 86 | const { value, delay } = this.props; 87 | const { query, inputQuery } = this.state; 88 | if (!shallowEqual(prevProps.value, value)) { 89 | const newQuery = value ? value.value : ''; 90 | if (query !== newQuery || inputQuery !== newQuery) { 91 | this.setState({ query: newQuery, inputQuery: newQuery }); 92 | } 93 | } 94 | 95 | if (delay !== prevProps.delay) { 96 | this.setupDebounce(delay); 97 | } 98 | } 99 | 100 | componentWillUnmount() { 101 | this.didMount = false; 102 | } 103 | 104 | get uid(): string { 105 | if (this.props.uid) { 106 | return this.props.uid; 107 | } 108 | if (!this._uid) { 109 | this._uid = nanoid(); 110 | } 111 | return this._uid as string; 112 | } 113 | 114 | get httpCache(): HttpCache | null { 115 | const { httpCache: cacheProp, httpCacheTtl: ttl } = this.props; 116 | if (!cacheProp) { 117 | return null; 118 | } 119 | if (cacheProp instanceof HttpCache) { 120 | return cacheProp; 121 | } 122 | const cache = DefaultHttpCache.shared; 123 | if (typeof ttl === 'number') { 124 | cache.ttl = ttl; 125 | } 126 | return cache; 127 | } 128 | 129 | protected getSuggestionsUrl = (): string => { 130 | const { url } = this.props; 131 | 132 | return url || this.loadSuggestionsUrl; 133 | }; 134 | 135 | protected setupDebounce = (delay: number | undefined): void => { 136 | if (typeof delay === 'number' && delay > 0) { 137 | this.fetchSuggestions = debounce(this.performFetchSuggestions, delay); 138 | } else { 139 | this.fetchSuggestions = this.performFetchSuggestions; 140 | } 141 | }; 142 | 143 | /** 144 | * Функция, которая вернет данные для отправки для получения подсказок 145 | */ 146 | protected abstract getLoadSuggestionsData(): RequestPayload; 147 | 148 | protected fetchSuggestions = (): void => { 149 | // 150 | }; 151 | 152 | private handleInputFocus = (event: FocusEvent) => { 153 | this.setState({ isFocused: true }); 154 | 155 | const { suggestions } = this.state; 156 | 157 | if (suggestions.length === 0) { 158 | this.fetchSuggestions(); 159 | } 160 | 161 | const { inputProps } = this.props; 162 | if (inputProps?.onFocus) { 163 | inputProps.onFocus(event); 164 | } 165 | }; 166 | 167 | private handleInputBlur = (event: FocusEvent) => { 168 | const { suggestions, suggestionIndex } = this.state; 169 | const { selectOnBlur, inputProps } = this.props; 170 | 171 | this.setState({ isFocused: false }); 172 | if (suggestions.length === 0) { 173 | this.fetchSuggestions(); 174 | } 175 | 176 | if (selectOnBlur && !this.dontPerformBlurHandler) { 177 | if (suggestions.length > 0) { 178 | const suggestionIndexToSelect = 179 | suggestionIndex >= 0 && suggestionIndex < suggestions.length ? suggestionIndex : 0; 180 | this.selectSuggestion(suggestionIndexToSelect, true); 181 | } 182 | } 183 | 184 | this.dontPerformBlurHandler = false; 185 | 186 | if (inputProps?.onBlur) { 187 | inputProps.onBlur(event); 188 | } 189 | }; 190 | 191 | private handleInputChange = (event: ChangeEvent) => { 192 | const { value } = event.target; 193 | const { inputProps } = this.props; 194 | if (this.didMount) { 195 | this.setState({ query: value, inputQuery: value, displaySuggestions: !!value }, () => { 196 | this.fetchSuggestions(); 197 | }); 198 | } 199 | 200 | if (inputProps?.onChange) { 201 | inputProps.onChange(event); 202 | } 203 | }; 204 | 205 | private handleInputKeyDown = (event: React.KeyboardEvent) => { 206 | this.handleKeyboard(event); 207 | 208 | const { inputProps } = this.props; 209 | if (inputProps?.onKeyDown) { 210 | inputProps.onKeyDown(event); 211 | } 212 | }; 213 | 214 | private handleInputKeyPress = (event: React.KeyboardEvent) => { 215 | this.handleKeyboard(event); 216 | 217 | const { inputProps } = this.props; 218 | if (inputProps?.onKeyPress) { 219 | inputProps.onKeyPress(event); 220 | } 221 | }; 222 | 223 | private handleKeyboard = (event: React.KeyboardEvent) => { 224 | const { suggestions, suggestionIndex, inputQuery } = this.state; 225 | if (event.key === 'ArrowDown') { 226 | // Arrow down 227 | event.preventDefault(); 228 | if (suggestionIndex < suggestions.length - 1) { 229 | const newSuggestionIndex = suggestionIndex + 1; 230 | const newInputQuery = suggestions[newSuggestionIndex].value; 231 | if (this.didMount) { 232 | this.setState({ 233 | suggestionIndex: newSuggestionIndex, 234 | query: newInputQuery, 235 | }); 236 | } 237 | } 238 | } else if (event.key === 'ArrowUp') { 239 | // Arrow up 240 | event.preventDefault(); 241 | if (suggestionIndex >= 0) { 242 | const newSuggestionIndex = suggestionIndex - 1; 243 | const newInputQuery = newSuggestionIndex === -1 ? inputQuery : suggestions[newSuggestionIndex].value; 244 | if (this.didMount) { 245 | this.setState({ 246 | suggestionIndex: newSuggestionIndex, 247 | query: newInputQuery, 248 | }); 249 | } 250 | } 251 | } else if (event.key === 'Enter') { 252 | // Enter 253 | event.preventDefault(); 254 | if (suggestionIndex >= 0) { 255 | this.selectSuggestion(suggestionIndex); 256 | } 257 | } 258 | }; 259 | 260 | private performFetchSuggestions = () => { 261 | const { minChars, token } = this.props; 262 | const { query } = this.state; 263 | 264 | // Проверяем на минимальное количество символов для отправки 265 | if (typeof minChars === 'number' && minChars > 0 && query.length < minChars) { 266 | this.setState({ suggestions: [], suggestionIndex: -1 }); 267 | return; 268 | } 269 | 270 | makeRequest( 271 | 'POST', 272 | this.getSuggestionsUrl(), 273 | { 274 | headers: { 275 | Accept: 'application/json', 276 | Authorization: `Token ${token}`, 277 | 'Content-Type': 'application/json', 278 | }, 279 | json: this.getLoadSuggestionsData(), 280 | }, 281 | this.httpCache, 282 | (suggestions) => { 283 | if (this.didMount) { 284 | this.setState({ suggestions, suggestionIndex: -1 }); 285 | } 286 | }, 287 | ); 288 | }; 289 | 290 | private onSuggestionClick = (index: number, event: MouseEvent) => { 291 | event.stopPropagation(); 292 | this.selectSuggestion(index); 293 | }; 294 | 295 | private selectSuggestion = (index: number, isSilent = false) => { 296 | const { suggestions } = this.state; 297 | const { selectOnBlur, onChange } = this.props; 298 | 299 | if (suggestions.length >= index - 1) { 300 | const suggestion = suggestions[index]; 301 | if (selectOnBlur) { 302 | this.dontPerformBlurHandler = true; 303 | } 304 | this.setState( 305 | { 306 | query: suggestion.value, 307 | inputQuery: suggestion.value, 308 | displaySuggestions: false, 309 | }, 310 | () => { 311 | if (!isSilent) { 312 | this.fetchSuggestions(); 313 | setTimeout(() => this.setCursorToEnd(this.textInput)); 314 | } 315 | }, 316 | ); 317 | 318 | if (onChange) { 319 | onChange(suggestion); 320 | } 321 | } 322 | }; 323 | 324 | private setCursorToEnd = (element: HTMLInputElement | undefined) => { 325 | if (element) { 326 | const valueLength = element.value.length; 327 | if (element.selectionStart || element.selectionStart === 0) { 328 | element.selectionStart = valueLength; 329 | element.selectionEnd = valueLength; 330 | element.focus(); 331 | } 332 | } 333 | }; 334 | 335 | protected getHighlightWords = (): string[] => { 336 | const { inputQuery } = this.state; 337 | const wordsToPass = ['г', 'респ', 'ул', 'р-н', 'село', 'деревня', 'поселок', 'пр-д', 'пл', 'к', 'кв', 'обл', 'д']; 338 | let words = inputQuery.replace(',', '').split(' '); 339 | words = words.filter((word) => { 340 | return wordsToPass.indexOf(word) < 0; 341 | }); 342 | return words; 343 | }; 344 | 345 | /** 346 | * Функция, которая вернет уникальный key для списка React 347 | * @param suggestion 348 | */ 349 | protected getSuggestionKey = (suggestion: DaDataSuggestion): string => suggestion.value; 350 | 351 | public focus = (): void => { 352 | if (this.textInput) { 353 | this.textInput.focus(); 354 | } 355 | }; 356 | 357 | public setInputValue = (value?: string): void => { 358 | this.setState({ query: value || '', inputQuery: value || '' }); 359 | }; 360 | 361 | protected abstract renderOption(suggestion: DaDataSuggestion): ReactNode; 362 | 363 | public render(): ReactNode { 364 | const { 365 | inputProps, 366 | hintText, 367 | containerClassName, 368 | hintClassName, 369 | suggestionsClassName, 370 | suggestionClassName, 371 | currentSuggestionClassName, 372 | customInput, 373 | children, 374 | } = this.props; 375 | const { query, isFocused, suggestions, suggestionIndex, displaySuggestions } = this.state; 376 | 377 | const Component = typeof customInput !== 'undefined' ? (customInput as ElementType) : 'input'; 378 | 379 | const optionsExpanded = isFocused && suggestions && displaySuggestions && suggestions.length > 0; 380 | return ( 381 |
389 |
390 | { 396 | this.textInput = input; 397 | }} 398 | onChange={this.handleInputChange} 399 | onKeyPress={this.handleInputKeyPress} 400 | onKeyDown={this.handleInputKeyDown} 401 | onFocus={this.handleInputFocus} 402 | onBlur={this.handleInputBlur} 403 | /> 404 |
405 | {optionsExpanded && ( 406 |
    410 | role="listbox" 411 | className={suggestionsClassName || 'react-dadata__suggestions'} 412 | > 413 | {typeof hintText !== 'undefined' && ( 414 |
    {hintText}
    415 | )} 416 | {suggestions.map((suggestion, index) => { 417 | let suggestionClass = suggestionClassName || 'react-dadata__suggestion'; 418 | if (index === suggestionIndex) { 419 | suggestionClass += ` ${currentSuggestionClassName || 'react-dadata__suggestion--current'}`; 420 | } 421 | return ( 422 | 432 | ); 433 | })} 434 |
435 | )} 436 | {children} 437 |
438 | ); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /packages/react-dadata/src/__tests__/AddressSuggestions.test.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | cleanup, 3 | findAllByRole, 4 | fireEvent, 5 | getAllByRole, 6 | queryAllByRole, 7 | render, 8 | screen, 9 | waitFor, 10 | } from '@testing-library/react'; 11 | import userEvent from '@testing-library/user-event'; 12 | import { http, HttpResponse } from 'msw'; 13 | import { type SetupServerApi, setupServer } from 'msw/node'; 14 | import React, { createRef, forwardRef, type HTMLProps, type ReactNode } from 'react'; 15 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 16 | // import { debug } from 'vitest-preview'; 17 | import * as requestModule from '../request'; 18 | import { AddressSuggestions } from '../variants/address/address-suggestions'; 19 | import type { DaDataAddressSuggestion } from '../variants/address/address-types'; 20 | import { addressMockKrasnodar, addressMocks, createAddressMock, mockedRequestCalls } from './mocks'; 21 | 22 | type RequestLog = { 23 | method: string; 24 | endpoint: string; 25 | data: Record; 26 | }; 27 | 28 | let server: SetupServerApi; 29 | let requestCalls: RequestLog[] = []; 30 | 31 | // const delay = (ms: number) => { 32 | // return new Promise((resolve) => { 33 | // setTimeout(resolve, ms); 34 | // }); 35 | // }; 36 | 37 | beforeEach(() => { 38 | requestCalls = []; 39 | 40 | server = setupServer( 41 | http.post<{ query: string }, Record>( 42 | '*/suggestions/api/4_1/rs/suggest/address', 43 | async ({ request }) => { 44 | const data = await request.json(); 45 | 46 | requestCalls.push({ method: request.method, endpoint: request.url.toString(), data }); 47 | 48 | const { query } = data; 49 | 50 | if (query && typeof query === 'string') { 51 | return HttpResponse.json({ suggestions: addressMocks[query] }); 52 | } 53 | return HttpResponse.json({ suggestions: [] }); 54 | }, 55 | ), 56 | ); 57 | 58 | server.listen(); 59 | }); 60 | 61 | afterEach(() => { 62 | server.close(); 63 | }); 64 | 65 | describe('AddressSuggestions', () => { 66 | it('is truthy', () => { 67 | expect(AddressSuggestions).toBeTruthy(); 68 | }); 69 | 70 | it('AddressSuggestions renders correctly', () => { 71 | render(); 72 | 73 | expect(screen.getByRole('combobox')).toBeInTheDocument(); 74 | expect(screen.getByRole('textbox')).toBeInTheDocument(); 75 | expect(screen.queryByRole('listbox')).not.toBeInTheDocument(); 76 | }); 77 | 78 | it('input renders correctly with props', () => { 79 | const inputProps: HTMLProps = { 80 | autoComplete: 'tel', 81 | 'aria-label': 'Test aria label', 82 | className: 'input-class-name', 83 | }; 84 | 85 | render(); 86 | 87 | const input = screen.getByRole('textbox'); 88 | expect(input).toHaveAttribute('aria-label', 'Test aria label'); 89 | expect(input).toHaveAttribute('autoComplete', 'tel'); 90 | expect(input).toHaveAttribute('class', 'input-class-name'); 91 | }); 92 | 93 | it("correctly fires input's onChange callback", async () => { 94 | const handleChangeMock = vi.fn(); 95 | 96 | render(); 97 | 98 | const input = await screen.findByRole('textbox'); 99 | 100 | await userEvent.tab(); 101 | await userEvent.type(input, 'М'); 102 | 103 | expect(handleChangeMock).toBeCalledTimes(1); 104 | expect(handleChangeMock.mock.calls[0][0].target.value).toBe('М'); 105 | }); 106 | 107 | it('correctly types and selects suggestions', async () => { 108 | const handleFocusMock = vi.fn(); 109 | const handleChangeMock = vi.fn(); 110 | 111 | render( 112 | , 113 | ); 114 | 115 | const input = await screen.findByRole('textbox'); 116 | 117 | await userEvent.tab(); 118 | 119 | expect(input).toHaveFocus(); 120 | expect(handleFocusMock.mock.calls.length).toBe(1); 121 | 122 | await userEvent.type(input, 'Мо'); 123 | 124 | const listBox = await screen.findByRole('listbox'); 125 | expect(listBox).toBeInTheDocument(); 126 | expect(getAllByRole(listBox, 'option')).toHaveLength(7); 127 | 128 | await userEvent.click(screen.getAllByRole('option')[0]); 129 | expect(input).toHaveFocus(); 130 | }); 131 | 132 | it('correctly fires blur', async () => { 133 | const handleBlurMock = vi.fn(); 134 | 135 | render(); 136 | 137 | await userEvent.tab(); 138 | 139 | expect(await screen.findByRole('textbox')).toHaveFocus(); 140 | expect(handleBlurMock).not.toBeCalled(); 141 | 142 | await userEvent.tab(); 143 | expect(await screen.findByRole('textbox')).not.toHaveFocus(); 144 | expect(handleBlurMock).toBeCalledTimes(1); 145 | }); 146 | 147 | it('correctly shows 0 suggestions with minChars', async () => { 148 | const handleFocusMock = vi.fn(); 149 | 150 | render(); 151 | const input = await screen.findByRole('textbox'); 152 | 153 | await userEvent.tab(); 154 | 155 | await userEvent.type(input, 'Мо'); 156 | expect(await screen.queryByRole('listbox')).not.toBeInTheDocument(); 157 | 158 | await userEvent.type(input, 'с'); 159 | const listBox = await screen.findByRole('listbox'); 160 | expect(listBox).toBeInTheDocument(); 161 | 162 | expect(getAllByRole(listBox, 'option')).toHaveLength(7); 163 | }); 164 | 165 | it('it respects defaultQuery or value on mount', async () => { 166 | render(); 167 | expect(await screen.findByRole('textbox')).toHaveValue(''); 168 | cleanup(); 169 | 170 | render(); 171 | expect(await screen.findByRole('textbox')).toHaveValue('My Query'); 172 | cleanup(); 173 | 174 | render(); 175 | expect(await screen.findByRole('textbox')).toHaveValue('My Query'); 176 | cleanup(); 177 | 178 | render(); 179 | expect(await screen.findByRole('textbox')).toHaveValue('Краснодарский край, Мостовский р-н'); 180 | }); 181 | 182 | it('changes value changes input query', async () => { 183 | const { rerender } = render(); 184 | 185 | expect(await screen.findByRole('textbox')).toHaveValue(''); 186 | 187 | rerender(); 188 | expect(await screen.findByRole('textbox')).toHaveValue('Краснодарский край, Мостовский р-н'); 189 | }); 190 | 191 | it('correctly navigates by keyboard up and down arrows', async () => { 192 | render(); 193 | 194 | const input = await screen.findByRole('textbox'); 195 | await userEvent.tab(); 196 | await userEvent.type(input, 'М'); 197 | 198 | expect(await screen.findByRole('listbox')).toBeInTheDocument(); 199 | expect(screen.queryAllByRole('option')).toHaveLength(7); 200 | expect(screen.queryByRole('option', { selected: true })).not.toBeInTheDocument(); 201 | 202 | await userEvent.type(input, '{arrowdown}'); 203 | expect(screen.getByRole('option', { selected: true })).toBeInTheDocument(); 204 | expect(screen.getByRole('option', { selected: true })).toHaveTextContent('г Москва'); 205 | expect(input).toHaveValue('г Москва'); 206 | 207 | await userEvent.type(input, '{arrowdown}'); 208 | expect(screen.getByRole('option', { selected: true })).toHaveTextContent('Московская обл'); 209 | expect(input).toHaveValue('Московская обл'); 210 | 211 | await userEvent.type(input, '{arrowup}'); 212 | expect(screen.getByRole('option', { selected: true })).toHaveTextContent('г Москва'); 213 | expect(input).toHaveValue('г Москва'); 214 | 215 | await userEvent.type(input, '{arrowdown}'); 216 | await userEvent.type(input, '{arrowdown}'); 217 | await userEvent.type(input, '{arrowdown}'); 218 | await userEvent.type(input, '{arrowdown}'); 219 | await userEvent.type(input, '{arrowdown}'); 220 | await userEvent.type(input, '{arrowdown}'); 221 | await userEvent.type(input, '{arrowdown}'); 222 | await userEvent.type(input, '{arrowdown}'); 223 | await userEvent.type(input, '{arrowdown}'); 224 | expect(screen.getByRole('option', { selected: true })).toHaveTextContent('Магаданская обл'); 225 | expect(input).toHaveValue('Магаданская обл'); 226 | 227 | await userEvent.type(input, '{arrowdown}'); 228 | expect(screen.getByRole('option', { selected: true })).toHaveTextContent('Магаданская обл'); 229 | expect(input).toHaveValue('Магаданская обл'); 230 | }); 231 | 232 | it('correctly fires onKeyDown and onKeyPress', async () => { 233 | const handleKeyDownMock = vi.fn(); 234 | const handleKeyPressMock = vi.fn(); 235 | render( 236 | , 243 | ); 244 | 245 | const input = await screen.findByRole('textbox'); 246 | 247 | fireEvent.keyPress(input, { key: 'ArrowDown', charCode: 40 }); 248 | expect(handleKeyPressMock).toHaveBeenCalledTimes(1); 249 | 250 | fireEvent.keyDown(input, { key: 'ArrowDown', charCode: 40 }); 251 | expect(handleKeyDownMock).toHaveBeenCalledTimes(1); 252 | }); 253 | 254 | it('correctly fires onChange by Enter', async () => { 255 | const handleChangeMock = vi.fn(); 256 | 257 | render(); 258 | 259 | const input = await screen.findByRole('textbox'); 260 | 261 | await userEvent.tab(); 262 | await userEvent.type(input, 'Мо'); 263 | 264 | expect(await screen.findByRole('listbox')).toBeInTheDocument(); 265 | await userEvent.type(input, '{Enter}'); 266 | 267 | expect(handleChangeMock).toBeCalledTimes(0); 268 | await userEvent.type(input, '{ArrowDown}{Enter}'); 269 | 270 | expect(handleChangeMock.mock.calls.length).toBe(1); 271 | expect(handleChangeMock.mock.calls[0][0].value).toBe('г Москва'); 272 | }); 273 | 274 | it('correctly fires onChange by suggestion click', async () => { 275 | const handleChangeMock = vi.fn(); 276 | 277 | render(); 278 | const input = await screen.findByRole('textbox'); 279 | 280 | await userEvent.tab(); 281 | await userEvent.type(input, 'Мо', { delay: 0 }); 282 | 283 | const listBox = await screen.findByRole('listbox'); 284 | expect(listBox).toBeInTheDocument(); 285 | 286 | const options = queryAllByRole(listBox, 'option'); 287 | 288 | await userEvent.click(options[1]); 289 | expect(handleChangeMock).toHaveBeenCalledTimes(1); 290 | expect(handleChangeMock).toHaveBeenCalledWith(addressMocks.Мо[1]); 291 | }); 292 | 293 | it('correctly sends http parameters', async () => { 294 | render( 295 | , 305 | ); 306 | 307 | const input = await screen.findByRole('textbox'); 308 | await userEvent.tab(); 309 | 310 | await waitFor(() => { 311 | expect(requestCalls.length).toBe(1); 312 | expect(requestCalls.length).toBe(1); 313 | expect(requestCalls[0].data.query).toBe(''); 314 | expect(requestCalls[0].endpoint).toBe('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address'); 315 | }); 316 | 317 | await userEvent.type(input, 'Мо'); 318 | 319 | await waitFor(() => { 320 | expect(requestCalls.length).toBe(3); 321 | expect(requestCalls[2].data.query).toBe('Мо'); 322 | expect(requestCalls[2].data.language).toBe('en'); 323 | expect(requestCalls[2].data.from_bound).toEqual({ value: 'country' }); 324 | expect(requestCalls[2].data.to_bound).toEqual({ value: 'street' }); 325 | expect(requestCalls[2].data.locations).toEqual([{ kladr_id: '65' }]); 326 | expect(requestCalls[2].data.locations_boost).toEqual([{ kladr_id: '77' }]); 327 | expect(requestCalls[2].data.restrict_value).toBe(true); 328 | }); 329 | }); 330 | 331 | it('fires ref method setInputValue fired', async () => { 332 | const ref = createRef(); 333 | render(); 334 | 335 | ref.current?.setInputValue('Test Value'); 336 | expect(await screen.findByRole('textbox')).toHaveValue('Test Value'); 337 | }); 338 | 339 | it('fires ref method focus fired', async () => { 340 | const ref = createRef(); 341 | 342 | render(); 343 | 344 | ref.current?.focus(); 345 | expect(await screen.findByRole('textbox')).toHaveFocus(); 346 | }); 347 | 348 | it('respects debounce', async () => { 349 | const makeRequestMock = vi.spyOn(requestModule, 'makeRequest'); 350 | makeRequestMock.mockImplementation(createAddressMock()); 351 | 352 | vi.useFakeTimers({ shouldAdvanceTime: true }); 353 | 354 | const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime }); 355 | 356 | render(); 357 | const input = await screen.findByRole('textbox'); 358 | 359 | await user.tab(); 360 | await user.type(input, 'Мо'); 361 | 362 | expect(mockedRequestCalls.length).toBe(3); 363 | 364 | cleanup(); 365 | 366 | const { rerender } = render(); 367 | 368 | await user.tab(); 369 | await user.type(input, 'Мо'); 370 | 371 | expect(mockedRequestCalls.length).toBe(3); 372 | vi.advanceTimersByTime(50); 373 | expect(mockedRequestCalls.length).toBe(4); 374 | expect(mockedRequestCalls[3].data.json.query).toBe('Мо'); 375 | 376 | rerender(); 377 | await userEvent.type(input, 'ск'); 378 | expect(mockedRequestCalls.length).toBe(4); 379 | vi.advanceTimersByTime(50); 380 | expect(mockedRequestCalls.length).toBe(4); 381 | vi.advanceTimersByTime(50); 382 | expect(mockedRequestCalls.length).toBe(5); 383 | expect(mockedRequestCalls[4].data.json.query).toBe('Моск'); 384 | 385 | // vi.restoreAllMocks(); 386 | vi.useRealTimers(); 387 | }); 388 | 389 | it('correctly renders with renderOption', async () => { 390 | const renderOption: (suggestion: DaDataAddressSuggestion) => ReactNode = (suggestion) => { 391 | return RenderOption {suggestion.data.country}; 392 | }; 393 | 394 | render(); 395 | 396 | const input = await screen.findByRole('textbox'); 397 | await userEvent.tab(); 398 | await userEvent.type(input, 'Мо'); 399 | 400 | const listBox = await screen.findByRole('listbox'); 401 | expect(listBox).toBeInTheDocument(); 402 | 403 | const suggestions = await findAllByRole(listBox, 'option'); 404 | expect(suggestions).toHaveLength(7); 405 | 406 | expect(suggestions[0]).toHaveTextContent('RenderOption Россия'); 407 | }); 408 | 409 | it('correctly renders with customInput', async () => { 410 | const CustomInput = forwardRef>((props, ref) => ( 411 | 412 | )); 413 | 414 | render(); 415 | const input = await screen.findByRole('textbox'); 416 | 417 | expect(input).toHaveAttribute('data-some-attr', 'foo'); 418 | }); 419 | 420 | it('passes current input value to renderOption', async () => { 421 | const renderOption = vi.fn<(suggestion: DaDataAddressSuggestion, query: string) => ReactNode>( 422 | (suggestion: DaDataAddressSuggestion): ReactNode => { 423 | return suggestion.value; 424 | }, 425 | ); 426 | 427 | render(); 428 | 429 | const input = await screen.findByRole('textbox'); 430 | 431 | await userEvent.tab(); 432 | await userEvent.type(input, 'Мо'); 433 | 434 | expect(await screen.findByRole('listbox')).toBeInTheDocument(); 435 | 436 | expect(renderOption.mock.calls[renderOption.mock.calls.length - 1][1]).toBe('Мо'); 437 | 438 | await userEvent.type(input, 'с'); 439 | expect(renderOption.mock.calls[renderOption.mock.calls.length - 1][1]).toBe('Мос'); 440 | }); 441 | 442 | it('uses url property if provided', async () => { 443 | const makeRequestMock = vi.spyOn(requestModule, 'makeRequest'); 444 | 445 | render(); 446 | 447 | const input = await screen.findByRole('textbox'); 448 | 449 | await userEvent.tab(); 450 | await userEvent.type(input, 'Мос'); 451 | 452 | expect(makeRequestMock.mock.calls[makeRequestMock.mock.calls.length - 1][1]).toBe( 453 | 'https://example.com/suggestions/api/4_1/rs/suggest/address', 454 | ); 455 | }); 456 | 457 | it('respects selectOnBlur prop', async () => { 458 | const handleChangeMock = vi.fn(); 459 | 460 | render(); 461 | 462 | await userEvent.tab(); 463 | 464 | const input = await screen.findByRole('textbox'); 465 | await userEvent.type(input, 'Мо'); 466 | 467 | const listBox = await screen.findByRole('listbox'); 468 | expect(listBox).toBeInTheDocument(); 469 | 470 | await userEvent.tab(); 471 | 472 | expect(handleChangeMock.mock.calls.length).toBe(1); 473 | expect(handleChangeMock.mock.calls[0][0].value).toBe('г Москва'); 474 | }); 475 | 476 | it('uses uid prop', async () => { 477 | const { rerender } = render(); 478 | 479 | const combobox = await screen.findByRole('combobox'); 480 | expect(combobox.getAttribute('aria-owns')).toBeTruthy(); 481 | expect(combobox.getAttribute('aria-owns')).toEqual(combobox.getAttribute('aria-controls')); 482 | 483 | rerender(); 484 | expect(combobox.getAttribute('aria-owns')).toBe('dadata-address-order-page'); 485 | expect(combobox.getAttribute('aria-controls')).toBe('dadata-address-order-page'); 486 | }); 487 | }); 488 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Dadata 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/vitalybaev/react-dadata/badge.svg)](https://coveralls.io/github/vitalybaev/react-dadata) 4 | ![npm](https://img.shields.io/npm/dt/react-dadata) 5 | [![dependencies](https://img.shields.io/librariesio/release/npm/react-dadata/2.16.0)](https://www.npmjs.com/package/react-dadata) 6 | [![npm package](https://img.shields.io/npm/v/react-dadata.svg)](https://www.npmjs.com/package/react-dadata) 7 | [![npm downloads](https://img.shields.io/npm/dm/react-dadata.svg)](https://www.npmjs.com/package/react-dadata) 8 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-dadata)](https://bundlephobia.com/result?p=react-dadata) 9 | ![licence](https://img.shields.io/npm/l/react-dadata) 10 | 11 | Лёгкий (**~5 kb min gzip**), типизированный и настраиваемый React компонент для подсказок **адресов, организаций, 12 | банков, ФИО и email** с помощью сервиса DaData.ru 13 | 14 | [Демонстрация](https://vitalybaev.github.io/react-dadata/) 15 | 16 | **Предоставлена документация для 2.x, версия 1.x не поддерживается** 17 | 18 | ## Содержание 19 | 20 | * [Внешний вид](#%D0%B2%D0%BD%D0%B5%D1%88%D0%BD%D0%B8%D0%B9-%D0%B2%D0%B8%D0%B4) 21 | * [Адреса](#%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0) 22 | * [Организации](#%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8) 23 | * [Банки](#%D0%B1%D0%B0%D0%BD%D0%BA%D0%B8) 24 | * [Установка](#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0) 25 | * [Пример использования](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F) 26 | * [Параметры](#%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B) 27 | * [Общие параметры](#%D0%BE%D0%B1%D1%89%D0%B8%D0%B5-%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B) 28 | * [Методы](#%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D1%8B) 29 | * [Типы подсказок и примеры](#%D1%82%D0%B8%D0%BF%D1%8B-%D0%BF%D0%BE%D0%B4%D1%81%D0%BA%D0%B0%D0%B7%D0%BE%D0%BA-%D0%B8-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B) 30 | * [Адреса](#%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0-1) 31 | * [Организации](#%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8-1) 32 | * [Банки](#%D0%B1%D0%B0%D0%BD%D0%BA%D0%B8-1) 33 | * [ФИО](#%D1%84%D0%B8%D0%BE) 34 | * [Email](#email) 35 | * [Стилизация](#%D1%81%D1%82%D0%B8%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) 36 | * [TypeScript](#typescript) 37 | * [Лицензия](#%D0%BB%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F) 38 | 39 | ## Внешний вид 40 | 41 | ### Адреса 42 | 43 | СReact DaData адреса 44 | 45 | ### Организации 46 | 47 | React DaData организации 48 | 49 | ### Банки 50 | 51 | React DaData банки 52 | 53 | ## Установка 54 | 55 | ### pnpm 56 | 57 | ``` 58 | pnpm add react-dadata 59 | ``` 60 | 61 | ### yarn 62 | 63 | ``` 64 | yarn add react-dadata 65 | ``` 66 | 67 | ### npm 68 | 69 | ``` 70 | npm install react-dadata 71 | ``` 72 | 73 | ## Пример использования 74 | 75 | ```jsx 76 | import { AddressSuggestions } from 'react-dadata'; 77 | import 'react-dadata/dist/react-dadata.css'; 78 | 79 | const [value, setValue] = useState(); 80 | 81 | ; 82 | ``` 83 | 84 | ## Параметры 85 | 86 | ### Общие параметры 87 | 88 | | Свойство | Обязательный | Тип | Описание | 89 | |----------------------------|--------------|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 90 | | token | Да | string | Авторизационный токен DaData.ru | 91 | | value | Нет | DaDataSuggestion<\*> | Текущее значение, если передается, то в поле ввода будет установлено значение `value` подсказки (если не указан `defaultQuery`) а также при изменении будет менять значение в поле ввода. | 92 | | defaultQuery | Нет | string | Начальное значение поля ввода, имеет больший приоритет перед `value`. Используется только при монтировании компонента. | 93 | | delay | Нет | number | Задержка для debounce при отправке запроса в миллисекундах. По-умолчанию отсутствует, запрос отправляется на каждое изменение в поле ввода | 94 | | count | Нет | number | Количество подсказок, которое требуется получит от DaData. По-умолчанию: **10** | 95 | | autoload | Нет | boolean | Если `true`, то запрос на получение подсказок будет инициирован в фоне сразу, после монтирования компонента | 96 | | onChange | Нет | function(suggestion: DaDataSuggestion) | Функция, вызываемая при выборе подсказки | 97 | | minChars | Нет | number | Минимальное количество символов для отправки запроса к DaData. По умолчанию не задан, то есть подсказки запрашиваются на каждый ввод | 98 | | inputProps | Нет | Object of HTMLInputElement Props | любые стандартные пропсы для input. Свойство `value` игнорируется. Используйте его для передачи инпуту определенных атрибутов или для отслеживания событий | 99 | | hintText | Нет | ReactNode | Если передано, отображается в виде подсказки над списком подсказок | 100 | | renderOption | Нет | function(suggestion: DaDataSuggestion) => ReactNode | Реализуйте этот callback, чтобы вернуть компонент для отображения подсказки | 101 | | url | Нет | string | Если передан, запросы будут выполняться на этот URL (полезно, если используется прокси или коробочная версия на своем сервере) | 102 | | containerClassName | Нет | string | CSS класс для контейнера компонента, если не передан, используется класс для стилей из коробки. | 103 | | suggestionClassName | Нет | string | CSS класс для компонента подсказки в списке, если не передан, используется класс для стилей из коробки. | 104 | | currentSuggestionClassName | Нет | string | CSS класс который добавляется к компоненту текущей выбранной подсказки в списке, если не передан, используется класс для стилей из коробки. | 105 | | hintClassName | Нет | string | CSS класс блока текста-пояснения над подсказками, если не передан, используется класс для стилей из коробки. | 106 | | highlightClassName | Нет | string | CSS класс элемента, подсвечивающего совпадения при наборе, если не передан, используется класс для стилей из коробки. | 107 | | customInput | Нет | Element or string | Кастомный компонент поля ввода, например от Styled Components | 108 | | selectOnBlur | Нет | boolean | Если `true`, то при потере фокуса будет выбрана первая подсказка из списка | 109 | | uid | Нет | string | Уникальный ID который используется внутри компонента для связывания элементов при помощи aria атрибутов | 110 | | httpCache | Нет | boolean | Необходимо ли кешировать HTTP-запросы | 111 | | httpCacheTtl | Нет | number | Время жизни кеша HTTP-запросов (в миллисекундах). Значение по умолчанию - 10 минут | 112 | 113 | ## Методы 114 | 115 | Поскольку компонент классовый, он поддерживает вызов методов с помощью `ref`. 116 | 117 | ```tsx 118 | import React, { useRef } from 'react'; 119 | import { AddressSuggestions } from 'react-dadata'; 120 | import 'react-dadata/dist/react-dadata.css'; 121 | 122 | //... 123 | 124 | const suggestionsRef = useRef(null); 125 | const handleClick = () => { 126 | if (suggestionsRef.current) { 127 | suggestionsRef.current.setInputValue('Тут пример запроса'); 128 | } 129 | }; 130 | 131 | //... 132 | 133 | 134 | 135 | ``` 136 | 137 | ### focus() 138 | 139 | Вызывает событие `focus` на поле ввода 140 | 141 | ### setInputValue(value: string | undefined) 142 | 143 | Устанавливает указанный текст в поле ввода 144 | 145 | ## Типы подсказок и примеры 146 | 147 | ### Адреса 148 | 149 | ```jsx 150 | import { AddressSuggestions } from 'react-dadata'; 151 | import 'react-dadata/dist/react-dadata.css'; 152 | 153 | const [value, setValue] = useState(); 154 | 155 | ; 156 | ``` 157 | 158 | #### Дополнительные параметры для компонента адресов 159 | 160 | | Свойство | Обязательный | Тип | Описание | 161 | |----------------------| ------------ |--------------|------------------------------------------------------------------| 162 | | filterLanguage | Нет | `ru` \| `en` | Язык подсказок в ответе (по умолчанию `ru`) | 163 | | filterFromBound | Нет | string | Сужение области поиска, параметр `from_bound` в запросе | 164 | | filterToBound | Нет | string | Сужение области поиска, параметр `to_bound` в запросе | 165 | | filterLocations | Нет | array | Сужение области поиска, параметр `locations` в запросе | 166 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр `locations_boost` в запросе | 167 | | filterRestrictValue | Нет | bool | Передача параметра `restrict_value` в запросе | 168 | 169 | ### Организации в России 🇷🇺 170 | 171 | ```jsx 172 | import { PartySuggestions } from 'react-dadata'; 173 | import 'react-dadata/dist/react-dadata.css'; 174 | 175 | const [value, setValue] = useState(); 176 | 177 | ; 178 | ``` 179 | 180 | #### Дополнительные параметры для компонента организаций в России 181 | 182 | | Свойство | Обязательный | Тип | Описание | 183 | |----------------------|--------------|-----------|----------------------------------------------------------------| 184 | | filterStatus | Нет | array | Фильтр по статусу организации, параметр status в запросе | 185 | | filterType | Нет | string | Фильтр по типу организации, параметр type в запросе | 186 | | filterOkved | Нет | string[] | Фильтр по ОКВЭД | 187 | | filterLocations | Нет | array | Сужение области поиска, параметр locations в запросе | 188 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр locations_boost в запросе | 189 | 190 | ### Организации в Беларуси 🇧🇾 191 | 192 | ```jsx 193 | import { PartyBelarusSuggestions } from 'react-dadata'; 194 | import 'react-dadata/dist/react-dadata.css'; 195 | 196 | const [value, setValue] = useState(); 197 | 198 | ; 199 | ``` 200 | 201 | #### Дополнительные параметры для компонента организаций в Беларуси 202 | 203 | | Свойство | Обязательный | Тип | Описание | 204 | |----------------------|--------------|-----------|----------------------------------------------------------------| 205 | | filterStatus | Нет | `DaDataPartyBelarusStatus[]` | Фильтр по статусу организации, параметр status в запросе | 206 | | filterType | Нет | `DaDataPartyType[]` | Фильтр по типу организации, параметр type в запросе | 207 | 208 | ### Организации в Казахстане 🇰🇿 209 | 210 | ```jsx 211 | import { PartyKazakhstanSuggestions } from 'react-dadata'; 212 | import 'react-dadata/dist/react-dadata.css'; 213 | 214 | const [value, setValue] = useState(); 215 | 216 | ; 217 | ``` 218 | 219 | #### Дополнительные параметры для компонента организаций в Казахстане 220 | 221 | | Свойство | Обязательный | Тип | Описание | 222 | |----------------------|--------------|-----------|----------------------------------------------------------------| 223 | | filterType | Нет | `DaDataPartyKazakhstanType[]` | Фильтр по типу организации, параметр type в запросе | 224 | 225 | 226 | ### Банки 227 | 228 | ```jsx 229 | import { BankSuggestions } from 'react-dadata'; 230 | import 'react-dadata/dist/react-dadata.css'; 231 | 232 | const [value, setValue] = useState(); 233 | 234 | ; 235 | ``` 236 | 237 | #### Дополнительные параметры для компонента банков 238 | 239 | | Свойство | Обязательный | Тип | Описание | 240 | | -------------------- | ------------ | ------ | -------------------------------------------------------------- | 241 | | filterStatus | Нет | array | Фильтр по статусу банка, параметр status в запросе | 242 | | filterType | Нет | string | Фильтр по типу банка, параметр type в запросе | 243 | | filterLocations | Нет | array | Сужение области поиска, параметр locations в запросе | 244 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр locations_boost в запросе | 245 | 246 | ### ФИО 247 | 248 | ```jsx 249 | import { FioSuggestions } from 'react-dadata'; 250 | import 'react-dadata/dist/react-dadata.css'; 251 | 252 | const [value, setValue] = useState(); 253 | 254 | ; 255 | ``` 256 | 257 | #### Дополнительные параметры для компонента ФИО 258 | 259 | | Свойство | Обязательный | Тип | Описание | 260 | | ------------ | ------------ | ------------------------------ | ---------------------- | 261 | | filterGender | Нет | `UNKNOWN`, `MALE` или `FEMALE` | Фильтр по полу | 262 | | filterParts | Нет | string[] | Подсказки по части ФИО | 263 | 264 | ### Email 265 | 266 | ```jsx 267 | import { EmailSuggestions } from 'react-dadata'; 268 | import 'react-dadata/dist/react-dadata.css'; 269 | 270 | const [value, setValue] = useState(); 271 | 272 | ; 273 | ``` 274 | 275 | ## Стилизация 276 | 277 | `react-dadata` поставляется с опциональным CSS файлом, который из коробки неплохо выглядит и выполняет свои функции. 278 | Чтобы использовать его, укажите этот CSS файл в импорте или создайте CSS файл у себя с нужными стилями. 279 | 280 | ```jsx 281 | import { AddressSuggestions } from 'react-dadata'; 282 | 283 | // Импортируем CSS файл 284 | import 'react-dadata/dist/react-dadata.css'; 285 | 286 | // ... 287 | ; 288 | ``` 289 | 290 | **Обратите внимание**, что ваш сборщик должен быть настроен соответствующим образом для обработки CSS файлов. 291 | 292 | Если у вас в проекте используется CSS-in-JS решение, то вы должны передавать CSS классы в компонент с помощью пропсов: 293 | 294 | - `inputProps.className` - для поля ввода 295 | - `containerClassName` - для контейнера компонента 296 | - `suggestionsClassName` - для блока с подсказками 297 | - `suggestionClassName` - для блока с подсказкой 298 | - `currentSuggestionClassName` - для блока с текущей выделенной подсказкой 299 | - `hintClassName` - для блока с пояснением 300 | - `highlightClassName` - для тега `mark`, которым выделяются совпадения с введенным текстом 301 | 302 | ## TypeScript 303 | 304 | `react-dadata` написан на TypeScript, поэтому типы встроены. 305 | 306 | ```tsx 307 | import React, { useState } from 'react'; 308 | import { AddressSuggestions, DaDataSuggestion, DaDataAddress } from 'react-dadata'; 309 | import 'react-dadata/dist/react-dadata.css'; 310 | 311 | const [value, setValue] = useState | undefined>(); 312 | 313 | // Также можно воспользоваться готовым типом DaDataAddressSuggestion для адреса или DaDataPartySuggestion для организаций 314 | // import { DaDataAddressSuggestion } from 'react-dadata'; 315 | // const [value, setValue] = useState(); 316 | 317 | ; 318 | ``` 319 | 320 | ## Ошибка в консоли `Prop aria-owns did not match...` 321 | 322 | Данная ошибка возникает при использовании серверного рендеринга. Под капотом, `react-dadata`, следуя принципам 323 | доступности, создает компонент с aria ролью "combobox", которому необходимо через обычные HTML идентификаторы связывать 324 | различные элементы. При использовании SSR в виду текущей архитектуры компонента данные ID генерируются дважды 325 | независимо: на сервере и на клиенте, из-за чего в момент регидратации React выявляет несовпадение этих идентификаторов. 326 | Эта проблема решается довольно просто в функциональных компонентах, однако на данный момент у меня нет быстрого решения 327 | этой проблемы. 328 | 329 | Чтобы иметь возможность избавиться от данной ошибки при использовании SSR можно передавать пропс `uid`, в которой вы 330 | можете передать _уникальный в рамках страницы_ строковый идентификатор. 331 | 332 | Если заранее известно, сколько компонентов и в каких местах страницы будут располагаться, можно передавать в качестве 333 | идентификаторов понятные строки: 334 | 335 | ```tsx 336 | ; 342 | ``` 343 | 344 | Если вы уже обновились на React 18, то можно воспользоваться стандартным хуком `useId`: 345 | 346 | ```tsx 347 | const id = useId(); 348 | 349 | return ( 350 | 356 | ); 357 | ``` 358 | 359 | ## Лицензия 360 | 361 | ``` 362 | The MIT License 363 | 364 | Copyright (c) 2016 Vitaly Baev , baev.dev 365 | 366 | Permission is hereby granted, free of charge, to any person obtaining a copy 367 | of this software and associated documentation files (the "Software"), to deal 368 | in the Software without restriction, including without limitation the rights 369 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 370 | copies of the Software, and to permit persons to whom the Software is 371 | furnished to do so, subject to the following conditions: 372 | 373 | The above copyright notice and this permission notice shall be included in 374 | all copies or substantial portions of the Software. 375 | 376 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 377 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 378 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 379 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 380 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 381 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 382 | THE SOFTWARE. 383 | ``` 384 | 385 | ### TODO 386 | 387 | - В ближайшее время добавить подсказки для ФИО. 388 | - Увеличить покрытие тестов 389 | - Сайт с документацией 390 | - Если вам чего-то не хватает в текущем функционале - создавайте issue, попробуем помочь! 391 | -------------------------------------------------------------------------------- /packages/react-dadata/readme.md: -------------------------------------------------------------------------------- 1 | # React Dadata 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/vitalybaev/react-dadata/badge.svg)](https://coveralls.io/github/vitalybaev/react-dadata) 4 | ![npm](https://img.shields.io/npm/dt/react-dadata) 5 | [![dependencies](https://img.shields.io/librariesio/release/npm/react-dadata/2.16.0)](https://www.npmjs.com/package/react-dadata) 6 | [![npm package](https://img.shields.io/npm/v/react-dadata.svg)](https://www.npmjs.com/package/react-dadata) 7 | [![npm downloads](https://img.shields.io/npm/dm/react-dadata.svg)](https://www.npmjs.com/package/react-dadata) 8 | [![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-dadata)](https://bundlephobia.com/result?p=react-dadata) 9 | ![licence](https://img.shields.io/npm/l/react-dadata) 10 | 11 | Лёгкий (**~5 kb min gzip**), типизированный и настраиваемый React компонент для подсказок **адресов, организаций, 12 | банков, ФИО и email** с помощью сервиса DaData.ru 13 | 14 | [Демонстрация](https://vitalybaev.github.io/react-dadata/) 15 | 16 | **Предоставлена документация для 2.x, версия 1.x не поддерживается** 17 | 18 | ## Содержание 19 | 20 | * [Внешний вид](#%D0%B2%D0%BD%D0%B5%D1%88%D0%BD%D0%B8%D0%B9-%D0%B2%D0%B8%D0%B4) 21 | * [Адреса](#%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0) 22 | * [Организации](#%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8) 23 | * [Банки](#%D0%B1%D0%B0%D0%BD%D0%BA%D0%B8) 24 | * [Установка](#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0) 25 | * [Пример использования](#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F) 26 | * [Параметры](#%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B) 27 | * [Общие параметры](#%D0%BE%D0%B1%D1%89%D0%B8%D0%B5-%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D1%8B) 28 | * [Методы](#%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D1%8B) 29 | * [Типы подсказок и примеры](#%D1%82%D0%B8%D0%BF%D1%8B-%D0%BF%D0%BE%D0%B4%D1%81%D0%BA%D0%B0%D0%B7%D0%BE%D0%BA-%D0%B8-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B) 30 | * [Адреса](#%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%B0-1) 31 | * [Организации](#%D0%BE%D1%80%D0%B3%D0%B0%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8-1) 32 | * [Банки](#%D0%B1%D0%B0%D0%BD%D0%BA%D0%B8-1) 33 | * [ФИО](#%D1%84%D0%B8%D0%BE) 34 | * [Email](#email) 35 | * [Стилизация](#%D1%81%D1%82%D0%B8%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F) 36 | * [TypeScript](#typescript) 37 | * [Лицензия](#%D0%BB%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F) 38 | 39 | ## Внешний вид 40 | 41 | ### Адреса 42 | 43 | СReact DaData адреса 44 | 45 | ### Организации 46 | 47 | React DaData организации 48 | 49 | ### Банки 50 | 51 | React DaData банки 52 | 53 | ## Установка 54 | 55 | ### pnpm 56 | 57 | ``` 58 | pnpm add react-dadata 59 | ``` 60 | 61 | ### yarn 62 | 63 | ``` 64 | yarn add react-dadata 65 | ``` 66 | 67 | ### npm 68 | 69 | ``` 70 | npm install react-dadata 71 | ``` 72 | 73 | ## Пример использования 74 | 75 | ```jsx 76 | import { AddressSuggestions } from 'react-dadata'; 77 | import 'react-dadata/dist/react-dadata.css'; 78 | 79 | const [value, setValue] = useState(); 80 | 81 | ; 82 | ``` 83 | 84 | ## Параметры 85 | 86 | ### Общие параметры 87 | 88 | | Свойство | Обязательный | Тип | Описание | 89 | |----------------------------|--------------|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 90 | | token | Да | string | Авторизационный токен DaData.ru | 91 | | value | Нет | DaDataSuggestion<\*> | Текущее значение, если передается, то в поле ввода будет установлено значение `value` подсказки (если не указан `defaultQuery`) а также при изменении будет менять значение в поле ввода. | 92 | | defaultQuery | Нет | string | Начальное значение поля ввода, имеет больший приоритет перед `value`. Используется только при монтировании компонента. | 93 | | delay | Нет | number | Задержка для debounce при отправке запроса в миллисекундах. По-умолчанию отсутствует, запрос отправляется на каждое изменение в поле ввода | 94 | | count | Нет | number | Количество подсказок, которое требуется получит от DaData. По-умолчанию: **10** | 95 | | autoload | Нет | boolean | Если `true`, то запрос на получение подсказок будет инициирован в фоне сразу, после монтирования компонента | 96 | | onChange | Нет | function(suggestion: DaDataSuggestion) | Функция, вызываемая при выборе подсказки | 97 | | minChars | Нет | number | Минимальное количество символов для отправки запроса к DaData. По умолчанию не задан, то есть подсказки запрашиваются на каждый ввод | 98 | | inputProps | Нет | Object of HTMLInputElement Props | любые стандартные пропсы для input. Свойство `value` игнорируется. Используйте его для передачи инпуту определенных атрибутов или для отслеживания событий | 99 | | hintText | Нет | ReactNode | Если передано, отображается в виде подсказки над списком подсказок | 100 | | renderOption | Нет | function(suggestion: DaDataSuggestion) => ReactNode | Реализуйте этот callback, чтобы вернуть компонент для отображения подсказки | 101 | | url | Нет | string | Если передан, запросы будут выполняться на этот URL (полезно, если используется прокси или коробочная версия на своем сервере) | 102 | | containerClassName | Нет | string | CSS класс для контейнера компонента, если не передан, используется класс для стилей из коробки. | 103 | | suggestionClassName | Нет | string | CSS класс для компонента подсказки в списке, если не передан, используется класс для стилей из коробки. | 104 | | currentSuggestionClassName | Нет | string | CSS класс который добавляется к компоненту текущей выбранной подсказки в списке, если не передан, используется класс для стилей из коробки. | 105 | | hintClassName | Нет | string | CSS класс блока текста-пояснения над подсказками, если не передан, используется класс для стилей из коробки. | 106 | | highlightClassName | Нет | string | CSS класс элемента, подсвечивающего совпадения при наборе, если не передан, используется класс для стилей из коробки. | 107 | | customInput | Нет | Element or string | Кастомный компонент поля ввода, например от Styled Components | 108 | | selectOnBlur | Нет | boolean | Если `true`, то при потере фокуса будет выбрана первая подсказка из списка | 109 | | uid | Нет | string | Уникальный ID который используется внутри компонента для связывания элементов при помощи aria атрибутов | 110 | | httpCache | Нет | boolean | Необходимо ли кешировать HTTP-запросы | 111 | | httpCacheTtl | Нет | number | Время жизни кеша HTTP-запросов (в миллисекундах). Значение по умолчанию - 10 минут | 112 | 113 | ## Методы 114 | 115 | Поскольку компонент классовый, он поддерживает вызов методов с помощью `ref`. 116 | 117 | ```tsx 118 | import React, { useRef } from 'react'; 119 | import { AddressSuggestions } from 'react-dadata'; 120 | import 'react-dadata/dist/react-dadata.css'; 121 | 122 | //... 123 | 124 | const suggestionsRef = useRef(null); 125 | const handleClick = () => { 126 | if (suggestionsRef.current) { 127 | suggestionsRef.current.setInputValue('Тут пример запроса'); 128 | } 129 | }; 130 | 131 | //... 132 | 133 | 134 | 135 | ``` 136 | 137 | ### focus() 138 | 139 | Вызывает событие `focus` на поле ввода 140 | 141 | ### setInputValue(value: string | undefined) 142 | 143 | Устанавливает указанный текст в поле ввода 144 | 145 | ## Типы подсказок и примеры 146 | 147 | ### Адреса 148 | 149 | ```jsx 150 | import { AddressSuggestions } from 'react-dadata'; 151 | import 'react-dadata/dist/react-dadata.css'; 152 | 153 | const [value, setValue] = useState(); 154 | 155 | ; 156 | ``` 157 | 158 | #### Дополнительные параметры для компонента адресов 159 | 160 | | Свойство | Обязательный | Тип | Описание | 161 | |----------------------| ------------ |--------------|------------------------------------------------------------------| 162 | | filterLanguage | Нет | `ru` \| `en` | Язык подсказок в ответе (по умолчанию `ru`) | 163 | | filterFromBound | Нет | string | Сужение области поиска, параметр `from_bound` в запросе | 164 | | filterToBound | Нет | string | Сужение области поиска, параметр `to_bound` в запросе | 165 | | filterLocations | Нет | array | Сужение области поиска, параметр `locations` в запросе | 166 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр `locations_boost` в запросе | 167 | | filterRestrictValue | Нет | bool | Передача параметра `restrict_value` в запросе | 168 | 169 | ### Организации в России 🇷🇺 170 | 171 | ```jsx 172 | import { PartySuggestions } from 'react-dadata'; 173 | import 'react-dadata/dist/react-dadata.css'; 174 | 175 | const [value, setValue] = useState(); 176 | 177 | ; 178 | ``` 179 | 180 | #### Дополнительные параметры для компонента организаций в России 181 | 182 | | Свойство | Обязательный | Тип | Описание | 183 | |----------------------|--------------|-----------|----------------------------------------------------------------| 184 | | filterStatus | Нет | array | Фильтр по статусу организации, параметр status в запросе | 185 | | filterType | Нет | string | Фильтр по типу организации, параметр type в запросе | 186 | | filterOkved | Нет | string[] | Фильтр по ОКВЭД | 187 | | filterLocations | Нет | array | Сужение области поиска, параметр locations в запросе | 188 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр locations_boost в запросе | 189 | 190 | ### Организации в Беларуси 🇧🇾 191 | 192 | ```jsx 193 | import { PartyBelarusSuggestions } from 'react-dadata'; 194 | import 'react-dadata/dist/react-dadata.css'; 195 | 196 | const [value, setValue] = useState(); 197 | 198 | ; 199 | ``` 200 | 201 | #### Дополнительные параметры для компонента организаций в Беларуси 202 | 203 | | Свойство | Обязательный | Тип | Описание | 204 | |----------------------|--------------|-----------|----------------------------------------------------------------| 205 | | filterStatus | Нет | `DaDataPartyBelarusStatus[]` | Фильтр по статусу организации, параметр status в запросе | 206 | | filterType | Нет | `DaDataPartyType[]` | Фильтр по типу организации, параметр type в запросе | 207 | 208 | ### Организации в Казахстане 🇰🇿 209 | 210 | ```jsx 211 | import { PartyKazakhstanSuggestions } from 'react-dadata'; 212 | import 'react-dadata/dist/react-dadata.css'; 213 | 214 | const [value, setValue] = useState(); 215 | 216 | ; 217 | ``` 218 | 219 | #### Дополнительные параметры для компонента организаций в Казахстане 220 | 221 | | Свойство | Обязательный | Тип | Описание | 222 | |----------------------|--------------|-----------|----------------------------------------------------------------| 223 | | filterType | Нет | `DaDataPartyKazakhstanType[]` | Фильтр по типу организации, параметр type в запросе | 224 | 225 | 226 | ### Банки 227 | 228 | ```jsx 229 | import { BankSuggestions } from 'react-dadata'; 230 | import 'react-dadata/dist/react-dadata.css'; 231 | 232 | const [value, setValue] = useState(); 233 | 234 | ; 235 | ``` 236 | 237 | #### Дополнительные параметры для компонента банков 238 | 239 | | Свойство | Обязательный | Тип | Описание | 240 | | -------------------- | ------------ | ------ | -------------------------------------------------------------- | 241 | | filterStatus | Нет | array | Фильтр по статусу банка, параметр status в запросе | 242 | | filterType | Нет | string | Фильтр по типу банка, параметр type в запросе | 243 | | filterLocations | Нет | array | Сужение области поиска, параметр locations в запросе | 244 | | filterLocationsBoost | Нет | array | Указание приоритета города, параметр locations_boost в запросе | 245 | 246 | ### ФИО 247 | 248 | ```jsx 249 | import { FioSuggestions } from 'react-dadata'; 250 | import 'react-dadata/dist/react-dadata.css'; 251 | 252 | const [value, setValue] = useState(); 253 | 254 | ; 255 | ``` 256 | 257 | #### Дополнительные параметры для компонента ФИО 258 | 259 | | Свойство | Обязательный | Тип | Описание | 260 | | ------------ | ------------ | ------------------------------ | ---------------------- | 261 | | filterGender | Нет | `UNKNOWN`, `MALE` или `FEMALE` | Фильтр по полу | 262 | | filterParts | Нет | string[] | Подсказки по части ФИО | 263 | 264 | ### Email 265 | 266 | ```jsx 267 | import { EmailSuggestions } from 'react-dadata'; 268 | import 'react-dadata/dist/react-dadata.css'; 269 | 270 | const [value, setValue] = useState(); 271 | 272 | ; 273 | ``` 274 | 275 | ## Стилизация 276 | 277 | `react-dadata` поставляется с опциональным CSS файлом, который из коробки неплохо выглядит и выполняет свои функции. 278 | Чтобы использовать его, укажите этот CSS файл в импорте или создайте CSS файл у себя с нужными стилями. 279 | 280 | ```jsx 281 | import { AddressSuggestions } from 'react-dadata'; 282 | 283 | // Импортируем CSS файл 284 | import 'react-dadata/dist/react-dadata.css'; 285 | 286 | // ... 287 | ; 288 | ``` 289 | 290 | **Обратите внимание**, что ваш сборщик должен быть настроен соответствующим образом для обработки CSS файлов. 291 | 292 | Если у вас в проекте используется CSS-in-JS решение, то вы должны передавать CSS классы в компонент с помощью пропсов: 293 | 294 | - `inputProps.className` - для поля ввода 295 | - `containerClassName` - для контейнера компонента 296 | - `suggestionsClassName` - для блока с подсказками 297 | - `suggestionClassName` - для блока с подсказкой 298 | - `currentSuggestionClassName` - для блока с текущей выделенной подсказкой 299 | - `hintClassName` - для блока с пояснением 300 | - `highlightClassName` - для тега `mark`, которым выделяются совпадения с введенным текстом 301 | 302 | ## TypeScript 303 | 304 | `react-dadata` написан на TypeScript, поэтому типы встроены. 305 | 306 | ```tsx 307 | import React, { useState } from 'react'; 308 | import { AddressSuggestions, DaDataSuggestion, DaDataAddress } from 'react-dadata'; 309 | import 'react-dadata/dist/react-dadata.css'; 310 | 311 | const [value, setValue] = useState | undefined>(); 312 | 313 | // Также можно воспользоваться готовым типом DaDataAddressSuggestion для адреса или DaDataPartySuggestion для организаций 314 | // import { DaDataAddressSuggestion } from 'react-dadata'; 315 | // const [value, setValue] = useState(); 316 | 317 | ; 318 | ``` 319 | 320 | ## Ошибка в консоли `Prop aria-owns did not match...` 321 | 322 | Данная ошибка возникает при использовании серверного рендеринга. Под капотом, `react-dadata`, следуя принципам 323 | доступности, создает компонент с aria ролью "combobox", которому необходимо через обычные HTML идентификаторы связывать 324 | различные элементы. При использовании SSR в виду текущей архитектуры компонента данные ID генерируются дважды 325 | независимо: на сервере и на клиенте, из-за чего в момент регидратации React выявляет несовпадение этих идентификаторов. 326 | Эта проблема решается довольно просто в функциональных компонентах, однако на данный момент у меня нет быстрого решения 327 | этой проблемы. 328 | 329 | Чтобы иметь возможность избавиться от данной ошибки при использовании SSR можно передавать пропс `uid`, в которой вы 330 | можете передать _уникальный в рамках страницы_ строковый идентификатор. 331 | 332 | Если заранее известно, сколько компонентов и в каких местах страницы будут располагаться, можно передавать в качестве 333 | идентификаторов понятные строки: 334 | 335 | ```tsx 336 | ; 342 | ``` 343 | 344 | Если вы уже обновились на React 18, то можно воспользоваться стандартным хуком `useId`: 345 | 346 | ```tsx 347 | const id = useId(); 348 | 349 | return ( 350 | 356 | ); 357 | ``` 358 | 359 | ## Лицензия 360 | 361 | ``` 362 | The MIT License 363 | 364 | Copyright (c) 2016 Vitaly Baev , baev.dev 365 | 366 | Permission is hereby granted, free of charge, to any person obtaining a copy 367 | of this software and associated documentation files (the "Software"), to deal 368 | in the Software without restriction, including without limitation the rights 369 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 370 | copies of the Software, and to permit persons to whom the Software is 371 | furnished to do so, subject to the following conditions: 372 | 373 | The above copyright notice and this permission notice shall be included in 374 | all copies or substantial portions of the Software. 375 | 376 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 377 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 378 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 379 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 380 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 381 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 382 | THE SOFTWARE. 383 | ``` 384 | 385 | ### TODO 386 | 387 | - В ближайшее время добавить подсказки для ФИО. 388 | - Увеличить покрытие тестов 389 | - Сайт с документацией 390 | - Если вам чего-то не хватает в текущем функционале - создавайте issue, попробуем помочь! 391 | --------------------------------------------------------------------------------