Add types for Menu.Divider and Menu.Header (#32299)

* add Menu.Divider and Menu.Header to Menu class

* remove csstype dependency, just grab it from React

* update renderToken prop definition from v3.0 upgrade

* add typings for <Token /> and other exported Components, update tests

* fix labelKey type to only allow for property names of an object where the value is a string

* add ClearButton test
This commit is contained in:
Dale Fenton
2019-01-28 18:44:17 -05:00
committed by Pranav Senthilnathan
parent 7abf322d4e
commit 2e803abbff
3 changed files with 343 additions and 71 deletions

View File

@@ -4,37 +4,121 @@
// Rajab Shakirov <https://github.com/radziksh>
// Paito Anderson <https://github.com/PaitoAnderson>
// Andreas Richter <https://github.com/arichter83>
// Dale Fenton <https://github.com/dalevfenton>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TODO: <Token> components
// TypeScript Version: 2.9
import * as React from 'react';
import * as CSS from 'csstype';
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export type StringPropertyNames<T extends object> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
export interface TypeaheadFilterbyProps {
filterBy: string[];
labelKey: (string | (() => void));
multiple: boolean;
selected: any[];
caseSensitive: boolean;
ignoreDiacritics: boolean;
/* ---------------------------------------------------------------------------
Constants and Enumerated Types
--------------------------------------------------------------------------- */
export type TypeaheadModel = string|object;
export type TypeaheadBsSizes = 'large' | 'lg' | 'small' | 'sm';
export type TypeaheadAlign = 'justify' | 'left' | 'right';
// if options is an object, only let labelKey be a key of that object, whose value is a string
// or a custom label function that takes a single option and returns a string for the label
export type TypeaheadLabelKey<T extends TypeaheadModel> = T extends object ? (StringPropertyNames<T> | ((option: T) => string)) : never;
// don't allow onBlur, onChange, onFocus or onKeyDown as members of inputProps
// those props should be supplied directly to <Typeahead /> or <AsyncTypeahead />
export interface InputProps extends Omit<React.InputHTMLAttributes<'input'>, 'onBlur'|'onChange'|'onFocus'|'onKeyDown'> { }
/* ---------------------------------------------------------------------------
Typeahead Contexts
--------------------------------------------------------------------------- */
export interface TypeaheadContext<T extends TypeaheadModel> {
activeIndex?: number;
hintText?: string;
initialItem?: T;
isOnlyResult?: boolean;
onActiveItemChange?: (options: T) => void;
onAdd?: (option: T) => void;
onInitialItemChange?: (option: T) => void;
onMenuItemClick?: (option: T, e: Event) => void;
selectHintOnEnter?: boolean;
}
export interface TypeaheadState<T extends TypeaheadModel> {
activeIndex: number|null;
activeItem: T|null;
initialItem: T|null;
isFocused: boolean;
selected: T[];
showMenu: boolean;
shownResults: number;
text: string;
}
export interface TypeaheadMenuProps<T> {
export interface InputContainerPropsSingle<T extends TypeaheadModel> {
'aria-activedescendant': string;
'aria-autocomplete': 'list' | 'both';
'aria-expanded': boolean | 'true' | 'false';
'aria-haspopup': 'listbox';
'aria-owns': string;
autoComplete: string;
disabled: boolean;
inputRef: React.LegacyRef<HTMLInputElement>;
onBlur: (e: Event) => void;
onChange: (selected: T[]) => void;
onClick: (e: Event) => void;
onFocus: (e: Event) => void;
onKeyDown: (e: Event) => void;
placeholder: string|null;
role: 'combobox';
value: string;
}
export interface InputContainerPropsMultiple<T extends TypeaheadModel> extends Omit<InputContainerPropsSingle<T>, 'role'> {
inputClassName: string;
labelKey: TypeaheadLabelKey<T>;
onRemove: (e: Event) => void;
renderToken: (selectedItem: T, props: TypeaheadMenuProps<T>, index: number) => React.ReactNode;
role: '';
selected: T[];
}
export type HintedInputContextKeys = 'hintText' | 'initialItem' | 'onAdd' | 'selectHintOnEnter';
export interface HintedInputContext<T extends TypeaheadModel> extends Pick<TypeaheadContext<T>, HintedInputContextKeys> {}
export type MenuItemContextKeys = 'activeIndex' | 'isOnlyResult' | 'onActiveItemChange' | 'onInitialItemChange' | 'onMenuItemClick';
export interface MenuItemContext<T extends TypeaheadModel> extends Pick<TypeaheadContext<T>, MenuItemContextKeys> {}
export interface TokenContext {
active: boolean;
onBlur: (e: any) => void;
onClick: (e: any) => void;
onFocus: (e: any) => void;
onKeyDown: (e: any) => void;
}
/* ---------------------------------------------------------------------------
Typeahead Props and Component
--------------------------------------------------------------------------- */
export interface TypeaheadContainerProps<T extends TypeaheadModel> {
activeIndex: number | null;
activeItem: T | null;
initialItem: T | null;
isFocused: boolean;
selected: T[];
showMenu: boolean;
shownResults: number;
text: string;
}
export interface TypeaheadProps<T> {
/* For localized accessibility: Should return a string indicating the number of results for screen readers. Receives the current results. */
export interface TypeaheadProps<T extends TypeaheadModel> {
/* For localized accessibility: Should return a string indicating the number of results for screen readers.
Receives the current results. */
a11yNumResults?: () => void;
/* For localized accessibility: Should return a string indicating the number of selections for screen readers. Receives the current selections. */
/* For localized accessibility: Should return a string indicating the number of selections for screen readers.
Receives the current selections. */
a11yNumSelected?: () => void;
/* Specify menu alignment. The default value is justify, which makes the menu as wide as the input and truncates long values.
Specifying left or right will align the menu to that side and the width will be determined by the length of menu item values. */
align?: 'justify' | 'left' | 'right';
align?: TypeaheadAlign;
/* Allows the creation of new selections on the fly. Any new items will be added to the list of selections,
but not the list of original options unless handled as such by Typeahead's parent.
@@ -49,7 +133,7 @@ export interface TypeaheadProps<T> {
bodyContainer?: boolean;
/* Specify the size of the input. */
bsSize?: 'large' | 'lg' | 'small' | 'sm';
bsSize?: TypeaheadBsSizes;
/* Whether or not filtering should be case-sensitive. */
caseSensitive?: boolean;
@@ -60,6 +144,9 @@ export interface TypeaheadProps<T> {
/* The initial value displayed in the text input. */
defaultInputValue?: string;
/* Whether or not the menu is displayed upon initial render. */
defaultOpen?: boolean;
/* Specify any pre-selected options. Use only if you want the component to be uncontrolled. */
defaultSelected?: T[];
@@ -74,7 +161,10 @@ export interface TypeaheadProps<T> {
emptyLabel?: string;
/* Either an array of fields in option to search, or a custom filtering callback. */
filterBy?: (string[] | ((option: T | string, props: TypeaheadFilterbyProps) => boolean));
filterBy?: (string[] | ((option: T, props: AllTypeaheadOwnAndInjectedProps<T>) => boolean));
/* Whether or not to automatically adjust the position of the menu when it reaches the viewport boundaries. */
flip?: boolean;
/* Highlights the menu item if there is only one result and allows selecting that item by hitting enter.
Does not work with allowNew. */
@@ -84,7 +174,7 @@ export interface TypeaheadProps<T> {
ignoreDiacritics?: boolean;
/* Props to be applied directly to the input. onBlur, onChange, onFocus, and onKeyDown are ignored. */
inputProps?: object;
inputProps?: InputProps;
/* Bootstrap 4 only. Adds the `is-invalid` classname to the `form-control`. */
isInvalid?: boolean;
@@ -95,8 +185,9 @@ export interface TypeaheadProps<T> {
/* Bootstrap 4 only. Adds the `is-valid` classname to the `form-control`. */
isValid?: boolean;
/* Specify which option key to use for display or a render function. By default, the selector will use the label key. */
labelKey?: string | ((option: T | string) => string);
/* Specify which option key to use for display or a render function.
By default, the selector will use the label key. */
labelKey?: TypeaheadLabelKey<T>;
/* Maximum height of the dropdown menu. */
maxHeight?: string;
@@ -105,6 +196,9 @@ export interface TypeaheadProps<T> {
so as not to render too many DOM nodes in the case of large data sets. */
maxResults?: number;
/* Id applied to the top-level menu element. Required for accessibility. */
menuId?: string;
/* Number of input characters that must be entered before showing results. */
minLength?: number;
@@ -115,28 +209,35 @@ export interface TypeaheadProps<T> {
newSelectionPrefix?: string;
/* Invoked when the input is blurred. Receives an event. */
onBlur?: (e: Event) => any;
onBlur?: (e: Event) => void;
/* Invoked whenever items are added or removed. Receives an array of the selected options. */
onChange?: (selected: T[]) => any;
onChange?: (selected: T[]) => void;
/* Invoked when the input is focused. Receives an event. */
onFocus?: (e: Event) => any;
onFocus?: (e: Event) => void;
/* Invoked when the input value changes. Receives the string value of the input, as well as the original event. */
onInputChange?: (input: string, e: Event) => any;
onInputChange?: (input: string, e: Event) => void;
/* Invoked when a key is pressed. Receives an event. */
onKeyDown?: (e: Event) => any;
onKeyDown?: (e: Event) => void;
/* Invoked when the menu is hidden. */
onMenuHide?: (e: Event) => any;
/* DEPRECATED: Invoked when the menu is hidden. */
onMenuHide?: () => void;
/* Invoked when the menu is shown. */
onMenuShow?: (e: Event) => any;
/* DEPRECATED: Invoked when the menu is shown. */
onMenuShow?: () => void;
/* Invoked when menu visibility changes. */
onMenuToggle?: (show: boolean) => void;
/* Invoked when the pagination menu item is clicked. */
onPaginate?: (e: Event) => any;
onPaginate?: (e: Event, numResults: number) => void;
/* Whether or not the menu should be displayed. undefined allows the component to control visibility,
while true and false show and hide the menu, respectively. */
open?: boolean;
/* Full set of options, including any pre-selected options. */
options: T[];
@@ -151,13 +252,13 @@ export interface TypeaheadProps<T> {
placeholder?: string;
/* Callback for custom menu rendering. */
renderMenu?: (results: Array<T | string>, menuProps: any) => any;
renderMenu?: (results: T[], menuProps: any) => React.ReactNode;
/* Provides a hook for customized rendering of menu item contents. */
renderMenuItemChildren?: (option: T, props: TypeaheadMenuProps<T>, index: number) => any;
renderMenuItemChildren?: (option: T, props: TypeaheadMenuProps<T>, index: number) => React.ReactNode;
/* Provides a hook for customized rendering of tokens when multiple selections are enabled. */
renderToken?: (selectedItem: T | string, onRemove: () => void) => any;
renderToken?: (selectedItem: T, props: TypeaheadMenuProps<T>, index: number) => React.ReactNode;
/* The selected option(s) displayed in the input. Use this prop if you want to control the component via its parent. */
selected?: T[];
@@ -166,9 +267,13 @@ export interface TypeaheadProps<T> {
selectHintOnEnter?: boolean;
}
export const Typeahead: React.ClassicComponentClass<TypeaheadProps<any>>;
export type AllTypeaheadOwnAndInjectedProps<T extends TypeaheadModel> = TypeaheadProps<T> & TypeaheadContainerProps<T>;
export class Typeahead<T extends TypeaheadModel> extends React.Component<TypeaheadProps<T>> { }
export interface AsyncTypeaheadProps<T> extends TypeaheadProps<T> {
/* ---------------------------------------------------------------------------
AsyncTypeahead Props and Component
--------------------------------------------------------------------------- */
export interface AsyncTypeaheadProps<T extends TypeaheadModel> extends TypeaheadProps<T> {
/* Delay, in milliseconds, before performing search. */
delay?: number;
@@ -179,45 +284,148 @@ export interface AsyncTypeaheadProps<T> extends TypeaheadProps<T> {
onSearch: (query: string) => void;
/* Message displayed in the menu when there is no user input. */
promptText?: string;
promptText?: React.ReactNode;
/* Message to display in the menu while the request is pending. */
searchText?: string;
searchText?: React.ReactNode;
/* Whether or not the component should cache query results. */
useCache?: boolean;
}
export const AsyncTypeahead: React.ClassicComponentClass<AsyncTypeaheadProps<any>>;
export class AsyncTypeahead<T extends TypeaheadModel> extends React.Component<AsyncTypeaheadProps<T>> { }
export interface HighligherProps<T> {
key?: string;
search: string;
optionId?: any;
/* ---------------------------------------------------------------------------
TypeaheadInputSingle & TypeaheadInputMulti Props and Component
--------------------------------------------------------------------------- */
export interface BaseTypeaheadInputProps extends React.InputHTMLAttributes<'input'> {
type: 'text';
}
export const Highlighter: React.ClassicComponentClass<HighligherProps<any>>;
export interface TypeaheadSingleInputWithHocProps<T extends TypeaheadModel> extends
Omit<BaseTypeaheadInputProps, keyof InputContainerPropsSingle<T>>,
InputContainerPropsSingle<T> {}
export interface MenuProps<T> {
export interface TypeaheadMulitInputWithHocProps<T extends TypeaheadModel> extends
Omit<BaseTypeaheadInputProps, keyof InputContainerPropsMultiple<T>>,
HintedInputContext<T>,
InputContainerPropsMultiple<T> {}
export type TypeaheadInputPropKeys = 'bsSize'|'disabled'|'inputProps'|'labelKey'|'multiple'|
'onBlur'|'onChange'|'onFocus'|'onKeyDown'|'placeholder'|'renderToken'|'selected';
export type TypeaheadInputProps<T extends TypeaheadModel> = Pick<TypeaheadProps<T>, TypeaheadInputPropKeys>;
export class TypeaheadInputSingle<T extends TypeaheadModel> extends React.Component<TypeaheadSingleInputWithHocProps<T>> { }
export class TypeaheadInputMulti<T extends TypeaheadModel> extends React.Component<TypeaheadMulitInputWithHocProps<T>> { }
/* ---------------------------------------------------------------------------
Highlighter Props and Component
--------------------------------------------------------------------------- */
export interface HighligherProps {
children: React.ReactNode;
search: string;
}
export class Highlighter extends React.PureComponent<HighligherProps> { }
/* ---------------------------------------------------------------------------
ClearButton Props and Component
--------------------------------------------------------------------------- */
export interface ClearButtonProps extends React.HTMLAttributes<'button'> {
bsSize?: TypeaheadBsSizes;
label?: string;
onClick: React.HTMLAttributes<'button'>['onClick']; // make onClick requried
}
export const ClearButton: React.FunctionComponent<ClearButtonProps>;
/* ---------------------------------------------------------------------------
Loader Props and Component
--------------------------------------------------------------------------- */
export interface LoaderProps {
bsSize: TypeaheadBsSizes;
}
export const Loader: React.FunctionComponent<LoaderProps>;
/* ---------------------------------------------------------------------------
AutosizeInput Props and Component
--------------------------------------------------------------------------- */
export interface AutosizeInputProps extends Pick<React.InputHTMLAttributes<'input'>, 'className' | 'style'> {
inputClassName?: string;
inputRef?: React.LegacyRef<HTMLInputElement>;
inputStyle?: Pick<React.CSSProperties, 'boxSizing' | 'width'>;
style: React.CSSProperties;
}
export class AutosizeInput extends React.Component<AutosizeInputProps> { }
/* ---------------------------------------------------------------------------
Menu Props and Component
--------------------------------------------------------------------------- */
export interface MenuProps {
id: string;
className?: string;
emptyLabel?: string;
innerRef?: string;
innerRef?: React.LegacyRef<HTMLUListElement>;
maxHeight?: string;
style?: CSS.Properties;
style?: React.CSSProperties;
text?: string;
}
export const Menu: React.ClassicComponentClass<MenuProps<any>>;
export type MenuHeaderProps = Omit<React.HTMLProps<'li'>, 'className'>;
export interface MenuItemProps<T> {
export class Menu extends React.Component<MenuProps> {
static Divider: React.FunctionComponent;
static Header: React.FunctionComponent<MenuHeaderProps>;
}
/* ---------------------------------------------------------------------------
TypeaheadMenu Props and Component
--------------------------------------------------------------------------- */
// prop names that Typeahead provides to TypeaheadMenu
export type TypeaheadMenuPropsPick = 'labelKey' | 'newSelectionPrefix'| 'options' | 'renderMenuItemChildren';
export interface TypeaheadMenuProps<T extends TypeaheadModel> extends MenuProps, Pick<AllTypeaheadOwnAndInjectedProps<T>, TypeaheadMenuPropsPick> {}
export class TypeaheadMenu<T extends TypeaheadModel> extends React.Component<TypeaheadMenuProps<T>> { }
/* ---------------------------------------------------------------------------
Menu Item Props and Component
--------------------------------------------------------------------------- */
export interface BaseMenuItemProps extends React.HTMLProps<'li'> {
active?: boolean;
}
export class BaseMenuItem extends React.Component<BaseMenuItemProps> { }
export interface MenuItemProps<T extends TypeaheadModel> extends BaseMenuItemProps {
option: T;
position: number;
label?: string;
active?: boolean;
className?: string;
disabled?: boolean;
onClick?: (e: Event) => any;
onMouseDown?: (e: Event) => any;
}
export const MenuItem: React.ClassicComponentClass<MenuItemProps<any>>;
export class MenuItem<T extends TypeaheadModel> extends React.Component<MenuItemProps<T>> { }
/* ---------------------------------------------------------------------------
Overlay Props and Component
--------------------------------------------------------------------------- */
export type OverlayTypeaheadProps = Pick<TypeaheadProps<any>, 'align' | 'dropup' | 'flip' | 'onMenuHide' | 'onMenuShow' | 'onMenuToggle'>;
export interface OverlayProps extends OverlayTypeaheadProps {
children?: React.ReactNode;
className?: string;
container: HTMLElement;
referenceElement?: HTMLElement;
show?: boolean;
}
export class Overlay extends React.Component<OverlayProps> { }
/* ---------------------------------------------------------------------------
Token Props and Component
--------------------------------------------------------------------------- */
export interface TokenProps extends React.HTMLProps<'div'> {
active?: boolean;
onRemove?: () => void; // Token does not invoke onRemove with any parameters
}
export class Token extends React.Component<TokenProps> { }

View File

@@ -1,6 +0,0 @@
{
"private": true,
"dependencies": {
"csstype": "^2.2.0"
}
}

View File

@@ -1,25 +1,78 @@
import * as React from 'react';
import { Typeahead, Highlighter, Menu, MenuItem } from 'react-bootstrap-typeahead';
import { ClearButton, Typeahead, Highlighter, Menu, MenuItem, Token } from 'react-bootstrap-typeahead';
const options = [
{ name: 'Alabama', population: 4780127, capital: 'Montgomery', region: 'South' },
{ name: 'Alaska', population: 710249, capital: 'Juneau', region: 'West' },
{ name: 'Arizona', population: 6392307, capital: 'Phoenix', region: 'West' },
{ name: 'Arkansas', population: 2915958, capital: 'Little Rock', region: 'South' },
{ name: 'California', population: 37254503, capital: 'Sacramento', region: 'West' },
{ name: 'Colorado', population: 5029324, capital: 'Denver', region: 'West' },
interface State {
capital: string;
name: string;
population: number;
region: string;
setValue: (value: State) => void;
}
interface GroupedStates {
[key: string]: State[];
}
type StringPropertyNames<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
type StateKeysValid = StringPropertyNames<State>;
const options: State[] = [
{ name: 'Alabama', population: 4780127, capital: 'Montgomery', region: 'South', setValue: () => {} },
{ name: 'Alaska', population: 710249, capital: 'Juneau', region: 'West', setValue: () => {} },
{ name: 'Arizona', population: 6392307, capital: 'Phoenix', region: 'West', setValue: () => {} },
{ name: 'Arkansas', population: 2915958, capital: 'Little Rock', region: 'South', setValue: () => {} },
{ name: 'California', population: 37254503, capital: 'Sacramento', region: 'West', setValue: () => {} },
{ name: 'Colorado', population: 5029324, capital: 'Denver', region: 'West', setValue: () => {} },
];
const stateNames = options.map(o => o.name);
const groups: GroupedStates = options.reduce((accum: GroupedStates, option: State) => {
const optKey = option.name.slice(0, 1).toLowerCase();
if (accum[optKey] !== undefined) {
accum[optKey].push(option);
} else {
accum[optKey] = [option];
}
return accum;
}, {});
class BasicExample extends React.Component {
state = {
multiple: false,
};
genCustomMenu = () => {
const menuItems = Object.keys(groups).reduce((accum, letter) => {
const header = [
<Menu.Divider key={`${letter}-start`} />,
<Menu.Header key={`${letter}-header`}>
{`States starting with: ${letter.toUpperCase()}`}
<ClearButton onClick={() => {}} />
</Menu.Header>,
<Menu.Divider key={`${letter}-end`} />,
];
const states = groups[letter].map((state: State, index: number) => {
return (<MenuItem key={state.name} position={index} option={state}>{state.name}</MenuItem>);
});
return [...accum, ...header, ...states];
}, [] as JSX.Element[]);
return menuItems;
}
render() {
const { multiple } = this.state;
return (
<div>
<Typeahead
options={stateNames}
placeholder="Choose a name"
/>
<Typeahead
options={stateNames}
placeholder="Choose a name"
multiple
filterBy={(option, props) => (props.text.indexOf(option) !== -1)}
/>
<Typeahead
labelKey="name"
multiple={multiple}
@@ -33,7 +86,7 @@ class BasicExample extends React.Component {
multiple={multiple}
options={options}
maxHeight='300px'
filterBy={(option, props) => (props.text.indexOf(option) !== -1) }
filterBy={(option, props) => (props.text.indexOf(option.name) !== -1) }
placeholder="Choose a state..."
/>
<Typeahead
@@ -41,7 +94,7 @@ class BasicExample extends React.Component {
multiple={multiple}
options={options}
maxHeight='300px'
filterBy={(option, {text}) => (text.indexOf(option) !== -1) }
filterBy={(option, {text}) => (text.indexOf(option.name) !== -1) }
placeholder="Choose a state..."
/>
<Typeahead
@@ -49,7 +102,7 @@ class BasicExample extends React.Component {
options={options}
placeholder="Choose a state..."
renderMenuItemChildren={ (option, props, index) =>
<Highlighter key="name" search={props.text}>
<Highlighter key="name" search={props.text || ""}>
{option.name} {index}
</Highlighter>
}
@@ -69,6 +122,23 @@ class BasicExample extends React.Component {
))}
</Menu>
</Typeahead>
<Typeahead
labelKey="name"
options={options}
placeholder="Choose a state..."
renderToken={(selectedItem, props, index) => {
return <Token
active
disabled={false}
tabIndex={5}
href="https://test.com"
onRemove={() => console.log(props.text)}>
{selectedItem.name}<ClearButton onClick={() => {}} />
</Token>;
}}
>
<Menu id="menu-id">{...this.genCustomMenu()}</Menu>
</Typeahead>
</div>
);
}