diff --git a/examples/debug.tsx b/examples/debug.tsx new file mode 100644 index 0000000..9a447e5 --- /dev/null +++ b/examples/debug.tsx @@ -0,0 +1,15 @@ +/* eslint no-console: 0 */ + +import React from 'react'; +import Mentions from '../src'; +import '../assets/index.less'; + +const { Option } = Mentions; + +export default () => ( + + + + + +); diff --git a/package.json b/package.json index be60fc7..71b187c 100644 --- a/package.json +++ b/package.json @@ -39,22 +39,21 @@ "react-dom": ">=16.9.0" }, "devDependencies": { + "@testing-library/jest-dom": "^5.16.4", + "@testing-library/react": "^13.3.0", "@types/classnames": "^2.2.6", - "@types/enzyme": "^3.1.15", "@types/react": "^18.0.8", "@types/react-dom": "^18.0.3", "@types/warning": "^3.0.0", "@umijs/fabric": "^2.0.8", - "enzyme": "^3.11.0", - "enzyme-to-json": "^3.1.4", "eslint": "^7.0.0", "father": "^2.13.6", "jest": "^28.0.3", "lodash.debounce": "^4.0.8", "np": "^7.0.0", "prettier": "^2.0.5", - "react": "^16.0.0", - "react-dom": "^16.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0", "typescript": "^4.0.3" }, "dependencies": { @@ -63,6 +62,6 @@ "rc-menu": "~9.6.0", "rc-textarea": "^0.3.0", "rc-trigger": "^5.0.4", - "rc-util": "^5.0.1" + "rc-util": "^5.22.5" } } diff --git a/src/DropdownMenu.tsx b/src/DropdownMenu.tsx index 29a6c15..f385ecc 100644 --- a/src/DropdownMenu.tsx +++ b/src/DropdownMenu.tsx @@ -1,7 +1,7 @@ import Menu, { MenuItem } from 'rc-menu'; import * as React from 'react'; -import { MentionsContextConsumer, MentionsContextProps } from './MentionsContext'; -import { OptionProps } from './Option'; +import MentionsContext from './MentionsContext'; +import type { OptionProps } from './Option'; interface DropdownMenuProps { prefixCls?: string; @@ -12,54 +12,50 @@ interface DropdownMenuProps { * We only use Menu to display the candidate. * The focus is controlled by textarea to make accessibility easy. */ -class DropdownMenu extends React.Component { - public renderDropdown = ({ +function DropdownMenu(props: DropdownMenuProps) { + const { notFoundContent, activeIndex, setActiveIndex, selectOption, onFocus, onBlur, - }: MentionsContextProps) => { - const { prefixCls, options } = this.props; - const activeOption = options[activeIndex] || {}; + } = React.useContext(MentionsContext); - return ( - { - const option = options.find(({ key: optionKey }) => optionKey === key); - selectOption(option); - }} - onFocus={onFocus} - onBlur={onBlur} - > - {options.map((option, index) => { - const { key, disabled, children, className, style } = option; - return ( - { - setActiveIndex(index); - }} - > - {children} - - ); - })} + const { prefixCls, options } = props; + const activeOption = options[activeIndex] || {}; - {!options.length && {notFoundContent}} - - ); - }; + return ( + { + const option = options.find(({ key: optionKey }) => optionKey === key); + selectOption(option); + }} + onFocus={onFocus} + onBlur={onBlur} + > + {options.map((option, index) => { + const { key, disabled, children, className, style } = option; + return ( + { + setActiveIndex(index); + }} + > + {children} + + ); + })} - public render() { - return {this.renderDropdown}; - } + {!options.length && {notFoundContent}} + + ); } export default DropdownMenu; diff --git a/src/Mentions.tsx b/src/Mentions.tsx index 1769e66..98565ee 100644 --- a/src/Mentions.tsx +++ b/src/Mentions.tsx @@ -1,23 +1,29 @@ import classNames from 'classnames'; +import useMergedState from 'rc-util/lib/hooks/useMergedState'; import toArray from 'rc-util/lib/Children/toArray'; import KeyCode from 'rc-util/lib/KeyCode'; import * as React from 'react'; -import TextArea, { TextAreaProps } from 'rc-textarea'; +import { useState, useRef } from 'react'; +import TextArea from 'rc-textarea'; +import type { TextAreaProps } from 'rc-textarea'; import KeywordTrigger from './KeywordTrigger'; -import { MentionsContextProvider } from './MentionsContext'; -import Option, { OptionProps } from './Option'; +import MentionsContext from './MentionsContext'; +import Option from './Option'; +import type { OptionProps } from './Option'; import { filterOption as defaultFilterOption, getBeforeSelectionText, getLastMeasureIndex, - omit, - Omit, replaceWithMeasure, setInputSelection, validateSearch as defaultValidateSearch, } from './util'; +import useEffectState from './hooks/useEffectState'; -type BaseTextareaAttrs = Omit; +type BaseTextareaAttrs = Omit< + TextAreaProps, + 'prefix' | 'onChange' | 'onSelect' +>; export type Placement = 'top' | 'bottom'; export type Direction = 'ltr' | 'rtl'; @@ -44,91 +50,174 @@ export interface MentionsProps extends BaseTextareaAttrs { onBlur?: React.FocusEventHandler; getPopupContainer?: () => HTMLElement; dropdownClassName?: string; + /** @private Testing usage. Do not use in prod. It will not work as your expect. */ + open?: boolean; + children?: React.ReactNode; } -interface MentionsState { - value: string; - measuring: boolean; - measureText: string | null; - measurePrefix: string; - measureLocation: number; - activeIndex: number; - isFocus: boolean; -} -class Mentions extends React.Component { - public static Option = Option; - - public textarea?: HTMLTextAreaElement; - public measure?: HTMLDivElement; - - public focusId: number | undefined = undefined; +export interface MentionsRef { + focus: VoidFunction; + blur: VoidFunction; +} - public static defaultProps = { - prefixCls: 'rc-mentions', - prefix: '@', - split: ' ', - validateSearch: defaultValidateSearch, - filterOption: defaultFilterOption, - notFoundContent: 'Not Found', - rows: 1, +const Mentions = React.forwardRef((props, ref) => { + const { + // Style + prefixCls, + className, + style, + + // Misc + prefix, + split, + notFoundContent, + value, + defaultValue, + children, + + // Events + validateSearch, + filterOption, + onChange, + onKeyDown, + onKeyUp, + onPressEnter, + onSearch, + onSelect, + + onFocus, + onBlur, + + // Dropdown + transitionName, + placement, + direction, + getPopupContainer, + dropdownClassName, + + // Rest + ...restProps + } = props; + + // =============================== Refs =============================== + const textareaRef = useRef